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 for file in $(ls maa-claims-*.json); do
path=$(realpath "${file}") path=$(realpath "${file}")
cat "${path}" cat "${path}"
bazel run //internal/api/attestationconfigapi -- --maa-claims-path "${path}" bazel run //internal/api/attestationconfigapi/cli -- --maa-claims-path "${path}"
done done

View File

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

View File

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

View File

@ -11,7 +11,6 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"time"
"github.com/edgelesssys/constellation/v2/cli/internal/terraform" "github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
@ -176,6 +175,6 @@ func (s *stubConfigFetcher) FetchAzureSEVSNPVersionList(context.Context, attesta
panic("not implemented") 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 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/resourcemanager/network/armnetwork/v4 v4.0.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.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/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 v1.18.1
github.com/aws/aws-sdk-go-v2/config v1.18.27 github.com/aws/aws-sdk-go-v2/config v1.18.27
github.com/aws/aws-sdk-go-v2/credentials v1.13.26 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/agext/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // 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/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/configsources v1.1.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 // 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/agext/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // 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 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/aws/protocol/eventstream v1.4.10 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.26 // 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-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 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 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 h1:+tefE750oAb7ZQGzla6bLkOwfcQCEtC5y2RqoqCeqKo=
github.com/aws/aws-sdk-go-v2 v1.18.1/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= 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= 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-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-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.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.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 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= 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-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-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.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.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.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= 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/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-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.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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= 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= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=

View File

@ -8,6 +8,7 @@ go_library(
"azure.go", "azure.go",
"client.go", "client.go",
"fetcher.go", "fetcher.go",
"reporter.go",
], ],
importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi", importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi",
visibility = ["//:__subpackages__"], visibility = ["//:__subpackages__"],
@ -19,6 +20,8 @@ go_library(
"//internal/logger", "//internal/logger",
"//internal/sigstore", "//internal/sigstore",
"//internal/staticupload", "//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 = [ srcs = [
"client_test.go", "client_test.go",
"fetcher_test.go", "fetcher_test.go",
"reporter_test.go",
], ],
embed = [":attestationconfigapi"], 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 ( import (
"fmt" "fmt"
"net/url"
"path" "path"
"sort" "sort"
"strings" "strings"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/constants"
) )
// attestationURLPath is the URL path to the attestation versions. // attestationURLPath is the URL path to the attestation versions.
@ -41,11 +39,6 @@ type AzureSEVSNPVersionAPI struct {
AzureSEVSNPVersion 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. // JSONPath returns the path to the JSON file for the request to the config api.
func (i AzureSEVSNPVersionAPI) JSONPath() string { func (i AzureSEVSNPVersionAPI) JSONPath() string {
return path.Join(attestationURLPath, variant.AzureSEVSNP{}.String(), i.Version) 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. // AzureSEVSNPVersionList is the request to list all versions in the config api.
type AzureSEVSNPVersionList []string 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. // JSONPath returns the path to the JSON file for the request to the config api.
func (i AzureSEVSNPVersionList) JSONPath() string { func (i AzureSEVSNPVersionList) JSONPath() string {
return path.Join(attestationURLPath, variant.AzureSEVSNP{}.String(), "list") return path.Join(attestationURLPath, variant.AzureSEVSNP{}.String(), "list")
@ -94,16 +82,3 @@ func (i AzureSEVSNPVersionList) Validate() error {
} }
return nil 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/constants",
"//internal/logger", "//internal/logger",
"//internal/staticupload", "//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", "@com_github_spf13_cobra//:cobra",
"@org_uber_go_zap//:zap", "@org_uber_go_zap//:zap",
], ],
@ -28,13 +31,9 @@ go_library(
go_test( go_test(
name = "cli_test", name = "cli_test",
srcs = [ srcs = ["delete_test.go"],
"delete_test.go",
"main_test.go",
],
embed = [":cli_lib"], embed = [":cli_lib"],
deps = [ deps = [
"//internal/api/attestationconfigapi",
"@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require", "@com_github_stretchr_testify//require",
], ],

View File

@ -10,6 +10,9 @@ import (
"errors" "errors"
"fmt" "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/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/staticupload" "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)") cmd.Flags().StringP("version", "v", "", "Name of the version to delete (without .json suffix)")
must(cmd.MarkFlagRequired("version")) must(cmd.MarkFlagRequired("version"))
recursivelyCmd := &cobra.Command{
Use: "recursive",
Short: "delete all objects from the API path",
RunE: runRecursiveDelete,
}
cmd.AddCommand(recursivelyCmd)
return cmd return cmd
} }
@ -59,17 +69,19 @@ func runDelete(cmd *cobra.Command, _ []string) (retErr error) {
return fmt.Errorf("getting bucket: %w", err) return fmt.Errorf("getting bucket: %w", err)
} }
distribution, err := cmd.Flags().GetString("distribution") testing, err := cmd.Flags().GetBool("testing")
if err != nil { if err != nil {
return fmt.Errorf("getting distribution: %w", err) return fmt.Errorf("getting testing flag: %w", err)
} }
_, distribution := getEnvironment(testing)
cfg := staticupload.Config{ cfg := staticupload.Config{
Bucket: bucket, Bucket: bucket,
Region: region, Region: region,
DistributionID: distribution, 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 { if err != nil {
return fmt.Errorf("create attestation client: %w", err) return fmt.Errorf("create attestation client: %w", err)
} }
@ -85,3 +97,64 @@ func runDelete(cmd *cobra.Command, _ []string) (retErr error) {
} }
return deleteCmd.delete(cmd) 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@@) configapi_cli=$(realpath @@CONFIGAPI_CLI@@)
stat "${configapi_cli}" >> /dev/null stat "${configapi_cli}" >> /dev/null
configapi_cli="${configapi_cli} --testing"
###### script body ###### ###### script body ######
readonly region="eu-west-1" readonly region="eu-west-1"
readonly bucket="resource-api-testing" readonly bucket="resource-api-testing"
readonly distribution="ETZGUP1CWRC2P"
tmpdir=$(mktemp -d) tmpdir=$(mktemp -d)
readonly tmpdir readonly tmpdir
registerExitHandler "rm -rf $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" readonly claim_path="$tmpdir/maaClaim.json"
cat << EOF > "$claim_path" cat << EOF > "$claim_path"
{ {
"x-ms-isolation-tee": { "x-ms-isolation-tee": {
"x-ms-sevsnpvm-tee-svn": 1, "x-ms-sevsnpvm-tee-svn": 255,
"x-ms-sevsnpvm-snpfw-svn": 9, "x-ms-sevsnpvm-snpfw-svn": 255,
"x-ms-sevsnpvm-microcode-svn": 116, "x-ms-sevsnpvm-microcode-svn": 255,
"x-ms-sevsnpvm-bootloader-svn": 4 "x-ms-sevsnpvm-bootloader-svn": 255
} }
} }
EOF EOF
readonly date="2023-02-02-03-04" # has an older version
${configapi_cli} --maa-claims-path "$claim_path" --upload-date "$date" --region "$region" --bucket "$bucket" --distribution "$distribution" 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" baseurl="https://d33dzgxuwsgbpw.cloudfront.net/constellation/v1/attestation/azure-sev-snp"
if ! curl -fsSL ${baseurl}/${date}.json > /dev/null; then if ! curl -fsSL ${baseurl}/${date_oldest}.json > version.json; then
echo "Checking for uploaded version file constellation/v1/attestation/azure-sev-snp/${date}.json: request returned ${?}" echo "Checking for uploaded version file constellation/v1/attestation/azure-sev-snp/${date_oldest}.json: request returned ${?}"
exit 1 exit 1
fi fi
# check that version values are equal to expected
if ! curl -fsSL ${baseurl}/${date}.json.sig > /dev/null; then if ! cmp -s <(echo -n '{"bootloader":255,"tee":255,"snp":255,"microcode":254}') version.json; then
echo "Checking for uploaded version signature file constellation/v1/attestation/azure-sev-snp/${date}.json.sig: request returned ${?}" 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 exit 1
fi fi
if ! curl -fsSL ${baseurl}/${date_oldest}.json.sig > /dev/null; then
if ! curl -fsSL ${baseurl}/list > /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 ${?}" echo "Checking for uploaded list file constellation/v1/attestation/azure-sev-snp/list: request returned ${?}"
exit 1 exit 1
fi fi
${configapi_cli} delete --version "$date" --region "$region" --bucket "$bucket" --distribution "$distribution" # 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
# Omit -f to check for 404. We want to check that a file was deleted, therefore we expect the query to fail. echo "The list content:"
http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null ${baseurl}/${date}.json) cat list.json
if [[ $http_code -ne 404 ]]; then echo " is not equal to the expected version content:"
echo "Expected HTTP code 404 for: constellation/v1/attestation/azure-sev-snp/${date}.json, but got ${http_code}" 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 exit 1
fi 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) http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null ${baseurl}/${date}.json.sig)
if [[ $http_code -ne 404 ]]; then if [[ $http_code -ne 404 ]]; then
echo "Expected HTTP code 404 for: constellation/v1/attestation/azure-sev-snp/${date}.json, but got ${http_code}" echo "Expected HTTP code 404 for: constellation/v1/attestation/azure-sev-snp/${date}.json, but got ${http_code}"
exit 1 exit 1
fi 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. 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`. 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. The CLI is used in the CI pipeline. Manual actions that change the bucket's data shouldn't be necessary.
Notice that there is no synchronization on API operations. 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 package main
@ -35,6 +36,8 @@ const (
distributionID = constants.CDNDefaultDistributionID distributionID = constants.CDNDefaultDistributionID
envCosignPwd = "COSIGN_PASSWORD" envCosignPwd = "COSIGN_PASSWORD"
envCosignPrivateKey = "COSIGN_PRIVATE_KEY" 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 ( var (
@ -53,10 +56,10 @@ func main() {
// newRootCmd creates the root command. // newRootCmd creates the root command.
func newRootCmd() *cobra.Command { func newRootCmd() *cobra.Command {
rootCmd := &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.", 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)"+ "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.", "to be able to upload to S3. Set the %s and %s environment variables to authenticate with cosign.",
envCosignPrivateKey, envCosignPwd, 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("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().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("region", "r", awsRegion, "region of the targeted bucket.")
rootCmd.PersistentFlags().StringP("bucket", "b", awsBucket, "bucket targeted by all operations.") 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")) must(rootCmd.MarkFlagRequired("maa-claims-path"))
rootCmd.AddCommand(newDeleteCmd()) rootCmd.AddCommand(newDeleteCmd())
return rootCmd return rootCmd
@ -110,23 +116,8 @@ func runCmd(cmd *cobra.Command, _ []string) (retErr error) {
inputVersion := maaTCB.ToAzureSEVSNPVersion() inputVersion := maaTCB.ToAzureSEVSNPVersion()
log.Infof("Input version: %+v", inputVersion) log.Infof("Input version: %+v", inputVersion)
latestAPIVersionAPI, err := attestationconfigapi.NewFetcher().FetchAzureSEVSNPVersionLatest(ctx, flags.uploadDate) client, clientClose, err := attestationconfigapi.NewClient(ctx, cfg,
if err != nil { []byte(cosignPwd), []byte(privateKey), false, flags.cacheWindowSize, log)
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)
defer func() { defer func() {
err := clientClose(cmd.Context()) err := clientClose(cmd.Context())
if err != nil { if err != nil {
@ -138,64 +129,98 @@ func runCmd(cmd *cobra.Command, _ []string) (retErr error) {
return fmt.Errorf("creating client: %w", err) return fmt.Errorf("creating client: %w", err)
} }
if err := client.UploadAzureSEVSNPVersion(ctx, inputVersion, flags.uploadDate); err != nil { latestAPIVersionAPI, err := attestationconfigapi.NewFetcherWithCustomCDNAndCosignKey(flags.url, constants.CosignPublicKeyDev).FetchAzureSEVSNPVersionLatest(ctx)
return fmt.Errorf("uploading version: %w", err) 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 return nil
} }
type cliFlags struct { type config struct {
maaFilePath string maaFilePath string
uploadDate time.Time uploadDate time.Time
region string region string
bucket string bucket string
distribution 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") maaFilePath, err := cmd.Flags().GetString("maa-claims-path")
if err != nil { 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") dateStr, err := cmd.Flags().GetString("upload-date")
if err != nil { if err != nil {
return cliFlags{}, fmt.Errorf("getting upload date: %w", err) return config{}, fmt.Errorf("getting upload date: %w", err)
} }
uploadDate := time.Now() uploadDate := time.Now()
if dateStr != "" { if dateStr != "" {
uploadDate, err = time.Parse(attestationconfigapi.VersionFormat, dateStr) uploadDate, err = time.Parse(attestationconfigapi.VersionFormat, dateStr)
if err != nil { 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") region, err := cmd.Flags().GetString("region")
if err != nil { 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") bucket, err := cmd.Flags().GetString("bucket")
if err != nil { 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 { 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{ 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, maaFilePath: maaFilePath,
uploadDate: uploadDate, uploadDate: uploadDate,
region: region, region: region,
bucket: bucket, bucket: bucket,
url: url,
distribution: distribution, distribution: distribution,
force: force,
cacheWindowSize: cacheWindowSize,
}, nil }, 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. // maaTokenTCBClaims describes the TCB information in a MAA token.
type maaTokenTCBClaims struct { type maaTokenTCBClaims struct {
IsolationTEE 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) { func must(err error) {
if err != nil { if err != nil {
panic(err) panic(err)

View File

@ -7,6 +7,7 @@ package attestationconfigapi
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"time" "time"
@ -14,6 +15,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/sigstore" "github.com/edgelesssys/constellation/v2/internal/sigstore"
"github.com/edgelesssys/constellation/v2/internal/staticupload" "github.com/edgelesssys/constellation/v2/internal/staticupload"
) )
@ -26,10 +28,11 @@ type Client struct {
s3ClientClose func(ctx context.Context) error s3ClientClose func(ctx context.Context) error
bucketID string bucketID string
signer sigstore.Signer signer sigstore.Signer
cacheWindowSize int
} }
// NewClient returns a new Client. // 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) s3Client, clientClose, err := apiclient.NewClient(ctx, cfg.Region, cfg.Bucket, cfg.DistributionID, dryRun, log)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to create s3 storage: %w", err) return nil, nil, fmt.Errorf("failed to create s3 storage: %w", err)
@ -40,12 +43,13 @@ func NewClient(ctx context.Context, cfg staticupload.Config, cosignPwd, privateK
s3ClientClose: clientClose, s3ClientClose: clientClose,
signer: sigstore.NewSigner(cosignPwd, privateKey), signer: sigstore.NewSigner(cosignPwd, privateKey),
bucketID: cfg.Bucket, bucketID: cfg.Bucket,
cacheWindowSize: versionWindowSize,
} }
return repo, clientClose, nil return repo, clientClose, nil
} }
// UploadAzureSEVSNPVersion uploads the latest version numbers of the Azure SEVSNP. Then version name is the UTC timestamp of the date. The /list entry stores the version name + .json suffix. // 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 { func (a Client) uploadAzureSEVSNPVersion(ctx context.Context, version AzureSEVSNPVersion, date time.Time) error {
versions, err := a.List(ctx, variant.AzureSEVSNP{}) versions, err := a.List(ctx, variant.AzureSEVSNP{})
if err != nil { if err != nil {
return fmt.Errorf("fetch version list: %w", err) 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{}) { if attestation.Equal(variant.AzureSEVSNP{}) {
versions, err := apiclient.Fetch(ctx, a.s3Client, AzureSEVSNPVersionList{}) versions, err := apiclient.Fetch(ctx, a.s3Client, AzureSEVSNPVersionList{})
if err != nil { if err != nil {
var notFoundErr *apiclient.NotFoundError
if errors.As(err, &notFoundErr) {
return nil, nil
}
return nil, err return nil, err
} }
return versions, nil return versions, nil

View File

@ -8,97 +8,93 @@ package attestationconfigapi
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings"
"time"
apifetcher "github.com/edgelesssys/constellation/v2/internal/api/fetcher" apifetcher "github.com/edgelesssys/constellation/v2/internal/api/fetcher"
"github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/sigstore" "github.com/edgelesssys/constellation/v2/internal/sigstore"
) )
// minimumAgeVersion is the minimum age to accept the version as latest.
const minimumAgeVersion = 14 * 24 * time.Hour
const cosignPublicKey = constants.CosignPublicKeyReleases 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. // Fetcher fetches config API resources without authentication.
type Fetcher interface { type Fetcher interface {
FetchAzureSEVSNPVersion(ctx context.Context, azureVersion AzureSEVSNPVersionAPI) (AzureSEVSNPVersionAPI, error) FetchAzureSEVSNPVersion(ctx context.Context, azureVersion AzureSEVSNPVersionAPI) (AzureSEVSNPVersionAPI, error)
FetchAzureSEVSNPVersionList(ctx context.Context, attestation AzureSEVSNPVersionList) (AzureSEVSNPVersionList, 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. // fetcher fetches AttestationCfg API resources without authentication.
type fetcher struct { type fetcher struct {
apifetcher.HTTPClient apifetcher.HTTPClient
cdnURL string
verifier sigstore.Verifier verifier sigstore.Verifier
} }
// NewFetcher returns a new apifetcher. // NewFetcher returns a new apifetcher.
func NewFetcher() Fetcher { 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. // 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)) verifier, err := sigstore.NewCosignVerifier([]byte(cosignPublicKey))
if err != nil { 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. // 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)) 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 { func newFetcherWithClientAndVerifier(client apifetcher.HTTPClient, cosignVerifier sigstore.Verifier, url string) Fetcher {
return &fetcher{client, cosignVerifier} return &fetcher{HTTPClient: client, verifier: cosignVerifier, cdnURL: url}
} }
// FetchAzureSEVSNPVersionList fetches the version list information from the config API. // FetchAzureSEVSNPVersionList fetches the version list information from the config API.
func (f *fetcher) FetchAzureSEVSNPVersionList(ctx context.Context, attestation AzureSEVSNPVersionList) (AzureSEVSNPVersionList, error) { 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. // 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. // FetchAzureSEVSNPVersion fetches the version information from the config API.
func (f *fetcher) FetchAzureSEVSNPVersion(ctx context.Context, azureVersion AzureSEVSNPVersionAPI) (AzureSEVSNPVersionAPI, error) { 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 { 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 return fetchedVersion, nil
} }
// FetchAzureSEVSNPVersionLatest returns the latest versions of the given type. // 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 var list AzureSEVSNPVersionList
list, err = f.FetchAzureSEVSNPVersionList(ctx, list) list, err = f.FetchAzureSEVSNPVersionList(ctx, list)
if err != nil { if err != nil {
return res, fmt.Errorf("fetching versions list: %w", err) return res, ErrNoVersionsFound
} }
getVersionRequest, err := getLatestVersionOlderThanMinimumAge(list, now, minimumAgeVersion) if len(list) < 1 {
if err != nil { return res, ErrNoVersionsFound
return res, fmt.Errorf("finding latest valid version: %w", err) }
getVersionRequest := AzureSEVSNPVersionAPI{
Version: list[0], // latest version is first in list
} }
res, err = f.FetchAzureSEVSNPVersion(ctx, getVersionRequest) res, err = f.FetchAzureSEVSNPVersion(ctx, getVersionRequest)
if err != nil { if err != nil {
return res, fmt.Errorf("fetching version: %w", err) return res, err
} }
return 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" "testing"
"time" "time"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestFetchLatestAzureSEVSNPVersion(t *testing.T) { func TestFetchLatestAzureSEVSNPVersion(t *testing.T) {
now := time.Date(2023, 6, 12, 0, 0, 0, 0, time.UTC)
latestStr := "2023-06-11-14-09.json" latestStr := "2023-06-11-14-09.json"
olderStr := "2019-01-01-01-01.json" olderStr := "2019-01-01-01-01.json"
testcases := map[string]struct { testcases := map[string]struct {
@ -29,21 +29,10 @@ func TestFetchLatestAzureSEVSNPVersion(t *testing.T) {
wantErr bool wantErr bool
want AzureSEVSNPVersionAPI want AzureSEVSNPVersionAPI
}{ }{
"get latest version if older than 2 weeks": { "get latest version": {
fetcherVersions: []string{latestStr, olderStr}, fetcherVersions: []string{latestStr, olderStr},
timeAtTest: now.Add(days(15)),
want: latestVersion, 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 { for name, tc := range testcases {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
@ -54,8 +43,8 @@ func TestFetchLatestAzureSEVSNPVersion(t *testing.T) {
olderVersion: olderStr, olderVersion: olderStr,
}, },
} }
fetcher := newFetcherWithClientAndVerifier(client, dummyVerifier{}) fetcher := newFetcherWithClientAndVerifier(client, dummyVerifier{}, constants.CDNRepositoryURL)
res, err := fetcher.FetchAzureSEVSNPVersionLatest(context.Background(), tc.timeAtTest) res, err := fetcher.FetchAzureSEVSNPVersionLatest(context.Background())
assert := assert.New(t) assert := assert.New(t)
if tc.wantErr { if tc.wantErr {
assert.Error(err) 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 { type fakeConfigAPIHandler struct {
versions []string versions []string
latestVersion 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 Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
*/ */
package attestationconfigapi
package main
import ( import (
"testing" "testing"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestIsInputNewerThanLatestAPI(t *testing.T) { func TestIsInputNewerThanLatestAPI(t *testing.T) {
newTestCfg := func() attestationconfigapi.AzureSEVSNPVersion { newTestCfg := func() AzureSEVSNPVersion {
return attestationconfigapi.AzureSEVSNPVersion{ return AzureSEVSNPVersion{
Microcode: 93, Microcode: 93,
TEE: 0, TEE: 0,
SNP: 6, SNP: 6,
@ -24,13 +21,13 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) {
} }
testCases := map[string]struct { testCases := map[string]struct {
latest attestationconfigapi.AzureSEVSNPVersion latest AzureSEVSNPVersion
input attestationconfigapi.AzureSEVSNPVersion input AzureSEVSNPVersion
expect bool expect bool
errMsg string errMsg string
}{ }{
"input is older than latest": { "input is older than latest": {
input: func(c attestationconfigapi.AzureSEVSNPVersion) attestationconfigapi.AzureSEVSNPVersion { input: func(c AzureSEVSNPVersion) AzureSEVSNPVersion {
c.Microcode-- c.Microcode--
return c return c
}(newTestCfg()), }(newTestCfg()),
@ -39,7 +36,7 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) {
errMsg: "input Microcode version: 92 is older than latest API version: 93", errMsg: "input Microcode version: 92 is older than latest API version: 93",
}, },
"input has greater and smaller version field than latest": { "input has greater and smaller version field than latest": {
input: func(c attestationconfigapi.AzureSEVSNPVersion) attestationconfigapi.AzureSEVSNPVersion { input: func(c AzureSEVSNPVersion) AzureSEVSNPVersion {
c.Microcode++ c.Microcode++
c.Bootloader-- c.Bootloader--
return c return c
@ -49,7 +46,7 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) {
errMsg: "input Bootloader version: 1 is older than latest API version: 2", errMsg: "input Bootloader version: 1 is older than latest API version: 2",
}, },
"input is newer than latest": { "input is newer than latest": {
input: func(c attestationconfigapi.AzureSEVSNPVersion) attestationconfigapi.AzureSEVSNPVersion { input: func(c AzureSEVSNPVersion) AzureSEVSNPVersion {
c.TEE++ c.TEE++
return c return c
}(newTestCfg()), }(newTestCfg()),
@ -64,7 +61,7 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) {
} }
for name, tc := range testCases { for name, tc := range testCases {
t.Run(name, func(t *testing.T) { 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) assert := assert.New(t)
if tc.errMsg != "" { if tc.errMsg != "" {
assert.EqualError(err, tc.errMsg) assert.EqualError(err, tc.errMsg)

View File

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

View File

@ -23,9 +23,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"strings" "strings"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/sigstore" "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 fetches the given apiObject from the public Constellation CDN.
// Fetch does not require authentication. // 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 { if err := obj.ValidateRequest(); err != nil {
return *new(T), fmt.Errorf("validating request for %T: %w", obj, err) return *new(T), fmt.Errorf("validating request for %T: %w", obj, err)
} }
url, err := obj.URL() urlObj, err := url.Parse(cdnURL)
if err != nil { 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) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if err != nil { 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. // 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. // 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. // 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) { func FetchAndVerify[T apiObject](ctx context.Context, c HTTPClient, cdnURL string, obj T, cosignVerifier sigstore.Verifier) (T, error) {
fetchedObj, err := Fetch(ctx, c, obj) fetchedObj, err := Fetch(ctx, c, cdnURL, obj)
if err != nil { if err != nil {
return fetchedObj, fmt.Errorf("fetching object: %w", err) 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 { if err != nil {
return fetchedObj, fmt.Errorf("marshalling object: %w", err) return fetchedObj, fmt.Errorf("marshalling object: %w", err)
} }
signature, err := Fetch(ctx, c, cdnURL, signature{Signed: obj.JSONPath()})
url, err := obj.URL()
if err != nil {
return fetchedObj, fmt.Errorf("getting signed URL: %w", err)
}
signature, err := Fetch(ctx, c, signature{Signed: url})
if err != nil { if err != nil {
return fetchedObj, fmt.Errorf("fetching signature: %w", err) return fetchedObj, fmt.Errorf("fetching signature: %w", err)
} }
err = cosignVerifier.VerifySignature(marshalledObj, signature.Signature) err = cosignVerifier.VerifySignature(marshalledObj, signature.Signature)
if err != nil { if err != nil {
return fetchedObj, fmt.Errorf("verifying signature: %w", err) return fetchedObj, fmt.Errorf("verifying signature: %w", err)
@ -127,28 +123,24 @@ type HTTPClient interface {
type apiObject interface { type apiObject interface {
ValidateRequest() error ValidateRequest() error
Validate() error Validate() error
URL() (string, error) JSONPath() string
} }
// signature manages the signature of a object saved at location 'Signed'. // signature manages the signature of a object saved at location 'Signed'.
type signature struct { type signature struct {
// Signed is the object that is signed. // Signed is the object that is signed.
Signed string `json:"signed"` Signed string `json:"-"`
// Signature is the signature of `Signed`. // Signature is the signature of `Signed`.
Signature []byte `json:"signature"` Signature []byte `json:"signature"`
} }
// URL returns the URL for the request to the config api. // URL returns the URL for the request to the config api.
func (s signature) URL() (string, error) { func (s signature) JSONPath() string {
return s.Signed + ".sig", nil return s.Signed + ".sig"
} }
// ValidateRequest validates the request. // ValidateRequest validates the request.
func (s signature) ValidateRequest() error { 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") { if !strings.HasSuffix(s.Signed, ".json") {
return errors.New("signed object missing .json suffix") return errors.New("signed object missing .json suffix")
} }

View File

@ -10,34 +10,36 @@ import (
"context" "context"
"github.com/edgelesssys/constellation/v2/internal/api/fetcher" "github.com/edgelesssys/constellation/v2/internal/api/fetcher"
"github.com/edgelesssys/constellation/v2/internal/constants"
) )
// Fetcher fetches version API resources without authentication. // Fetcher fetches version API resources without authentication.
type Fetcher struct { type Fetcher struct {
fetcher.HTTPClient fetcher.HTTPClient
cdnURL string
} }
// NewFetcher returns a new Fetcher. // NewFetcher returns a new Fetcher.
func NewFetcher() *Fetcher { func NewFetcher() *Fetcher {
return &Fetcher{fetcher.NewHTTPClient()} return &Fetcher{fetcher.NewHTTPClient(), constants.CDNRepositoryURL}
} }
// FetchVersionList fetches the given version list from the versions API. // FetchVersionList fetches the given version list from the versions API.
func (f *Fetcher) FetchVersionList(ctx context.Context, list List) (List, error) { 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. // FetchVersionLatest fetches the latest version from the versions API.
func (f *Fetcher) FetchVersionLatest(ctx context.Context, latest Latest) (Latest, error) { 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. // FetchImageInfo fetches the given image info from the versions API.
func (f *Fetcher) FetchImageInfo(ctx context.Context, imageInfo ImageInfo) (ImageInfo, error) { 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. // FetchCLIInfo fetches the given cli info from the versions API.
func (f *Fetcher) FetchCLIInfo(ctx context.Context, cliInfo CLIInfo) (CLIInfo, error) { 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" "net/http"
"testing" "testing"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/goleak" "go.uber.org/goleak"
@ -189,7 +190,7 @@ func TestFetchVersionList(t *testing.T) {
return tc.serverResp return tc.serverResp
}) })
fetcher := Fetcher{client} fetcher := Fetcher{client, constants.CDNRepositoryURL}
list, err := fetcher.FetchVersionList(context.Background(), tc.list) list, err := fetcher.FetchVersionList(context.Background(), tc.list)

View File

@ -9,7 +9,6 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"time"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest" "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. // 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 { func (c *AzureSEVSNP) FetchAndSetLatestVersionNumbers(ctx context.Context, fetcher attestationconfigapi.Fetcher) error {
versions, err := fetcher.FetchAzureSEVSNPVersionLatest(ctx, now) versions, err := fetcher.FetchAzureSEVSNPVersionLatest(ctx)
if err != nil { if err != nil {
return err return err
} }

View File

@ -26,7 +26,6 @@ import (
"os" "os"
"reflect" "reflect"
"strings" "strings"
"time"
"github.com/go-playground/locales/en" "github.com/go-playground/locales/en"
ut "github.com/go-playground/universal-translator" 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 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 return c, err
} }
} }

View File

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