From 3fde118b335725e6746f2c43a0f35b52921aeff7 Mon Sep 17 00:00:00 2001 From: Adrian Stobbe Date: Fri, 9 Jun 2023 12:48:12 +0200 Subject: [PATCH] config: enable azure snp version fetcher again + minimum age for latest version (#1899) * fetch latest version when older than 2 weeks * extend hack upload tool to pass an upload date * Revert "config: disable user-facing version Azure SEV SNP fetch for v2.8 (#1882)" This reverts commit c7b22d314a35fa260b97bf156989328caf1c384b. * fix tests * use NewAzureSEVSNPVersionList for type guarantees * Revert "use NewAzureSEVSNPVersionList for type guarantees" This reverts commit 942566453f4b4a2b6dc16f8689248abf1dc47db4. * assure list is sorted * improve root.go style * daniel feedback --- .../cmd/configfetchmeasurements_test.go | 3 +- hack/configapi/cmd/root.go | 53 +++++-- internal/api/attestationconfigapi/azure.go | 6 + internal/api/attestationconfigapi/client.go | 5 +- internal/api/attestationconfigapi/fetcher.go | 42 +++++- .../api/attestationconfigapi/fetcher_test.go | 129 ++++++++++++------ .../attestation/azure/snp/validator_test.go | 10 -- internal/config/attestationversion.go | 12 +- internal/config/attestationversion_test.go | 37 ----- internal/config/config.go | 33 ++--- internal/config/config_test.go | 90 ++++++------ internal/sigstore/verify.go | 5 + rfc/attestation-config.md | 3 + 13 files changed, 239 insertions(+), 189 deletions(-) diff --git a/cli/internal/cmd/configfetchmeasurements_test.go b/cli/internal/cmd/configfetchmeasurements_test.go index c99813f89..2d18ca183 100644 --- a/cli/internal/cmd/configfetchmeasurements_test.go +++ b/cli/internal/cmd/configfetchmeasurements_test.go @@ -15,6 +15,7 @@ import ( "net/url" "strconv" "testing" + "time" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/api/versionsapi" @@ -314,7 +315,7 @@ func (f stubAttestationFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ att }, nil } -func (f stubAttestationFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context) (attestationconfigapi.AzureSEVSNPVersionAPI, error) { +func (f stubAttestationFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context, _ time.Time) (attestationconfigapi.AzureSEVSNPVersionAPI, error) { return attestationconfigapi.AzureSEVSNPVersionAPI{ AzureSEVSNPVersion: testCfg, }, nil diff --git a/hack/configapi/cmd/root.go b/hack/configapi/cmd/root.go index 2272930a2..99e08ea88 100644 --- a/hack/configapi/cmd/root.go +++ b/hack/configapi/cmd/root.go @@ -56,6 +56,7 @@ func newRootCmd() *cobra.Command { } rootCmd.Flags().StringVarP(&versionFilePath, "version-file", "f", "", "File path to the version json file.") rootCmd.Flags().BoolVar(&force, "force", false, "force to upload version regardless of comparison to latest API value.") + rootCmd.Flags().StringP("upload-date", "d", "", "upload a version with this date as version name. Setting it implies --force.") must(enforceRequiredFlags(rootCmd, "version-file")) rootCmd.AddCommand(newDeleteCmd()) return rootCmd @@ -85,21 +86,40 @@ func runCmd(cmd *cobra.Command, _ []string) error { return fmt.Errorf("unmarshalling version file: %w", err) } - latestAPIVersion, err := attestationconfigapi.NewFetcher().FetchAzureSEVSNPVersionLatest(ctx) + dateStr, err := cmd.Flags().GetString("upload-date") if err != nil { - return fmt.Errorf("fetching latest version: %w", err) + return fmt.Errorf("getting upload date: %w", err) + } + var uploadDate time.Time + if dateStr != "" { + uploadDate, err = time.Parse("2006-01-01-01-01", dateStr) + if err != nil { + return fmt.Errorf("parsing date: %w", err) + } + } else { + uploadDate = time.Now() + force = true } - isNewer, err := isInputNewerThanLatestAPI(inputVersion, latestAPIVersion.AzureSEVSNPVersion) - if err != nil { - return fmt.Errorf("comparing versions: %w", err) - } - if isNewer || force { - if force { - cmd.Println("Forcing upload of new version") - } else { - cmd.Printf("Input version: %+v is newer than latest API version: %+v\n", inputVersion, latestAPIVersion) + doUpload := false + if !force { + latestAPIVersion, err := attestationconfigapi.NewFetcher().FetchAzureSEVSNPVersionLatest(ctx, time.Now()) + if err != nil { + return fmt.Errorf("fetching latest version: %w", err) } + + isNewer, err := isInputNewerThanLatestAPI(inputVersion, latestAPIVersion.AzureSEVSNPVersion) + if err != nil { + return fmt.Errorf("comparing versions: %w", err) + } + cmd.Print(versionComparisonInformation(isNewer, inputVersion, latestAPIVersion.AzureSEVSNPVersion)) + doUpload = isNewer + } else { + doUpload = true + cmd.Println("Forcing upload of new version") + } + + if doUpload { sut, sutClose, err := attestationconfigapi.NewClient(ctx, cfg, []byte(cosignPwd), []byte(privateKey), false, log()) defer func() { if err := sutClose(ctx); err != nil { @@ -110,16 +130,21 @@ func runCmd(cmd *cobra.Command, _ []string) error { return fmt.Errorf("creating repo: %w", err) } - if err := sut.UploadAzureSEVSNP(ctx, inputVersion, time.Now()); err != nil { + if err := sut.UploadAzureSEVSNP(ctx, inputVersion, uploadDate); err != nil { return fmt.Errorf("uploading version: %w", err) } cmd.Printf("Successfully uploaded new Azure SEV-SNP version: %+v\n", inputVersion) - } else { - cmd.Printf("Input version: %+v is not newer than latest API version: %+v\n", inputVersion, latestAPIVersion) } return nil } +func versionComparisonInformation(isNewer bool, inputVersion attestationconfigapi.AzureSEVSNPVersion, latestAPIVersion attestationconfigapi.AzureSEVSNPVersion) string { + if isNewer { + return fmt.Sprintf("Input version: %+v is newer than latest API version: %+v\n", inputVersion, latestAPIVersion) + } + return fmt.Sprintf("Input version: %+v is not newer than latest API version: %+v\n", inputVersion, latestAPIVersion) +} + // 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) { inputValues := reflect.ValueOf(input) diff --git a/internal/api/attestationconfigapi/azure.go b/internal/api/attestationconfigapi/azure.go index ce9aedf59..a350ceffd 100644 --- a/internal/api/attestationconfigapi/azure.go +++ b/internal/api/attestationconfigapi/azure.go @@ -10,6 +10,7 @@ import ( "fmt" "net/url" "path" + "sort" "strings" "github.com/edgelesssys/constellation/v2/internal/constants" @@ -110,6 +111,11 @@ func (i AzureSEVSNPVersionList) ValidateRequest() error { return nil } +// SortAzureSEVSNPVersionList sorts the list of versions in reverse order. +func SortAzureSEVSNPVersionList(versions AzureSEVSNPVersionList) { + sort.Sort(sort.Reverse(sort.StringSlice(versions))) +} + // Validate validates the response. func (i AzureSEVSNPVersionList) Validate() error { if len(i) < 1 { diff --git a/internal/api/attestationconfigapi/client.go b/internal/api/attestationconfigapi/client.go index 86bf64732..98f8c01a8 100644 --- a/internal/api/attestationconfigapi/client.go +++ b/internal/api/attestationconfigapi/client.go @@ -9,7 +9,6 @@ import ( "context" "encoding/json" "fmt" - "sort" "time" apiclient "github.com/edgelesssys/constellation/v2/internal/api/client" @@ -43,7 +42,7 @@ func NewClient(ctx context.Context, cfg staticupload.Config, cosignPwd, privateK return repo, clientClose, nil } -// UploadAzureSEVSNP uploads the latest version numbers of the Azure SEVSNP. +// UploadAzureSEVSNP 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) UploadAzureSEVSNP(ctx context.Context, version AzureSEVSNPVersion, date time.Time) error { versions, err := a.List(ctx, variant.AzureSEVSNP{}) if err != nil { @@ -181,6 +180,6 @@ func executeAllCmds(ctx context.Context, client *apiclient.Client, cmds []crudCm func addVersion(versions []string, newVersion string) []string { versions = append(versions, newVersion) versions = variant.RemoveDuplicate(versions) - sort.Sort(sort.Reverse(sort.StringSlice(versions))) + SortAzureSEVSNPVersionList(versions) return versions } diff --git a/internal/api/attestationconfigapi/fetcher.go b/internal/api/attestationconfigapi/fetcher.go index 190a7a889..7f8cdb0f3 100644 --- a/internal/api/attestationconfigapi/fetcher.go +++ b/internal/api/attestationconfigapi/fetcher.go @@ -10,24 +10,30 @@ import ( "context" "encoding/json" "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 // 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) (AzureSEVSNPVersionAPI, error) + FetchAzureSEVSNPVersionLatest(ctx context.Context, now time.Time) (AzureSEVSNPVersionAPI, error) } // fetcher fetches AttestationCfg API resources without authentication. type fetcher struct { apifetcher.HTTPClient + verifier sigstore.Verifier } // NewFetcher returns a new apifetcher. @@ -37,7 +43,11 @@ func NewFetcher() Fetcher { // NewFetcherWithClient returns a new fetcher with custom http client. func NewFetcherWithClient(client apifetcher.HTTPClient) Fetcher { - return &fetcher{client} + return newFetcherWithClientAndVerifier(client, sigstore.CosignVerifier{}) +} + +func newFetcherWithClientAndVerifier(client apifetcher.HTTPClient, cosignVerifier sigstore.Verifier) Fetcher { + return &fetcher{client, cosignVerifier} } // FetchAzureSEVSNPVersionList fetches the version list information from the config API. @@ -63,7 +73,7 @@ func (f *fetcher) FetchAzureSEVSNPVersion(ctx context.Context, azureVersion Azur return fetchedVersion, fmt.Errorf("fetch version %s signature: %w", azureVersion.Version, err) } - err = sigstore.CosignVerifier{}.VerifySignature(versionBytes, signature.Signature, []byte(cosignPublicKey)) + err = f.verifier.VerifySignature(versionBytes, signature.Signature, []byte(cosignPublicKey)) if err != nil { return fetchedVersion, fmt.Errorf("verify version %s signature: %w", azureVersion.Version, err) } @@ -71,16 +81,34 @@ func (f *fetcher) FetchAzureSEVSNPVersion(ctx context.Context, azureVersion Azur } // FetchAzureSEVSNPVersionLatest returns the latest versions of the given type. -func (f *fetcher) FetchAzureSEVSNPVersionLatest(ctx context.Context) (res AzureSEVSNPVersionAPI, err error) { +func (f *fetcher) FetchAzureSEVSNPVersionLatest(ctx context.Context, now time.Time) (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) } - get := AzureSEVSNPVersionAPI{Version: list[0]} // get latest version (as sorted reversely alphanumerically) - get, err = f.FetchAzureSEVSNPVersion(ctx, get) + getVersionRequest, err := getLatestVersionOlderThanMinimumAge(list, now, minimumAgeVersion) + if err != nil { + return res, fmt.Errorf("finding latest valid version: %w", err) + } + res, err = f.FetchAzureSEVSNPVersion(ctx, getVersionRequest) if err != nil { return res, fmt.Errorf("fetching version: %w", err) } - return get, nil + 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("2006-01-01-01-01", 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") } diff --git a/internal/api/attestationconfigapi/fetcher_test.go b/internal/api/attestationconfigapi/fetcher_test.go index d0e9d0020..b5d6a2e45 100644 --- a/internal/api/attestationconfigapi/fetcher_test.go +++ b/internal/api/attestationconfigapi/fetcher_test.go @@ -13,11 +13,56 @@ import ( "io" "net/http" "testing" + "time" "github.com/stretchr/testify/assert" ) -var testCfg = AzureSEVSNPVersionAPI{ +func TestFetchLatestAzureSEVSNPVersion(t *testing.T) { + now := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) + testcases := map[string]struct { + fetcherVersions []string + timeAtTest time.Time + wantErr bool + want AzureSEVSNPVersionAPI + }{ + "get latest version if older than 2 weeks": { + fetcherVersions: []string{"2021-01-01-01-01.json", "2019-01-01-01-01.json"}, + timeAtTest: now.Add(days(15)), + want: latestVersion, + }, + "get older version if latest version is not older than minimum age": { + fetcherVersions: []string{"2021-01-01-01-01.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-01-01-01-01.json", "2020-12-31-00-00.json"}, + timeAtTest: now.Add(days(2)), + wantErr: true, + }, + } + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + client := &http.Client{ + Transport: &fakeConfigAPIHandler{ + versions: tc.fetcherVersions, + }, + } + fetcher := newFetcherWithClientAndVerifier(client, dummyVerifier{}) + res, err := fetcher.FetchAzureSEVSNPVersionLatest(context.Background(), tc.timeAtTest) + assert := assert.New(t) + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + assert.Equal(tc.want, res) + } + }) + } +} + +var latestVersion = AzureSEVSNPVersionAPI{ AzureSEVSNPVersion: AzureSEVSNPVersion{ Microcode: 93, TEE: 0, @@ -26,52 +71,29 @@ var testCfg = AzureSEVSNPVersionAPI{ }, } -func TestFetchLatestAzureSEVSNPVersion(t *testing.T) { - testcases := map[string]struct { - signature []byte - wantErr bool - want AzureSEVSNPVersionAPI - }{ - "get version with valid signature": { - signature: []byte("MEQCIBPEbYg89MIQuaGStLhKGLGMKvKFoYCaAniDLwoIwulqAiB+rj7KMaMOMGxmUsjI7KheCXSNM8NzN+tuDw6AywI75A=="), // signed with release key - want: testCfg, - }, - "fail with invalid signature": { - signature: []byte("invalid"), - wantErr: true, - }, - } - for name, tc := range testcases { - t.Run(name, func(t *testing.T) { - client := &http.Client{ - Transport: &fakeConfigAPIHandler{ - signature: tc.signature, - }, - } - fetcher := NewFetcherWithClient(client) - res, err := fetcher.FetchAzureSEVSNPVersionLatest(context.Background()) +var olderVersion = AzureSEVSNPVersionAPI{ + AzureSEVSNPVersion: AzureSEVSNPVersion{ + Microcode: 1, + TEE: 0, + SNP: 1, + Bootloader: 1, + }, +} - assert := assert.New(t) - if tc.wantErr { - assert.Error(err) - } else { - assert.NoError(err) - assert.Equal(testCfg, res) - } - }) - } +func days(days int) time.Duration { + return time.Duration(days*24) * time.Hour } type fakeConfigAPIHandler struct { - signature []byte + versions []string } // RoundTrip resolves the request and returns a dummy response. func (f *fakeConfigAPIHandler) RoundTrip(req *http.Request) (*http.Response, error) { + signature := []byte("placeholderSignature") if req.URL.Path == "/constellation/v1/attestation/azure-sev-snp/list" { res := &http.Response{} - data := []string{"2021-01-01-01-01.json", "2019-01-01-01-02.json"} // return multiple versions to check that latest version is correctly selected - bt, err := json.Marshal(data) + bt, err := json.Marshal(f.versions) if err != nil { return nil, err } @@ -82,7 +104,17 @@ func (f *fakeConfigAPIHandler) RoundTrip(req *http.Request) (*http.Response, err return res, nil } else if req.URL.Path == "/constellation/v1/attestation/azure-sev-snp/2021-01-01-01-01.json" { res := &http.Response{} - bt, err := json.Marshal(testCfg) + bt, err := json.Marshal(latestVersion) + if err != nil { + return nil, err + } + res.Body = io.NopCloser(bytes.NewReader(bt)) + res.StatusCode = http.StatusOK + return res, nil + + } else if req.URL.Path == "/constellation/v1/attestation/azure-sev-snp/2019-01-01-01-01.json" { + res := &http.Response{} + bt, err := json.Marshal(olderVersion) if err != nil { return nil, err } @@ -93,7 +125,20 @@ func (f *fakeConfigAPIHandler) RoundTrip(req *http.Request) (*http.Response, err } else if req.URL.Path == "/constellation/v1/attestation/azure-sev-snp/2021-01-01-01-01.json.sig" { res := &http.Response{} obj := AzureSEVSNPVersionSignature{ - Signature: f.signature, + Signature: signature, + } + bt, err := json.Marshal(obj) + if err != nil { + return nil, err + } + res.Body = io.NopCloser(bytes.NewReader(bt)) + res.StatusCode = http.StatusOK + return res, nil + + } else if req.URL.Path == "/constellation/v1/attestation/azure-sev-snp/2019-01-01-01-01.json.sig" { + res := &http.Response{} + obj := AzureSEVSNPVersionSignature{ + Signature: signature, } bt, err := json.Marshal(obj) if err != nil { @@ -106,3 +151,9 @@ func (f *fakeConfigAPIHandler) RoundTrip(req *http.Request) (*http.Response, err } return nil, errors.New("no endpoint found") } + +type dummyVerifier struct{} + +func (s dummyVerifier) VerifySignature(_, _, _ []byte) error { + return nil +} diff --git a/internal/attestation/azure/snp/validator_test.go b/internal/attestation/azure/snp/validator_test.go index 2cf45a52c..4c8ca8887 100644 --- a/internal/attestation/azure/snp/validator_test.go +++ b/internal/attestation/azure/snp/validator_test.go @@ -218,10 +218,6 @@ func TestTrustedKeyFromSNP(t *testing.T) { AcceptedKeyDigests: tc.idkeydigests, EnforcementPolicy: tc.enforceIDKeyDigest, } - cfg.BootloaderVersion = config.AttestationVersion{Value: 2} - cfg.TEEVersion = config.AttestationVersion{Value: 0} - cfg.MicrocodeVersion = config.AttestationVersion{Value: 93} - cfg.SNPVersion = config.AttestationVersion{Value: 6} validator := &Validator{ hclValidator: &instanceInfo, @@ -353,12 +349,6 @@ func TestNewSNPReportFromBytes(t *testing.T) { }, } cfg := config.DefaultForAzureSEVSNP() - - cfg.BootloaderVersion = config.AttestationVersion{Value: 2} - cfg.TEEVersion = config.AttestationVersion{Value: 0} - cfg.MicrocodeVersion = config.AttestationVersion{Value: 93} - cfg.SNPVersion = config.AttestationVersion{Value: 6} - for name, tc := range testCases { t.Run(name, func(t *testing.T) { assert := assert.New(t) diff --git a/internal/config/attestationversion.go b/internal/config/attestationversion.go index 884fb3ce2..7286c7d67 100644 --- a/internal/config/attestationversion.go +++ b/internal/config/attestationversion.go @@ -8,7 +8,6 @@ package config import ( "encoding/json" - "errors" "fmt" "strings" ) @@ -34,7 +33,7 @@ func (v AttestationVersion) MarshalYAML() (any, error) { if v.IsLatest { return "latest", nil } - return int(v.Value), nil + return v.Value, nil } // UnmarshalYAML implements a custom unmarshaller to resolve "atest" values. @@ -68,12 +67,11 @@ func (v *AttestationVersion) parseRawUnmarshal(rawUnmarshal any) error { switch s := rawUnmarshal.(type) { case string: if strings.ToLower(s) == "latest" { - // TODO(elchead): activate latest logic for next release AB#3036 - return errors.New("latest is not supported as a version value") - // v.IsLatest = true - // v.Value = placeholderVersionValue + v.IsLatest = true + v.Value = placeholderVersionValue + } else { + return fmt.Errorf("invalid version value: %s", s) } - return fmt.Errorf("invalid version value: %s", s) case int: v.Value = uint8(s) // yaml spec allows "1" as float64, so version number might come as a float: https://github.com/go-yaml/yaml/issues/430 diff --git a/internal/config/attestationversion_test.go b/internal/config/attestationversion_test.go index 5ef8dfe24..62626e2be 100644 --- a/internal/config/attestationversion_test.go +++ b/internal/config/attestationversion_test.go @@ -9,7 +9,6 @@ package config import ( "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" ) @@ -45,39 +44,3 @@ func TestVersionMarshalYAML(t *testing.T) { }) } } - -func TestVersionUnmarshalYAML(t *testing.T) { - tests := []struct { - name string - expected AttestationVersion - yamlInput string - wantErr bool - }{ - // TODO(elchead): activate latest logic for next release AB#3036 - { - name: "latest value is not allowed", - expected: AttestationVersion{}, - yamlInput: "latest\n", - wantErr: true, - }, - { - name: "value 5 resolves to 5", - expected: AttestationVersion{ - Value: 5, - }, - yamlInput: "5\n", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - sut := AttestationVersion{} - err := yaml.Unmarshal([]byte(tt.yamlInput), &sut) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - assert.Equal(t, tt.expected.IsLatest, sut.IsLatest) - }) - } -} diff --git a/internal/config/config.go b/internal/config/config.go index a6a1e8775..562d59a11 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -27,6 +27,7 @@ import ( "os" "reflect" "strings" + "time" "github.com/go-playground/locales/en" ut "github.com/go-playground/universal-translator" @@ -386,19 +387,18 @@ func fromFile(fileHandler file.Handler, name string) (*Config, error) { // 2. For "latest" version values of the attestation variants fetch the version numbers. // 3. Read secrets from environment variables. // 4. Validate config. If `--force` is set the version validation will be disabled and any version combination is allowed. -func New(fileHandler file.Handler, name string, _ attestationconfigapi.Fetcher, force bool) (*Config, error) { +func New(fileHandler file.Handler, name string, fetcher attestationconfigapi.Fetcher, force bool) (*Config, error) { // Read config file c, err := fromFile(fileHandler, name) if err != nil { return nil, err } - // TODO(elchead): activate latest logic for next release AB#3036 - //if azure := c.Attestation.AzureSEVSNP; azure != nil { - // if err := azure.FetchAndSetLatestVersionNumbers(fetcher); err != nil { - // return c, err - // } - //} + if azure := c.Attestation.AzureSEVSNP; azure != nil { + if err := azure.FetchAndSetLatestVersionNumbers(context.Background(), fetcher, time.Now()); err != nil { + return c, err + } + } // Read secrets from env-vars. clientSecretValue := os.Getenv(constants.EnvVarAzureClientSecretValue) @@ -925,19 +925,12 @@ type AzureSEVSNP struct { // DefaultForAzureSEVSNP returns the default configuration for Azure SEV-SNP attestation. // Version numbers have placeholder values and the latest available values can be fetched using [AzureSEVSNP.FetchAndSetLatestVersionNumbers]. func DefaultForAzureSEVSNP() *AzureSEVSNP { - // TODO(elchead): activate latest logic for next release AB#3036 - azureSNPCfg := attestationconfigapi.AzureSEVSNPVersion{ - Bootloader: 3, - TEE: 0, - SNP: 8, - Microcode: 115, - } return &AzureSEVSNP{ Measurements: measurements.DefaultsFor(cloudprovider.Azure, variant.AzureSEVSNP{}), - BootloaderVersion: AttestationVersion{Value: azureSNPCfg.Bootloader}, // NewLatestPlaceholderVersion(), - TEEVersion: AttestationVersion{Value: azureSNPCfg.TEE}, // NewLatestPlaceholderVersion(), - SNPVersion: AttestationVersion{Value: azureSNPCfg.SNP}, // NewLatestPlaceholderVersion(), - MicrocodeVersion: AttestationVersion{Value: azureSNPCfg.Microcode}, // NewLatestPlaceholderVersion(), + BootloaderVersion: NewLatestPlaceholderVersion(), + TEEVersion: NewLatestPlaceholderVersion(), + SNPVersion: NewLatestPlaceholderVersion(), + MicrocodeVersion: NewLatestPlaceholderVersion(), FirmwareSignerConfig: SNPFirmwareSignerConfig{ AcceptedKeyDigests: idkeydigest.DefaultList(), EnforcementPolicy: idkeydigest.MAAFallback, @@ -981,8 +974,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(fetcher attestationconfigapi.Fetcher) error { - versions, err := fetcher.FetchAzureSEVSNPVersionLatest(context.Background()) +func (c *AzureSEVSNP) FetchAndSetLatestVersionNumbers(ctx context.Context, fetcher attestationconfigapi.Fetcher, now time.Time) error { + versions, err := fetcher.FetchAzureSEVSNPVersionLatest(ctx, now) if err != nil { return err } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 52f5d92ad..a03ed6b99 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -11,6 +11,7 @@ import ( "errors" "reflect" "testing" + "time" "github.com/go-playground/locales/en" ut "github.com/go-playground/universal-translator" @@ -41,21 +42,20 @@ func TestDefaultConfig(t *testing.T) { assert.NotNil(def) } -// TODO(elchead): activate latest logic for next release AB#3036 -// func TestDefaultConfigWritesLatestVersion(t *testing.T) { -// conf := Default() -// bt, err := yaml.Marshal(conf) -// require := require.New(t) -// require.NoError(err) +func TestDefaultConfigWritesLatestVersion(t *testing.T) { + conf := Default() + bt, err := yaml.Marshal(conf) + require := require.New(t) + require.NoError(err) -// var mp configMap -// require.NoError(yaml.Unmarshal(bt, &mp)) -// assert := assert.New(t) -// assert.Equal("latest", mp.getAzureSEVSNPVersion("microcodeVersion")) -// assert.Equal("latest", mp.getAzureSEVSNPVersion("teeVersion")) -// assert.Equal("latest", mp.getAzureSEVSNPVersion("snpVersion")) -// assert.Equal("latest", mp.getAzureSEVSNPVersion("bootloaderVersion")) -//} + var mp configMap + require.NoError(yaml.Unmarshal(bt, &mp)) + assert := assert.New(t) + assert.Equal("latest", mp.getAzureSEVSNPVersion("microcodeVersion")) + assert.Equal("latest", mp.getAzureSEVSNPVersion("teeVersion")) + assert.Equal("latest", mp.getAzureSEVSNPVersion("snpVersion")) + assert.Equal("latest", mp.getAzureSEVSNPVersion("bootloaderVersion")) +} func TestReadConfigFile(t *testing.T) { testCases := map[string]struct { @@ -64,41 +64,29 @@ func TestReadConfigFile(t *testing.T) { wantResult *Config wantErr bool }{ - // TODO(elchead): activate latest logic for next release AB#3036 - //"mix of Latest and uint as version value": { - // config: func() configMap { - // conf := Default() - // m := getConfigAsMap(conf, t) - // m.setAzureSEVSNPVersion("microcodeVersion", "Latest") // check uppercase also works - // m.setAzureSEVSNPVersion("teeVersion", 2) - // m.setAzureSEVSNPVersion("bootloaderVersion", 1) - // return m - // }(), - - // configName: constants.ConfigFilename, - // wantResult: func() *Config { - // conf := Default() - // conf.Attestation.AzureSEVSNP.BootloaderVersion = AttestationVersion{ - // Value: 1, - // IsLatest: false, - // } - // conf.Attestation.AzureSEVSNP.TEEVersion = AttestationVersion{ - // Value: 2, - // IsLatest: false, - // } - // return conf - // }(), - //}, - // TODO(elchead): activate latest logic for next release AB#3036 - "refuse invalid latest value": { + "mix of Latest and uint as version value": { config: func() configMap { conf := Default() m := getConfigAsMap(conf, t) - m.setAzureSEVSNPVersion("microcodeVersion", "latest") + m.setAzureSEVSNPVersion("microcodeVersion", "Latest") // check uppercase also works + m.setAzureSEVSNPVersion("teeVersion", 2) + m.setAzureSEVSNPVersion("bootloaderVersion", 1) return m }(), + configName: constants.ConfigFilename, - wantErr: true, + wantResult: func() *Config { + conf := Default() + conf.Attestation.AzureSEVSNP.BootloaderVersion = AttestationVersion{ + Value: 1, + IsLatest: false, + } + conf.Attestation.AzureSEVSNP.TEEVersion = AttestationVersion{ + Value: 2, + IsLatest: false, + } + return conf + }(), }, "refuse invalid version value": { config: func() configMap { @@ -271,7 +259,7 @@ func TestNewWithDefaultOptions(t *testing.T) { } // Test - c, err := New(fileHandler, constants.ConfigFilename, fakeConfigFetcher{}, false) + c, err := New(fileHandler, constants.ConfigFilename, stubAttestationFetcher{}, false) if tc.wantErr { assert.Error(err) return @@ -889,9 +877,9 @@ func (c configMap) setAzureSEVSNPVersion(versionType string, value interface{}) c["attestation"].(configMap)["azureSEVSNP"].(configMap)[versionType] = value } -//func (c configMap) getAzureSEVSNPVersion(versionType string) interface{} { -// return c["attestation"].(configMap)["azureSEVSNP"].(configMap)[versionType] -//} +func (c configMap) getAzureSEVSNPVersion(versionType string) interface{} { + return c["attestation"].(configMap)["azureSEVSNP"].(configMap)[versionType] +} // getConfigAsMap returns a map of the config. func getConfigAsMap(conf *Config, t *testing.T) (res configMap) { @@ -905,21 +893,21 @@ func getConfigAsMap(conf *Config, t *testing.T) (res configMap) { return } -type fakeConfigFetcher struct{} +type stubAttestationFetcher struct{} -func (f fakeConfigFetcher) FetchAzureSEVSNPVersionList(_ context.Context, _ configapi.AzureSEVSNPVersionList) (configapi.AzureSEVSNPVersionList, error) { +func (f stubAttestationFetcher) FetchAzureSEVSNPVersionList(_ context.Context, _ configapi.AzureSEVSNPVersionList) (configapi.AzureSEVSNPVersionList, error) { return configapi.AzureSEVSNPVersionList( []string{}, ), nil } -func (f fakeConfigFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ configapi.AzureSEVSNPVersionAPI) (configapi.AzureSEVSNPVersionAPI, error) { +func (f stubAttestationFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ configapi.AzureSEVSNPVersionAPI) (configapi.AzureSEVSNPVersionAPI, error) { return configapi.AzureSEVSNPVersionAPI{ AzureSEVSNPVersion: testCfg, }, nil } -func (f fakeConfigFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context) (configapi.AzureSEVSNPVersionAPI, error) { +func (f stubAttestationFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context, _ time.Time) (configapi.AzureSEVSNPVersionAPI, error) { return configapi.AzureSEVSNPVersionAPI{ AzureSEVSNPVersion: testCfg, }, nil diff --git a/internal/sigstore/verify.go b/internal/sigstore/verify.go index ace485b29..30fc422dc 100644 --- a/internal/sigstore/verify.go +++ b/internal/sigstore/verify.go @@ -18,6 +18,11 @@ import ( sigsig "github.com/sigstore/sigstore/pkg/signature" ) +// Verifier checks if the signature of content can be verified. +type Verifier interface { + VerifySignature(content, signature, publicKey []byte) error +} + // CosignVerifier checks if the signature of content can be verified // using a cosign public key. type CosignVerifier struct{} diff --git a/rfc/attestation-config.md b/rfc/attestation-config.md index a33fa69f5..a38bcee05 100644 --- a/rfc/attestation-config.md +++ b/rfc/attestation-config.md @@ -129,6 +129,9 @@ While this API should stay compatible with old release, extensive changes to our In this case a new API version will be used to retrieve the config in the updated format, e.g. `/constellation/v2/attestation//`. The old API will still receive updates for at least the next release cycle, during this time this API version will also return a deprecation warning when requesting `list`. +### Azure SEV-SNP +IMPORTANT: Since the current version fetches from the Azure SEV-SNP report are not guaranteed to be globally rolled out at the time of the report, we introduce a minimum age (2 weeks) of the version to consider it a valid latest version. +This validation is only enforced on the fetcher side! This means that the HTTP endpoints contain all versions, even those that do not yet have the minimum age. ### AWS AWS provides a way to precalculate launch-measurements for their firmware in SEV-SNP CVMs.