From d52f3db2a31a9bcdedbfbc9bd1c9af5c58ef2384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Wei=C3=9Fe?= <66256922+daniel-weisse@users.noreply.github.com> Date: Mon, 28 Nov 2022 10:27:33 +0100 Subject: [PATCH] AB#2644 Fetch measurements from CDN (#653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fetch measurements from CDN * Perform metadata validation on fetched measurements * Remove deprecated public bucket Signed-off-by: Daniel Weiße --- cli/internal/cmd/configfetchmeasurements.go | 50 +++++--- .../cmd/configfetchmeasurements_test.go | 72 ++++++----- cli/internal/cmd/upgradeplan.go | 27 +++- cli/internal/cmd/upgradeplan_test.go | 115 +++++++++++++----- hack/pcr-reader/main.go | 3 +- hack/pcr-reader/main_test.go | 9 +- .../attestation/measurements/measurements.go | 25 +++- .../measurements/measurements_test.go | 75 +++++++----- internal/cloud/cloudprovider/cloudprovider.go | 15 +++ .../cloud/cloudprovider/cloudprovider_test.go | 94 +++++++++++++- internal/config/config.go | 4 + internal/config/config_doc.go | 7 +- internal/constants/constants.go | 18 +-- internal/constants/keys_enterprise.go | 16 +++ internal/constants/keys_oss.go | 16 +++ internal/image/image.go | 4 +- 16 files changed, 406 insertions(+), 144 deletions(-) create mode 100644 internal/constants/keys_enterprise.go create mode 100644 internal/constants/keys_oss.go diff --git a/cli/internal/cmd/configfetchmeasurements.go b/cli/internal/cmd/configfetchmeasurements.go index ea344757b..964cda6fe 100644 --- a/cli/internal/cmd/configfetchmeasurements.go +++ b/cli/internal/cmd/configfetchmeasurements.go @@ -11,13 +11,15 @@ import ( "fmt" "net/http" "net/url" + "path" + "strings" "time" "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" - "github.com/edgelesssys/constellation/v2/internal/image" "github.com/edgelesssys/constellation/v2/internal/sigstore" "github.com/spf13/afero" "github.com/spf13/cobra" @@ -49,10 +51,13 @@ func runConfigFetchMeasurements(cmd *cobra.Command, args []string) error { if err != nil { return fmt.Errorf("constructing Rekor client: %w", err) } - return configFetchMeasurements(cmd, rekor, fileHandler, http.DefaultClient, image.New()) + return configFetchMeasurements(cmd, rekor, []byte(constants.CosignPublicKey), fileHandler, http.DefaultClient) } -func configFetchMeasurements(cmd *cobra.Command, verifier rekorVerifier, fileHandler file.Handler, client *http.Client, img imageFetcher) error { +func configFetchMeasurements( + cmd *cobra.Command, verifier rekorVerifier, cosignPublicKey []byte, + fileHandler file.Handler, client *http.Client, +) error { flags, err := parseFetchMeasurementsFlags(cmd) if err != nil { return err @@ -70,12 +75,21 @@ func configFetchMeasurements(cmd *cobra.Command, verifier rekorVerifier, fileHan ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - if err := flags.updateURLs(ctx, conf, img); err != nil { + if err := flags.updateURLs(conf); err != nil { return err } var fetchedMeasurements measurements.M - hash, err := fetchedMeasurements.FetchAndVerify(ctx, client, flags.measurementsURL, flags.signatureURL, []byte(constants.CosignPublicKey)) + hash, err := fetchedMeasurements.FetchAndVerify( + ctx, client, + flags.measurementsURL, + flags.signatureURL, + cosignPublicKey, + measurements.WithMetadata{ + CSP: conf.GetProvider(), + Image: conf.Image, + }, + ) if err != nil { return err } @@ -129,31 +143,31 @@ func parseFetchMeasurementsFlags(cmd *cobra.Command) (*fetchMeasurementsFlags, e }, nil } -func (f *fetchMeasurementsFlags) updateURLs(ctx context.Context, conf *config.Config, img imageFetcher) error { - imageRef, err := img.FetchReference(ctx, conf) - if err != nil { - return err - } - +func (f *fetchMeasurementsFlags) updateURLs(conf *config.Config) error { if f.measurementsURL == nil { - // TODO(AB#2644): resolve image version to reference - parsedURL, err := url.Parse(constants.S3PublicBucket + imageRef + "/measurements.json") + url, err := measurementURL(conf.GetProvider(), conf.Image, "measurements.json") if err != nil { return err } - f.measurementsURL = parsedURL + f.measurementsURL = url } if f.signatureURL == nil { - parsedURL, err := url.Parse(constants.S3PublicBucket + imageRef + "/measurements.json.sig") + url, err := measurementURL(conf.GetProvider(), conf.Image, "measurements.json.sig") if err != nil { return err } - f.signatureURL = parsedURL + f.signatureURL = url } return nil } -type imageFetcher interface { - FetchReference(ctx context.Context, config *config.Config) (string, error) +func measurementURL(provider cloudprovider.Provider, image, file string) (*url.URL, error) { + url, err := url.Parse(constants.CDNRepositoryURL) + if err != nil { + return nil, fmt.Errorf("parsing image version repository URL: %w", err) + } + url.Path = path.Join(constants.CDNMeasurementsPath, image, strings.ToLower(provider.String()), file) + + return url, nil } diff --git a/cli/internal/cmd/configfetchmeasurements_test.go b/cli/internal/cmd/configfetchmeasurements_test.go index 2b56559d9..de2d67517 100644 --- a/cli/internal/cmd/configfetchmeasurements_test.go +++ b/cli/internal/cmd/configfetchmeasurements_test.go @@ -8,8 +8,8 @@ package cmd import ( "bytes" - "context" "errors" + "fmt" "io" "net/http" "net/url" @@ -109,8 +109,8 @@ func TestUpdateURLs(t *testing.T) { }, }, flags: &fetchMeasurementsFlags{}, - wantMeasurementsURL: constants.S3PublicBucket + "some/image/path/image-123456/measurements.json", - wantMeasurementsSigURL: constants.S3PublicBucket + "some/image/path/image-123456/measurements.json.sig", + wantMeasurementsURL: constants.CDNRepositoryURL + "/" + constants.CDNMeasurementsPath + "/someImageVersion/gcp/measurements.json", + wantMeasurementsSigURL: constants.CDNRepositoryURL + "/" + constants.CDNMeasurementsPath + "/someImageVersion/gcp/measurements.json.sig", }, "both set by user": { conf: &config.Config{}, @@ -127,9 +127,7 @@ func TestUpdateURLs(t *testing.T) { t.Run(name, func(t *testing.T) { assert := assert.New(t) - err := tc.flags.updateURLs(context.Background(), tc.conf, &stubImageFetcher{ - reference: "some/image/path/image-123456", - }) + err := tc.flags.updateURLs(tc.conf) assert.NoError(err) assert.Equal(tc.wantMeasurementsURL, tc.flags.measurementsURL.String()) }) @@ -152,32 +150,57 @@ func newTestClient(fn roundTripFunc) *http.Client { } func TestConfigFetchMeasurements(t *testing.T) { - measurements := `1: fPRxd3lV3uybnSVhcBmM6XLzcvMitXW78G0RRuQxYGc= -2: PUWM/lXMA+ofRD8VYr7sjfUcdeFKn8+acjShPxmOeWk= -3: PUWM/lXMA+ofRD8VYr7sjfUcdeFKn8+acjShPxmOeWk= -4: HaV5ivUAGzMxmKkfKjcG3wmW08MRUWr+vsfIMVQpOH0= -5: PemdXV59WnLLzPz0F4GGCTKm8KbHskPRvon1dtNw7oY= -7: 8dI/6SUmQ5sd8+bulPDpJ8ghs0UX0+fgLlW8kutAYKw= -8: XJ5IBWy6b6vqojkTsk/GLOWyfNUB2qaf58+JjMYiAB4= -9: Gw5gq8D1WXfz46sF/OKiWbkBssyt4ayGybzNyV9cUCQ= + // Cosign private key used to sign the measurements. + // Generated with: cosign generate-key-pair + // Password left empty. + // + // -----BEGIN ENCRYPTED COSIGN PRIVATE KEY----- + // eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjozMjc2OCwiciI6 + // OCwicCI6MX0sInNhbHQiOiJlRHVYMWRQMGtIWVRnK0xkbjcxM0tjbFVJaU92eFVX + // VXgvNi9BbitFVk5BPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94 + // Iiwibm9uY2UiOiJwaWhLL2txNmFXa2hqSVVHR3RVUzhTVkdHTDNIWWp4TCJ9LCJj + // aXBoZXJ0ZXh0Ijoidm81SHVWRVFWcUZ2WFlQTTVPaTVaWHM5a255bndZU2dvcyth + // VklIeHcrOGFPamNZNEtvVjVmL3lHRHR0K3BHV2toanJPR1FLOWdBbmtsazFpQ0c5 + // a2czUXpPQTZsU2JRaHgvZlowRVRZQ0hLeElncEdPRVRyTDlDenZDemhPZXVSOXJ6 + // TDcvRjBBVy9vUDVqZXR3dmJMNmQxOEhjck9kWE8yVmYxY2w0YzNLZjVRcnFSZzlN + // dlRxQWFsNXJCNHNpY1JaMVhpUUJjb0YwNHc9PSJ9 + // -----END ENCRYPTED COSIGN PRIVATE KEY----- + + cosignPublicKey := []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----") + + measurements := `{ + "csp": "gcp", + "image": "v999.999.999", + "measurements": { + "0": "0000000000000000000000000000000000000000000000000000000000000000", + "1": "1111111111111111111111111111111111111111111111111111111111111111", + "2": "2222222222222222222222222222222222222222222222222222222222222222", + "3": "3333333333333333333333333333333333333333333333333333333333333333", + "4": "4444444444444444444444444444444444444444444444444444444444444444", + "5": "5555555555555555555555555555555555555555555555555555555555555555", + "6": "6666666666666666666666666666666666666666666666666666666666666666" + } +} ` - signature := "MEUCIFdJ5dH6HDywxQWTUh9Bw77wMrq0mNCUjMQGYP+6QsVmAiEAmazj/L7rFGA4/Gz8y+kI5h5E5cDgc3brihvXBKF6qZA=" + signature := "MEYCIQDRAQNK2NjHJBGrnw3HQAyBsXMCmVCptBdgA6VZ3IlyiAIhAPG42waF1aFZq7dnjP3b2jsMNUtaKYDQQSazW1AX8jgF" client := newTestClient(func(req *http.Request) *http.Response { - if req.URL.String() == "https://public-edgeless-constellation.s3.us-east-2.amazonaws.com/someImage/measurements.json" { + if req.URL.String() == "https://cdn.confidential.cloud/constellation/v1/measurements/v999.999.999/gcp/measurements.json" { return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString(measurements)), Header: make(http.Header), } } - if req.URL.String() == "https://public-edgeless-constellation.s3.us-east-2.amazonaws.com/someImage/measurements.json.sig" { + if req.URL.String() == "https://cdn.confidential.cloud/constellation/v1/measurements/v999.999.999/gcp/measurements.json.sig" { return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString(signature)), Header: make(http.Header), } } + + fmt.Println("unexpected request", req.URL.String()) return &http.Response{ StatusCode: http.StatusNotFound, Body: io.NopCloser(bytes.NewBufferString("Not found.")), @@ -215,23 +238,12 @@ func TestConfigFetchMeasurements(t *testing.T) { fileHandler := file.NewHandler(afero.NewMemMapFs()) gcpConfig := defaultConfigWithExpectedMeasurements(t, config.Default(), cloudprovider.GCP) - gcpConfig.Image = "someImage" + gcpConfig.Image = "v999.999.999" err := fileHandler.WriteYAML(constants.ConfigFilename, gcpConfig, file.OptMkdirAll) require.NoError(err) - assert.NoError(configFetchMeasurements(cmd, tc.verifier, fileHandler, client, &stubImageFetcher{ - reference: "someImage", - })) + assert.NoError(configFetchMeasurements(cmd, tc.verifier, cosignPublicKey, fileHandler, client)) }) } } - -type stubImageFetcher struct { - reference string - fetchReferenceErr error -} - -func (f *stubImageFetcher) FetchReference(_ context.Context, _ *config.Config) (string, error) { - return f.reference, f.fetchReferenceErr -} diff --git a/cli/internal/cmd/upgradeplan.go b/cli/internal/cmd/upgradeplan.go index 320e646bd..7af99a578 100644 --- a/cli/internal/cmd/upgradeplan.go +++ b/cli/internal/cmd/upgradeplan.go @@ -13,10 +13,12 @@ import ( "io" "net/http" "net/url" + "path" "regexp" "strings" "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" + "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/constants" @@ -163,14 +165,20 @@ func getCompatibleImages(csp cloudprovider.Provider, currentVersion string, imag case cloudprovider.Azure: for imgVersion, image := range images { if semver.Compare(currentVersion, imgVersion) < 0 { - compatibleImages[imgVersion] = config.UpgradeConfig{Image: image.AzureImage} + compatibleImages[imgVersion] = config.UpgradeConfig{ + Image: image.AzureImage, + CSP: cloudprovider.Azure, + } } } case cloudprovider.GCP: for imgVersion, image := range images { if semver.Compare(currentVersion, imgVersion) < 0 { - compatibleImages[imgVersion] = config.UpgradeConfig{Image: image.GCPImage} + compatibleImages[imgVersion] = config.UpgradeConfig{ + Image: image.GCPImage, + CSP: cloudprovider.GCP, + } } } } @@ -181,17 +189,26 @@ func getCompatibleImages(csp cloudprovider.Provider, currentVersion string, imag // getCompatibleImageMeasurements retrieves the expected measurements for each image. func getCompatibleImageMeasurements(ctx context.Context, cmd *cobra.Command, client *http.Client, rekor rekorVerifier, pubK []byte, images map[string]config.UpgradeConfig) error { for idx, img := range images { - measurementsURL, err := url.Parse(constants.S3PublicBucket + strings.ToLower(img.Image) + "/measurements.json") + measurementsURL, err := url.Parse(constants.CDNRepositoryURL + "/" + path.Join(img.Image, strings.ToLower(img.CSP.String()), "measurements.json")) if err != nil { return err } - signatureURL, err := url.Parse(constants.S3PublicBucket + strings.ToLower(img.Image) + "/measurements.json.sig") + signatureURL, err := url.Parse(constants.CDNRepositoryURL + "/" + path.Join(img.Image, strings.ToLower(img.CSP.String()), "measurements.json.sig")) if err != nil { return err } - hash, err := img.Measurements.FetchAndVerify(ctx, client, measurementsURL, signatureURL, pubK) + hash, err := img.Measurements.FetchAndVerify( + ctx, client, + measurementsURL, + signatureURL, + pubK, + measurements.WithMetadata{ + Image: img.Image, + CSP: img.CSP, + }, + ) if err != nil { return err } diff --git a/cli/internal/cmd/upgradeplan_test.go b/cli/internal/cmd/upgradeplan_test.go index ea3051a87..0faa26807 100644 --- a/cli/internal/cmd/upgradeplan_test.go +++ b/cli/internal/cmd/upgradeplan_test.go @@ -192,12 +192,15 @@ func TestGetCompatibleImages(t *testing.T) { wantImages: map[string]config.UpgradeConfig{ "v1.0.1": { Image: "azure-v1.0.1", + CSP: cloudprovider.Azure, }, "v1.0.2": { Image: "azure-v1.0.2", + CSP: cloudprovider.Azure, }, "v1.1.0": { Image: "azure-v1.1.0", + CSP: cloudprovider.Azure, }, }, }, @@ -208,12 +211,15 @@ func TestGetCompatibleImages(t *testing.T) { wantImages: map[string]config.UpgradeConfig{ "v1.0.1": { Image: "gcp-v1.0.1", + CSP: cloudprovider.GCP, }, "v1.0.2": { Image: "gcp-v1.0.2", + CSP: cloudprovider.GCP, }, "v1.1.0": { Image: "gcp-v1.1.0", + CSP: cloudprovider.GCP, }, }, }, @@ -240,25 +246,42 @@ func TestGetCompatibleImageMeasurements(t *testing.T) { testImages := map[string]config.UpgradeConfig{ "v0.0.0": { - Image: "azure-v0.0.0", + Image: "v0.0.0", + CSP: cloudprovider.Azure, }, "v1.0.0": { - Image: "azure-v1.0.0", + Image: "v1.0.0", + CSP: cloudprovider.Azure, }, } client := newTestClient(func(req *http.Request) *http.Response { - if strings.HasSuffix(req.URL.String(), "/measurements.json") { + if strings.HasSuffix(req.URL.String(), "v0.0.0/azure/measurements.json") { return &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader("0: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n")), + Body: io.NopCloser(strings.NewReader(`{"csp":"azure","image":"v0.0.0","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`)), Header: make(http.Header), } } - if strings.HasSuffix(req.URL.String(), "/measurements.json.sig") { + if strings.HasSuffix(req.URL.String(), "v0.0.0/azure/measurements.json.sig") { return &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader("MEUCIBs1g2/n0FsgPfJ+0uLD5TaunGhxwDcQcUGBroejKvg3AiEAzZtcLU9O6IiVhxB8tBS+ty6MXoPNwL8WRWMzyr35eKI=")), + Body: io.NopCloser(strings.NewReader("MEQCIGRR7RaSMs892Ta06/Tz7LqPUxI05X4wQcP+nFFmZtmaAiBNl9X8mUKmUBfxg13LQBfmmpw6JwYQor5hOwM3NFVPAg==")), + Header: make(http.Header), + } + } + + if strings.HasSuffix(req.URL.String(), "v1.0.0/azure/measurements.json") { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"csp":"azure","image":"v1.0.0","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`)), + Header: make(http.Header), + } + } + if strings.HasSuffix(req.URL.String(), "v1.0.0/azure/measurements.json.sig") { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader("MEQCIFh8CVELp/Da2U2Jt404OXsUeDfqtrf3pqGRuvxnxhI8AiBTHF9tHEPwFedYG3Jgn2ELOxss+Ybc6135vEtClBrbpg==")), Header: make(http.Header), } } @@ -270,7 +293,7 @@ func TestGetCompatibleImageMeasurements(t *testing.T) { } }) - pubK := []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUs5fDUIz9aiwrfr8BK4VjN7jE6sl\ngz7UuXsOin8+dB0SGrbNHy7TJToa2fAiIKPVLTOfvY75DqRAtffhO1fpBA==\n-----END PUBLIC KEY-----") + pubK := []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----") err := getCompatibleImageMeasurements(context.Background(), &cobra.Command{}, client, singleUUIDVerifier(), pubK, testImages) assert.NoError(err) @@ -283,15 +306,28 @@ func TestGetCompatibleImageMeasurements(t *testing.T) { func TestUpgradePlan(t *testing.T) { testImages := map[string]imageManifest{ "v1.0.0": { - AzureImage: "azure-v1.0.0", - GCPImage: "gcp-v1.0.0", - }, - "v2.0.0": { - AzureImage: "azure-v2.0.0", - GCPImage: "gcp-v2.0.0", + AzureImage: "v1.0.0", + GCPImage: "v1.0.0", }, } + // Cosign private key used to sign the measurements. + // Generated with: cosign generate-key-pair + // Password left empty. + // + // -----BEGIN ENCRYPTED COSIGN PRIVATE KEY----- + // eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjozMjc2OCwiciI6 + // OCwicCI6MX0sInNhbHQiOiJlRHVYMWRQMGtIWVRnK0xkbjcxM0tjbFVJaU92eFVX + // VXgvNi9BbitFVk5BPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94 + // Iiwibm9uY2UiOiJwaWhLL2txNmFXa2hqSVVHR3RVUzhTVkdHTDNIWWp4TCJ9LCJj + // aXBoZXJ0ZXh0Ijoidm81SHVWRVFWcUZ2WFlQTTVPaTVaWHM5a255bndZU2dvcyth + // VklIeHcrOGFPamNZNEtvVjVmL3lHRHR0K3BHV2toanJPR1FLOWdBbmtsazFpQ0c5 + // a2czUXpPQTZsU2JRaHgvZlowRVRZQ0hLeElncEdPRVRyTDlDenZDemhPZXVSOXJ6 + // TDcvRjBBVy9vUDVqZXR3dmJMNmQxOEhjck9kWE8yVmYxY2w0YzNLZjVRcnFSZzlN + // dlRxQWFsNXJCNHNpY1JaMVhpUUJjb0YwNHc9PSJ9 + // -----END ENCRYPTED COSIGN PRIVATE KEY----- + pubK := "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----" + testCases := map[string]struct { planner stubUpgradePlanner flags upgradePlanFlags @@ -311,7 +347,7 @@ func TestUpgradePlan(t *testing.T) { flags: upgradePlanFlags{ configPath: constants.ConfigFilename, filePath: "upgrade-plan.yaml", - cosignPubKey: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUs5fDUIz9aiwrfr8BK4VjN7jE6sl\ngz7UuXsOin8+dB0SGrbNHy7TJToa2fAiIKPVLTOfvY75DqRAtffhO1fpBA==\n-----END PUBLIC KEY-----", + cosignPubKey: pubK, }, csp: cloudprovider.GCP, verifier: singleUUIDVerifier(), @@ -319,14 +355,14 @@ func TestUpgradePlan(t *testing.T) { }, "upgrades gcp": { planner: stubUpgradePlanner{ - image: "projects/constellation-images/global/images/constellation-v1-0-0", + image: "projects/constellation-images/global/images/constellation-v0-0-0", }, imageFetchStatus: http.StatusOK, measurementsFetchStatus: http.StatusOK, flags: upgradePlanFlags{ configPath: constants.ConfigFilename, filePath: "upgrade-plan.yaml", - cosignPubKey: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUs5fDUIz9aiwrfr8BK4VjN7jE6sl\ngz7UuXsOin8+dB0SGrbNHy7TJToa2fAiIKPVLTOfvY75DqRAtffhO1fpBA==\n-----END PUBLIC KEY-----", + cosignPubKey: pubK, }, csp: cloudprovider.GCP, verifier: singleUUIDVerifier(), @@ -341,7 +377,7 @@ func TestUpgradePlan(t *testing.T) { flags: upgradePlanFlags{ configPath: constants.ConfigFilename, filePath: "upgrade-plan.yaml", - cosignPubKey: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUs5fDUIz9aiwrfr8BK4VjN7jE6sl\ngz7UuXsOin8+dB0SGrbNHy7TJToa2fAiIKPVLTOfvY75DqRAtffhO1fpBA==\n-----END PUBLIC KEY-----", + cosignPubKey: pubK, }, csp: cloudprovider.Azure, verifier: singleUUIDVerifier(), @@ -349,14 +385,14 @@ func TestUpgradePlan(t *testing.T) { }, "upgrade to stdout": { planner: stubUpgradePlanner{ - image: "projects/constellation-images/global/images/constellation-v1-0-0", + image: "projects/constellation-images/global/images/constellation-v0-0-0", }, imageFetchStatus: http.StatusOK, measurementsFetchStatus: http.StatusOK, flags: upgradePlanFlags{ configPath: constants.ConfigFilename, filePath: "-", - cosignPubKey: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUs5fDUIz9aiwrfr8BK4VjN7jE6sl\ngz7UuXsOin8+dB0SGrbNHy7TJToa2fAiIKPVLTOfvY75DqRAtffhO1fpBA==\n-----END PUBLIC KEY-----", + cosignPubKey: pubK, }, csp: cloudprovider.GCP, verifier: singleUUIDVerifier(), @@ -371,7 +407,7 @@ func TestUpgradePlan(t *testing.T) { flags: upgradePlanFlags{ configPath: constants.ConfigFilename, filePath: "upgrade-plan.yaml", - cosignPubKey: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUs5fDUIz9aiwrfr8BK4VjN7jE6sl\ngz7UuXsOin8+dB0SGrbNHy7TJToa2fAiIKPVLTOfvY75DqRAtffhO1fpBA==\n-----END PUBLIC KEY-----", + cosignPubKey: pubK, }, csp: cloudprovider.GCP, verifier: singleUUIDVerifier(), @@ -379,14 +415,14 @@ func TestUpgradePlan(t *testing.T) { }, "image fetch error": { planner: stubUpgradePlanner{ - image: "projects/constellation-images/global/images/constellation-v1-0-0", + image: "projects/constellation-images/global/images/constellation-v0-0-0", }, imageFetchStatus: http.StatusInternalServerError, measurementsFetchStatus: http.StatusOK, flags: upgradePlanFlags{ configPath: constants.ConfigFilename, filePath: "upgrade-plan.yaml", - cosignPubKey: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUs5fDUIz9aiwrfr8BK4VjN7jE6sl\ngz7UuXsOin8+dB0SGrbNHy7TJToa2fAiIKPVLTOfvY75DqRAtffhO1fpBA==\n-----END PUBLIC KEY-----", + cosignPubKey: pubK, }, csp: cloudprovider.GCP, verifier: singleUUIDVerifier(), @@ -394,14 +430,14 @@ func TestUpgradePlan(t *testing.T) { }, "measurements fetch error": { planner: stubUpgradePlanner{ - image: "projects/constellation-images/global/images/constellation-v1-0-0", + image: "projects/constellation-images/global/images/constellation-v0-0-0", }, imageFetchStatus: http.StatusOK, measurementsFetchStatus: http.StatusInternalServerError, flags: upgradePlanFlags{ configPath: constants.ConfigFilename, filePath: "upgrade-plan.yaml", - cosignPubKey: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUs5fDUIz9aiwrfr8BK4VjN7jE6sl\ngz7UuXsOin8+dB0SGrbNHy7TJToa2fAiIKPVLTOfvY75DqRAtffhO1fpBA==\n-----END PUBLIC KEY-----", + cosignPubKey: pubK, }, csp: cloudprovider.GCP, verifier: singleUUIDVerifier(), @@ -409,14 +445,14 @@ func TestUpgradePlan(t *testing.T) { }, "failing search should not result in error": { planner: stubUpgradePlanner{ - image: "projects/constellation-images/global/images/constellation-v1-0-0", + image: "projects/constellation-images/global/images/constellation-v0-0-0", }, imageFetchStatus: http.StatusOK, measurementsFetchStatus: http.StatusOK, flags: upgradePlanFlags{ configPath: constants.ConfigFilename, filePath: "upgrade-plan.yaml", - cosignPubKey: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUs5fDUIz9aiwrfr8BK4VjN7jE6sl\ngz7UuXsOin8+dB0SGrbNHy7TJToa2fAiIKPVLTOfvY75DqRAtffhO1fpBA==\n-----END PUBLIC KEY-----", + cosignPubKey: pubK, }, csp: cloudprovider.GCP, verifier: &stubRekorVerifier{ @@ -427,14 +463,14 @@ func TestUpgradePlan(t *testing.T) { }, "failing verify should not result in error": { planner: stubUpgradePlanner{ - image: "projects/constellation-images/global/images/constellation-v1-0-0", + image: "projects/constellation-images/global/images/constellation-v0-0-0", }, imageFetchStatus: http.StatusOK, measurementsFetchStatus: http.StatusOK, flags: upgradePlanFlags{ configPath: constants.ConfigFilename, filePath: "upgrade-plan.yaml", - cosignPubKey: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUs5fDUIz9aiwrfr8BK4VjN7jE6sl\ngz7UuXsOin8+dB0SGrbNHy7TJToa2fAiIKPVLTOfvY75DqRAtffhO1fpBA==\n-----END PUBLIC KEY-----", + cosignPubKey: pubK, }, csp: cloudprovider.GCP, verifier: &stubRekorVerifier{ @@ -470,17 +506,32 @@ func TestUpgradePlan(t *testing.T) { Header: make(http.Header), } } - if strings.HasSuffix(req.URL.String(), "/measurements.json") { + if strings.HasSuffix(req.URL.String(), "azure/measurements.json") { return &http.Response{ StatusCode: tc.measurementsFetchStatus, - Body: io.NopCloser(strings.NewReader("0: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n")), + Body: io.NopCloser(strings.NewReader(`{"csp":"azure","image":"v1.0.0","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`)), Header: make(http.Header), } } - if strings.HasSuffix(req.URL.String(), "/measurements.json.sig") { + if strings.HasSuffix(req.URL.String(), "azure/measurements.json.sig") { return &http.Response{ StatusCode: tc.measurementsFetchStatus, - Body: io.NopCloser(strings.NewReader("MEUCIBs1g2/n0FsgPfJ+0uLD5TaunGhxwDcQcUGBroejKvg3AiEAzZtcLU9O6IiVhxB8tBS+ty6MXoPNwL8WRWMzyr35eKI=")), + Body: io.NopCloser(strings.NewReader("MEQCIFh8CVELp/Da2U2Jt404OXsUeDfqtrf3pqGRuvxnxhI8AiBTHF9tHEPwFedYG3Jgn2ELOxss+Ybc6135vEtClBrbpg==")), + Header: make(http.Header), + } + } + + if strings.HasSuffix(req.URL.String(), "gcp/measurements.json") { + return &http.Response{ + StatusCode: tc.measurementsFetchStatus, + Body: io.NopCloser(strings.NewReader(`{"csp":"gcp","image":"v1.0.0","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`)), + Header: make(http.Header), + } + } + if strings.HasSuffix(req.URL.String(), "gcp/measurements.json.sig") { + return &http.Response{ + StatusCode: tc.measurementsFetchStatus, + Body: io.NopCloser(strings.NewReader("MEYCIQCr/gDGjj11mR5OeImwOLjxnBqMbBmqoK7yXqy0cXR3HQIhALpVDdYwR9VNJnWwtl8bTfrezyJbc7UNZJO4PJe+stFP")), Header: make(http.Header), } } diff --git a/hack/pcr-reader/main.go b/hack/pcr-reader/main.go index acd657e72..f74e9a063 100644 --- a/hack/pcr-reader/main.go +++ b/hack/pcr-reader/main.go @@ -22,6 +22,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/crypto" "github.com/edgelesssys/constellation/v2/verify/verifyproto" @@ -73,7 +74,7 @@ func main() { if *metadata { outputWithMetadata := measurements.WithMetadata{ - CSP: strings.ToLower(*csp), + CSP: cloudprovider.FromString(*csp), Image: strings.ToLower(*image), Measurements: pcrs, } diff --git a/hack/pcr-reader/main_test.go b/hack/pcr-reader/main_test.go index e757fc5dc..33de77de0 100644 --- a/hack/pcr-reader/main_test.go +++ b/hack/pcr-reader/main_test.go @@ -15,6 +15,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/google/go-tpm-tools/proto/attest" "github.com/google/go-tpm-tools/proto/tpm" "github.com/stretchr/testify/assert" @@ -152,22 +153,22 @@ func TestPrintPCRs(t *testing.T) { func TestPrintPCRsWithMetadata(t *testing.T) { testCases := map[string]struct { format string - csp string + csp cloudprovider.Provider image string }{ "json": { format: "json", - csp: "azure", + csp: cloudprovider.Azure, image: "v2.0.0", }, "yaml": { - csp: "gcp", + csp: cloudprovider.GCP, image: "v2.0.0-testimage", format: "yaml", }, "empty format": { format: "", - csp: "qemu", + csp: cloudprovider.QEMU, image: "v2.0.0-testimage", }, "empty": {}, diff --git a/internal/attestation/measurements/measurements.go b/internal/attestation/measurements/measurements.go index 871599738..2364dc4e5 100644 --- a/internal/attestation/measurements/measurements.go +++ b/internal/attestation/measurements/measurements.go @@ -40,15 +40,18 @@ type M map[uint32]Measurement // WithMetadata is a struct supposed to provide CSP & image metadata next to measurements. type WithMetadata struct { - CSP string `json:"csp" yaml:"csp"` - Image string `json:"image" yaml:"image"` - Measurements M `json:"measurements" yaml:"measurements"` + CSP cloudprovider.Provider `json:"csp" yaml:"csp"` + Image string `json:"image" yaml:"image"` + Measurements M `json:"measurements" yaml:"measurements"` } // FetchAndVerify fetches measurement and signature files via provided URLs, // using client for download. The publicKey is used to verify the measurements. // The hash of the fetched measurements is returned. -func (m *M) FetchAndVerify(ctx context.Context, client *http.Client, measurementsURL *url.URL, signatureURL *url.URL, publicKey []byte) (string, error) { +func (m *M) FetchAndVerify( + ctx context.Context, client *http.Client, measurementsURL, signatureURL *url.URL, + publicKey []byte, metadata WithMetadata, +) (string, error) { measurements, err := getFromURL(ctx, client, measurementsURL) if err != nil { return "", fmt.Errorf("failed to fetch measurements: %w", err) @@ -61,8 +64,9 @@ func (m *M) FetchAndVerify(ctx context.Context, client *http.Client, measurement return "", err } - if err := json.Unmarshal(measurements, m); err != nil { - if yamlErr := yaml.Unmarshal(measurements, m); yamlErr != nil { + var mWithMetadata WithMetadata + if err := json.Unmarshal(measurements, &mWithMetadata); err != nil { + if yamlErr := yaml.Unmarshal(measurements, &mWithMetadata); yamlErr != nil { return "", multierr.Append( err, fmt.Errorf("trying yaml format: %w", yamlErr), @@ -70,6 +74,15 @@ func (m *M) FetchAndVerify(ctx context.Context, client *http.Client, measurement } } + if mWithMetadata.CSP != metadata.CSP { + return "", fmt.Errorf("invalid measurement metadata: CSP mismatch: expected %s, got %s", metadata.CSP, mWithMetadata.CSP) + } + if mWithMetadata.Image != metadata.Image { + return "", fmt.Errorf("invalid measurement metadata: image mismatch: expected %s, got %s", metadata.Image, mWithMetadata.Image) + } + + *m = mWithMetadata.Measurements + shaHash := sha256.Sum256(measurements) return hex.EncodeToString(shaHash[:]), nil diff --git a/internal/attestation/measurements/measurements_test.go b/internal/attestation/measurements/measurements_test.go index 07b0d7ef5..86f815ce0 100644 --- a/internal/attestation/measurements/measurements_test.go +++ b/internal/attestation/measurements/measurements_test.go @@ -15,6 +15,7 @@ import ( "strings" "testing" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" @@ -278,80 +279,86 @@ func TestMeasurementsFetchAndVerify(t *testing.T) { // TDcvRjBBVy9vUDVqZXR3dmJMNmQxOEhjck9kWE8yVmYxY2w0YzNLZjVRcnFSZzlN // dlRxQWFsNXJCNHNpY1JaMVhpUUJjb0YwNHc9PSJ9 // -----END ENCRYPTED COSIGN PRIVATE KEY----- + cosignPublicKey := []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----") testCases := map[string]struct { measurements string + metadata WithMetadata measurementsStatus int signature string signatureStatus int - publicKey []byte wantMeasurements M wantSHA string wantError bool }{ - "simple": { - measurements: "0: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n", - measurementsStatus: http.StatusOK, - signature: "MEUCIQDcHS2bLls7OrLHpQKuiFGXhPrTcehPDwgVyERHl4V02wIgeIxK4J9oJpXWRBjokbog2lgifRXuJK8ljlAID26MbHk=", - signatureStatus: http.StatusOK, - publicKey: []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----"), - wantMeasurements: M{ - 0: WithAllBytes(0x00, false), - }, - wantSHA: "4cd9d6ed8d9322150dff7738994c5e2fabff35f3bae6f5c993412d13249a5e87", - }, "json measurements": { - measurements: `{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}`, + measurements: `{"csp":"test","image":"test","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`, + metadata: WithMetadata{CSP: cloudprovider.Unknown, Image: "test"}, measurementsStatus: http.StatusOK, - signature: "MEUCIQDh3nCgrdTiYWiV4NkiaZ6vxovj79Pk8V90mdWAnmCEOwIgMAVWAx5dW0saut+8X15SgtBEiKqEixYiSICSqqhxUMg=", + signature: "MEYCIQD1RR91pWPw1BMWXTSmTBHg/JtfKerbZNQ9PJTWDdW0sgIhANQbETJGb67qzQmMVmcq007VUFbHRMtYWKZeeyRf0gVa", signatureStatus: http.StatusOK, - publicKey: []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----"), wantMeasurements: M{ 0: WithAllBytes(0x00, false), }, - wantSHA: "1da09758c89537946496358f80b892e508563fcbbc695c90b6c16bf158e69c11", + wantSHA: "c04e13c1312b6f5659303871d14bf49b05c99a6515548763b6322f60bbb61a24", }, "yaml measurements": { - measurements: "0:\n expected: \"0000000000000000000000000000000000000000000000000000000000000000\"\n warnOnly: false\n", + measurements: "csp: test\nimage: test\nmeasurements:\n 0:\n expected: \"0000000000000000000000000000000000000000000000000000000000000000\"\n warnOnly: false\n", + metadata: WithMetadata{CSP: cloudprovider.Unknown, Image: "test"}, measurementsStatus: http.StatusOK, - signature: "MEUCIFzQdwBS92aJjY0bcIag1uQRl42lUSBmmjEvO0tM/N0ZAiEAvuWaP744qYMw5uEmc7BY4mm4Ij3TEqAWFgxNhFkckp4=", + signature: "MEUCIQC9WI2ijlQjBktYFctKpbnqkUTey3U9W99Jp1NTLi5AbQIgNZxxOtiawgTkWPXLoH9D2CxpEjxQrqLn/zWF6NoKxWQ=", signatureStatus: http.StatusOK, - publicKey: []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----"), wantMeasurements: M{ 0: WithAllBytes(0x00, false), }, - wantSHA: "c651cd419fd536c63cfc5349ad44da140a09987465e31192660059d383413807", + wantSHA: "648fcfd5d22e623a948ab2dd4eb334be2701d8f158231726084323003daab8d4", }, "404 measurements": { - measurements: `{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}`, + measurements: `{"csp":"test","image":"test","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`, + metadata: WithMetadata{CSP: cloudprovider.Unknown, Image: "test"}, measurementsStatus: http.StatusNotFound, - signature: "MEUCIQDh3nCgrdTiYWiV4NkiaZ6vxovj79Pk8V90mdWAnmCEOwIgMAVWAx5dW0saut+8X15SgtBEiKqEixYiSICSqqhxUMg=", + signature: "MEYCIQD1RR91pWPw1BMWXTSmTBHg/JtfKerbZNQ9PJTWDdW0sgIhANQbETJGb67qzQmMVmcq007VUFbHRMtYWKZeeyRf0gVa", signatureStatus: http.StatusOK, - publicKey: []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----"), wantError: true, }, "404 signature": { - measurements: `{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}`, + measurements: `{"csp":"test","image":"test","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`, + metadata: WithMetadata{CSP: cloudprovider.Unknown, Image: "test"}, measurementsStatus: http.StatusOK, - signature: "MEUCIQDh3nCgrdTiYWiV4NkiaZ6vxovj79Pk8V90mdWAnmCEOwIgMAVWAx5dW0saut+8X15SgtBEiKqEixYiSICSqqhxUMg=", + signature: "MEYCIQD1RR91pWPw1BMWXTSmTBHg/JtfKerbZNQ9PJTWDdW0sgIhANQbETJGb67qzQmMVmcq007VUFbHRMtYWKZeeyRf0gVa", signatureStatus: http.StatusNotFound, - publicKey: []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----"), wantError: true, }, "broken signature": { - measurements: `{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}`, + measurements: `{"csp":"test","image":"test","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`, + metadata: WithMetadata{CSP: cloudprovider.Unknown, Image: "test"}, measurementsStatus: http.StatusOK, - signature: "AAAAAAAA3nCgrdTiYWiV4NkiaZ6vxovj79Pk8V90mdWAnmCEOwIgMAVWAx5dW0saut+8X15SgtBEiKqEixYiSICSqqhxUMg=", + signature: "AAAAAAA1RR91pWPw1BMWXTSmTBHg/JtfKerbZNQ9PJTWDdW0sgIhANQbETJGb67qzQmMVmcq007VUFbHRMtYWKZeeyRf0gVa", + signatureStatus: http.StatusOK, + wantError: true, + }, + "metadata CSP mismatch": { + measurements: `{"csp":"test","image":"test","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`, + metadata: WithMetadata{CSP: cloudprovider.GCP, Image: "test"}, + measurementsStatus: http.StatusOK, + signature: "MEYCIQD1RR91pWPw1BMWXTSmTBHg/JtfKerbZNQ9PJTWDdW0sgIhANQbETJGb67qzQmMVmcq007VUFbHRMtYWKZeeyRf0gVa", + signatureStatus: http.StatusOK, + wantError: true, + }, + "metadata image mismatch": { + measurements: `{"csp":"test","image":"test","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`, + metadata: WithMetadata{CSP: cloudprovider.Unknown, Image: "another-image"}, + measurementsStatus: http.StatusOK, + signature: "MEYCIQD1RR91pWPw1BMWXTSmTBHg/JtfKerbZNQ9PJTWDdW0sgIhANQbETJGb67qzQmMVmcq007VUFbHRMtYWKZeeyRf0gVa", signatureStatus: http.StatusOK, - publicKey: []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----"), wantError: true, }, "not yaml or json": { measurements: "This is some content to be signed!\n", + metadata: WithMetadata{CSP: cloudprovider.Unknown, Image: "test"}, measurementsStatus: http.StatusOK, signature: "MEUCIQCGA/lSu5qCJgNNvgMaTKJ9rj6vQMecUDaQo3ukaiAfUgIgWoxXRoDKLY9naN7YgxokM7r2fwnyYk3M2WKJJO1g6yo=", signatureStatus: http.StatusOK, - publicKey: []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----"), wantError: true, }, } @@ -386,7 +393,13 @@ func TestMeasurementsFetchAndVerify(t *testing.T) { }) m := M{} - hash, err := m.FetchAndVerify(context.Background(), client, measurementsURL, signatureURL, tc.publicKey) + + hash, err := m.FetchAndVerify( + context.Background(), client, + measurementsURL, signatureURL, + cosignPublicKey, + tc.metadata, + ) if tc.wantError { assert.Error(err) diff --git a/internal/cloud/cloudprovider/cloudprovider.go b/internal/cloud/cloudprovider/cloudprovider.go index e578e8783..31452653e 100644 --- a/internal/cloud/cloudprovider/cloudprovider.go +++ b/internal/cloud/cloudprovider/cloudprovider.go @@ -44,6 +44,21 @@ func (p *Provider) UnmarshalJSON(b []byte) error { return nil } +// MarshalYAML marshals the Provider to YAML string. +func (p Provider) MarshalYAML() (interface{}, error) { + return p.String(), nil +} + +// UnmarshalYAML unmarshals the Provider from YAML string. +func (p *Provider) UnmarshalYAML(unmarshal func(any) error) error { + var s string + if err := unmarshal(&s); err != nil { + return err + } + *p = FromString(s) + return nil +} + // FromString returns cloud provider from string. func FromString(s string) Provider { s = strings.ToLower(s) diff --git a/internal/cloud/cloudprovider/cloudprovider_test.go b/internal/cloud/cloudprovider/cloudprovider_test.go index 76f918f89..73a3b9135 100644 --- a/internal/cloud/cloudprovider/cloudprovider_test.go +++ b/internal/cloud/cloudprovider/cloudprovider_test.go @@ -7,9 +7,11 @@ SPDX-License-Identifier: AGPL-3.0-only package cloudprovider import ( + "encoding/json" "testing" "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" ) func TestMarshalJSON(t *testing.T) { @@ -43,7 +45,7 @@ func TestMarshalJSON(t *testing.T) { t.Run(name, func(t *testing.T) { assert := assert.New(t) - b, err := tc.input.MarshalJSON() + b, err := json.Marshal(tc.input) assert.NoError(err) assert.Equal(tc.want, b) @@ -88,7 +90,95 @@ func TestUnmarshalJSON(t *testing.T) { assert := assert.New(t) var p Provider - err := p.UnmarshalJSON(tc.input) + err := json.Unmarshal(tc.input, &p) + + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + assert.Equal(tc.want, p) + } + }) + } +} + +func TestMarshalYAML(t *testing.T) { + testCases := map[string]struct { + input Provider + want []byte + }{ + "unknown": { + input: Unknown, + want: []byte("Unknown\n"), + }, + "aws": { + input: AWS, + want: []byte("AWS\n"), + }, + "azure": { + input: Azure, + want: []byte("Azure\n"), + }, + "gcp": { + input: GCP, + want: []byte("GCP\n"), + }, + "qemu": { + input: QEMU, + want: []byte("QEMU\n"), + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + b, err := yaml.Marshal(tc.input) + + assert.NoError(err) + assert.Equal(tc.want, b) + }) + } +} + +func TestUnmarshalYAML(t *testing.T) { + testCases := map[string]struct { + input []byte + want Provider + wantErr bool + }{ + "empty": { + input: []byte("foo: bar\n"), + wantErr: true, + }, + "unknown": { + input: []byte("unknown\n"), + want: Unknown, + }, + "aws": { + input: []byte("aws\n"), + want: AWS, + }, + "azure": { + input: []byte("azure\n"), + want: Azure, + }, + "gcp": { + input: []byte("gcp\n"), + want: GCP, + }, + "qemu": { + input: []byte("qemu\n"), + want: QEMU, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + var p Provider + err := yaml.Unmarshal(tc.input, &p) if tc.wantErr { assert.Error(err) diff --git a/internal/config/config.go b/internal/config/config.go index 6b74c43eb..61309ea7e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -73,6 +73,10 @@ type UpgradeConfig struct { // description: | // Measurements of the updated image. Measurements Measurements `yaml:"measurements"` + // description: | + // temporary field for upgrade migration + // TODO(AB#2654): Remove with refactoring upgrade plan command + CSP cloudprovider.Provider `yaml:"csp"` } // ProviderConfig are cloud-provider specific configuration values used by the CLI. diff --git a/internal/config/config_doc.go b/internal/config/config_doc.go index e5866d6a3..17178cc85 100644 --- a/internal/config/config_doc.go +++ b/internal/config/config_doc.go @@ -74,7 +74,7 @@ func init() { FieldName: "upgrade", }, } - UpgradeConfigDoc.Fields = make([]encoder.Doc, 2) + UpgradeConfigDoc.Fields = make([]encoder.Doc, 3) UpgradeConfigDoc.Fields[0].Name = "image" UpgradeConfigDoc.Fields[0].Type = "string" UpgradeConfigDoc.Fields[0].Note = "" @@ -85,6 +85,11 @@ func init() { UpgradeConfigDoc.Fields[1].Note = "" UpgradeConfigDoc.Fields[1].Description = "Measurements of the updated image." UpgradeConfigDoc.Fields[1].Comments[encoder.LineComment] = "Measurements of the updated image." + UpgradeConfigDoc.Fields[2].Name = "csp" + UpgradeConfigDoc.Fields[2].Type = "Provider" + UpgradeConfigDoc.Fields[2].Note = "" + UpgradeConfigDoc.Fields[2].Description = "temporary field for upgrade migration\nTODO(AB#2654): Remove with refactoring upgrade plan command" + UpgradeConfigDoc.Fields[2].Comments[encoder.LineComment] = "temporary field for upgrade migration" ProviderConfigDoc.Type = "ProviderConfig" ProviderConfigDoc.Comments[encoder.LineComment] = "ProviderConfig are cloud-provider specific configuration values used by the CLI." diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 1a76d05cf..1b41d5a7e 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -148,18 +148,12 @@ const ( // Releases. // - // S3PublicBucket contains measurements & releases. - S3PublicBucket = "https://public-edgeless-constellation.s3.us-east-2.amazonaws.com/" - // CosignPublicKey signs all our releases. - CosignPublicKey = `-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEf8F1hpmwE+YCFXzjGtaQcrL6XZVT -JmEe5iSLvG1SyQSAew7WdMKF6o9t8e2TFuCkzlOhhlws2OHWbiFZnFWCFw== ------END PUBLIC KEY----- -` - - // ImageVersionRepositoryURL is the base URL of the repository containing - // image version information. - ImageVersionRepositoryURL = "https://cdn.confidential.cloud" + // CDNRepositoryURL is the base URL of the Constellation CDN artifact repository. + CDNRepositoryURL = "https://cdn.confidential.cloud" + // CDNImagePath is the default path to image references in the CDN repository. + CDNImagePath = "constellation/v1/images" + // CDNMeasurementsPath is the default path to image measurements in the CDN repository. + CDNMeasurementsPath = "constellation/v1/measurements" ) // VersionInfo is the version of a binary. Left as a separate variable to allow override during build. diff --git a/internal/constants/keys_enterprise.go b/internal/constants/keys_enterprise.go new file mode 100644 index 000000000..2f03dd7a6 --- /dev/null +++ b/internal/constants/keys_enterprise.go @@ -0,0 +1,16 @@ +//go:build enterprise + +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package constants + +// CosignPublicKey signs all our releases. +const CosignPublicKey = `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEf8F1hpmwE+YCFXzjGtaQcrL6XZVT +JmEe5iSLvG1SyQSAew7WdMKF6o9t8e2TFuCkzlOhhlws2OHWbiFZnFWCFw== +-----END PUBLIC KEY----- +` diff --git a/internal/constants/keys_oss.go b/internal/constants/keys_oss.go new file mode 100644 index 000000000..e40d681de --- /dev/null +++ b/internal/constants/keys_oss.go @@ -0,0 +1,16 @@ +//go:build !enterprise + +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package constants + +// CosignPublicKey signs all our development builds. +const CosignPublicKey = `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELcPl4Ik+qZuH4K049wksoXK/Os3Z +b92PDCpM7FZAINQF88s1TZS/HmRXYk62UJ4eqPduvUnJmXhNikhLbMi6fw== +-----END PUBLIC KEY----- +` diff --git a/internal/image/image.go b/internal/image/image.go index eddc799c6..9c0c3843e 100644 --- a/internal/image/image.go +++ b/internal/image/image.go @@ -125,12 +125,12 @@ func getFromFile(fs *afero.Afero, version string) ([]byte, error) { // getFromURL fetches the image lookup table from a URL. func getFromURL(ctx context.Context, client httpc, version string) ([]byte, error) { - url, err := url.Parse(constants.ImageVersionRepositoryURL) + url, err := url.Parse(constants.CDNRepositoryURL) if err != nil { return nil, fmt.Errorf("parsing image version repository URL: %w", err) } versionFilename := path.Base(version) + ".json" - url.Path = path.Join("constellation/v1/images", versionFilename) + url.Path = path.Join(constants.CDNImagePath, versionFilename) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), http.NoBody) if err != nil { return nil, err