cli: image measurements (v2)

This commit is contained in:
Malte Poll 2023-05-22 14:59:28 +02:00 committed by Malte Poll
parent 2ebc0cf2c8
commit e5b394db87
18 changed files with 274 additions and 195 deletions

View File

@ -100,16 +100,19 @@ func (cfm *configFetchMeasurementsCmd) configFetchMeasurements(
} }
cfm.log.Debugf("Fetching and verifying measurements") cfm.log.Debugf("Fetching and verifying measurements")
imageVersion, err := versionsapi.NewVersionFromShortPath(conf.Image, versionsapi.VersionKindImage)
if err != nil {
return err
}
var fetchedMeasurements measurements.M var fetchedMeasurements measurements.M
hash, err := fetchedMeasurements.FetchAndVerify( hash, err := fetchedMeasurements.FetchAndVerify(
ctx, client, ctx, client,
flags.measurementsURL, flags.measurementsURL,
flags.signatureURL, flags.signatureURL,
cosignPublicKey, cosignPublicKey,
measurements.WithMetadata{ imageVersion,
CSP: conf.GetProvider(), conf.GetProvider(),
Image: conf.Image, conf.GetAttestationConfig().GetVariant(),
},
) )
if err != nil { if err != nil {
return err return err
@ -182,7 +185,7 @@ func (f *fetchMeasurementsFlags) updateURLs(conf *config.Config) error {
if err != nil { if err != nil {
return fmt.Errorf("creating version from image name: %w", err) return fmt.Errorf("creating version from image name: %w", err)
} }
measurementsURL, signatureURL, err := versionsapi.MeasurementURL(ver, conf.GetProvider()) measurementsURL, signatureURL, err := versionsapi.MeasurementURL(ver)
if err != nil { if err != nil {
return err return err
} }

View File

@ -119,8 +119,8 @@ func TestUpdateURLs(t *testing.T) {
}, },
}, },
flags: &fetchMeasurementsFlags{}, flags: &fetchMeasurementsFlags{},
wantMeasurementsURL: ver.ArtifactsURL() + "/image/csp/gcp/measurements.json", wantMeasurementsURL: ver.ArtifactsURL("v2") + "/image/measurements.json",
wantMeasurementsSigURL: ver.ArtifactsURL() + "/image/csp/gcp/measurements.json.sig", wantMeasurementsSigURL: ver.ArtifactsURL("v2") + "/image/measurements.json.sig",
}, },
"both set by user": { "both set by user": {
conf: &config.Config{ conf: &config.Config{
@ -181,30 +181,58 @@ func TestConfigFetchMeasurements(t *testing.T) {
cosignPublicKey := []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----") cosignPublicKey := []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----")
measurements := `{ measurements := `{
"csp": "gcp", "version": "v999.999.999",
"image": "v999.999.999", "ref": "-",
"measurements": { "stream": "stable",
"0": "0000000000000000000000000000000000000000000000000000000000000000", "list": [
"1": "1111111111111111111111111111111111111111111111111111111111111111", {
"2": "2222222222222222222222222222222222222222222222222222222222222222", "csp": "GCP",
"3": "3333333333333333333333333333333333333333333333333333333333333333", "attestationVariant":"gcp-sev-es",
"4": "4444444444444444444444444444444444444444444444444444444444444444", "measurements": {
"5": "5555555555555555555555555555555555555555555555555555555555555555", "0": {
"6": "6666666666666666666666666666666666666666666666666666666666666666" "expected": "0000000000000000000000000000000000000000000000000000000000000000",
} "warnOnly":false
},
"1": {
"expected": "1111111111111111111111111111111111111111111111111111111111111111",
"warnOnly":false
},
"2": {
"expected": "2222222222222222222222222222222222222222222222222222222222222222",
"warnOnly":false
},
"3": {
"expected": "3333333333333333333333333333333333333333333333333333333333333333",
"warnOnly":false
},
"4": {
"expected": "4444444444444444444444444444444444444444444444444444444444444444",
"warnOnly":false
},
"5": {
"expected": "5555555555555555555555555555555555555555555555555555555555555555",
"warnOnly":false
},
"6": {
"expected": "6666666666666666666666666666666666666666666666666666666666666666",
"warnOnly":false
}
}
}
]
} }
` `
signature := "MEYCIQDRAQNK2NjHJBGrnw3HQAyBsXMCmVCptBdgA6VZ3IlyiAIhAPG42waF1aFZq7dnjP3b2jsMNUtaKYDQQSazW1AX8jgF" signature := "MEUCIHQETkvMRy8WaWMroX4Aa2J86bTW0kGMp8NG0YLXJKZJAiEA7ZdxoQzSTyBFNhZ1bwB5eT3av0biAdb66dJRFxQlKLA="
client := newTestClient(func(req *http.Request) *http.Response { client := newTestClient(func(req *http.Request) *http.Response {
if req.URL.Path == "/constellation/v1/ref/-/stream/stable/v999.999.999/image/csp/gcp/measurements.json" { if req.URL.Path == "/constellation/v2/ref/-/stream/stable/v999.999.999/image/measurements.json" {
return &http.Response{ return &http.Response{
StatusCode: http.StatusOK, StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(measurements)), Body: io.NopCloser(bytes.NewBufferString(measurements)),
Header: make(http.Header), Header: make(http.Header),
} }
} }
if req.URL.Path == "/constellation/v1/ref/-/stream/stable/v999.999.999/image/csp/gcp/measurements.json.sig" { if req.URL.Path == "/constellation/v2/ref/-/stream/stable/v999.999.999/image/measurements.json.sig" {
return &http.Response{ return &http.Response{
StatusCode: http.StatusOK, StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(signature)), Body: io.NopCloser(bytes.NewBufferString(signature)),

View File

@ -26,6 +26,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl" "github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl"
conSemver "github.com/edgelesssys/constellation/v2/internal/semver" conSemver "github.com/edgelesssys/constellation/v2/internal/semver"
"github.com/edgelesssys/constellation/v2/internal/sigstore" "github.com/edgelesssys/constellation/v2/internal/sigstore"
"github.com/edgelesssys/constellation/v2/internal/variant"
"github.com/edgelesssys/constellation/v2/internal/versions" "github.com/edgelesssys/constellation/v2/internal/versions"
"github.com/edgelesssys/constellation/v2/internal/versionsapi" "github.com/edgelesssys/constellation/v2/internal/versionsapi"
"github.com/edgelesssys/constellation/v2/internal/versionsapi/fetcher" "github.com/edgelesssys/constellation/v2/internal/versionsapi/fetcher"
@ -139,7 +140,8 @@ func (u *upgradeCheckCmd) upgradeCheck(cmd *cobra.Command, fileHandler file.Hand
u.log.Debugf("Read configuration from %q", flags.configPath) u.log.Debugf("Read configuration from %q", flags.configPath)
// get current image version of the cluster // get current image version of the cluster
csp := conf.GetProvider() csp := conf.GetProvider()
u.log.Debugf("Using provider %s", csp.String()) attestationVariant := conf.GetAttestationConfig().GetVariant()
u.log.Debugf("Using provider %s with attestation variant %s", csp.String(), attestationVariant.String())
current, err := u.collect.currentVersions(cmd.Context()) current, err := u.collect.currentVersions(cmd.Context())
if err != nil { if err != nil {
@ -167,7 +169,7 @@ func (u *upgradeCheckCmd) upgradeCheck(cmd *cobra.Command, fileHandler file.Hand
semver.Sort(newKubernetes) semver.Sort(newKubernetes)
supported.image = filterImageUpgrades(current.image, supported.image) supported.image = filterImageUpgrades(current.image, supported.image)
newImages, err := u.collect.newMeasurements(cmd.Context(), csp, supported.image) newImages, err := u.collect.newMeasurements(cmd.Context(), csp, attestationVariant, supported.image)
if err != nil { if err != nil {
return err return err
} }
@ -240,7 +242,7 @@ type collector interface {
currentVersions(ctx context.Context) (currentVersionInfo, error) currentVersions(ctx context.Context) (currentVersionInfo, error)
supportedVersions(ctx context.Context, version, currentK8sVersion string) (supportedVersionInfo, error) supportedVersions(ctx context.Context, version, currentK8sVersion string) (supportedVersionInfo, error)
newImages(ctx context.Context, version string) ([]versionsapi.Version, error) newImages(ctx context.Context, version string) ([]versionsapi.Version, error)
newMeasurements(ctx context.Context, csp cloudprovider.Provider, images []versionsapi.Version) (map[string]measurements.M, error) newMeasurements(ctx context.Context, csp cloudprovider.Provider, attestationVariant variant.Variant, images []versionsapi.Version) (map[string]measurements.M, error)
newerVersions(ctx context.Context, allowedVersions []string) ([]versionsapi.Version, error) newerVersions(ctx context.Context, allowedVersions []string) ([]versionsapi.Version, error)
newCLIVersions(ctx context.Context) ([]string, error) newCLIVersions(ctx context.Context) ([]string, error)
filterCompatibleCLIVersions(ctx context.Context, cliPatchVersions []string, currentK8sVersion string) ([]string, error) filterCompatibleCLIVersions(ctx context.Context, cliPatchVersions []string, currentK8sVersion string) ([]string, error)
@ -259,9 +261,9 @@ type versionCollector struct {
log debugLog log debugLog
} }
func (v *versionCollector) newMeasurements(ctx context.Context, csp cloudprovider.Provider, images []versionsapi.Version) (map[string]measurements.M, error) { func (v *versionCollector) newMeasurements(ctx context.Context, csp cloudprovider.Provider, attestationVariant variant.Variant, images []versionsapi.Version) (map[string]measurements.M, error) {
// get expected measurements for each image // get expected measurements for each image
upgrades, err := getCompatibleImageMeasurements(ctx, v.writer, v.client, v.rekor, []byte(v.flags.cosignPubKey), csp, images, v.log) upgrades, err := getCompatibleImageMeasurements(ctx, v.writer, v.client, v.rekor, []byte(v.flags.cosignPubKey), csp, attestationVariant, images, v.log)
if err != nil { if err != nil {
return nil, fmt.Errorf("fetching measurements for compatible images: %w", err) return nil, fmt.Errorf("fetching measurements for compatible images: %w", err)
} }
@ -524,13 +526,13 @@ func getCurrentKubernetesVersion(ctx context.Context, checker upgradeChecker) (s
// getCompatibleImageMeasurements retrieves the expected measurements for each image. // getCompatibleImageMeasurements retrieves the expected measurements for each image.
func getCompatibleImageMeasurements(ctx context.Context, writer io.Writer, client *http.Client, rekor rekorVerifier, pubK []byte, func getCompatibleImageMeasurements(ctx context.Context, writer io.Writer, client *http.Client, rekor rekorVerifier, pubK []byte,
csp cloudprovider.Provider, versions []versionsapi.Version, log debugLog, csp cloudprovider.Provider, attestationVariant variant.Variant, versions []versionsapi.Version, log debugLog,
) (map[string]measurements.M, error) { ) (map[string]measurements.M, error) {
upgrades := make(map[string]measurements.M) upgrades := make(map[string]measurements.M)
for _, version := range versions { for _, version := range versions {
log.Debugf("Fetching measurements for image: %s", version) log.Debugf("Fetching measurements for image: %s", version)
shortPath := version.ShortPath() shortPath := version.ShortPath()
measurementsURL, signatureURL, err := versionsapi.MeasurementURL(version, csp) measurementsURL, signatureURL, err := versionsapi.MeasurementURL(version)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -542,10 +544,9 @@ func getCompatibleImageMeasurements(ctx context.Context, writer io.Writer, clien
measurementsURL, measurementsURL,
signatureURL, signatureURL,
pubK, pubK,
measurements.WithMetadata{ version,
CSP: csp, csp,
Image: shortPath, attestationVariant,
},
) )
if err != nil { if err != nil {
if _, err := fmt.Fprintf(writer, "Skipping compatible image %q: %s\n", shortPath, err); err != nil { if _, err := fmt.Fprintf(writer, "Skipping compatible image %q: %s\n", shortPath, err); err != nil {

View File

@ -150,6 +150,7 @@ func TestGetCompatibleImageMeasurements(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
csp := cloudprovider.Azure csp := cloudprovider.Azure
attestationVariant := variant.AzureSEVSNP{}
zero := versionsapi.Version{ zero := versionsapi.Version{
Ref: "-", Ref: "-",
Stream: "stable", Stream: "stable",
@ -204,7 +205,7 @@ func TestGetCompatibleImageMeasurements(t *testing.T) {
pubK := []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----") pubK := []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----")
upgrades, err := getCompatibleImageMeasurements(context.Background(), &bytes.Buffer{}, client, singleUUIDVerifier(), pubK, csp, images, logger.NewTest(t)) upgrades, err := getCompatibleImageMeasurements(context.Background(), &bytes.Buffer{}, client, singleUUIDVerifier(), pubK, csp, attestationVariant, images, logger.NewTest(t))
assert.NoError(err) assert.NoError(err)
for _, measurement := range upgrades { for _, measurement := range upgrades {
@ -297,7 +298,7 @@ type stubVersionCollector struct {
someErr error someErr error
} }
func (s *stubVersionCollector) newMeasurements(_ context.Context, _ cloudprovider.Provider, _ []versionsapi.Version) (map[string]measurements.M, error) { func (s *stubVersionCollector) newMeasurements(_ context.Context, _ cloudprovider.Provider, _ variant.Variant, _ []versionsapi.Version) (map[string]measurements.M, error) {
return s.supportedImageVersions, nil return s.supportedImageVersions, nil
} }

View File

@ -15,9 +15,9 @@ go_library(
"//internal/cloud/cloudprovider", "//internal/cloud/cloudprovider",
"//internal/constants", "//internal/constants",
"//internal/logger", "//internal/logger",
"//internal/variant",
"//internal/versionsapi", "//internal/versionsapi",
"//internal/versionsapi/fetcher", "//internal/versionsapi/fetcher",
"@in_gopkg_yaml_v3//:yaml_v3",
"@sh_helm_helm_v3//pkg/action", "@sh_helm_helm_v3//pkg/action",
"@sh_helm_helm_v3//pkg/cli", "@sh_helm_helm_v3//pkg/cli",
], ],

View File

@ -10,17 +10,12 @@ package upgrade
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt"
"io"
"net/http" "net/http"
"net/url"
"gopkg.in/yaml.v3"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements" "github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/variant"
"github.com/edgelesssys/constellation/v2/internal/versionsapi" "github.com/edgelesssys/constellation/v2/internal/versionsapi"
"github.com/edgelesssys/constellation/v2/internal/versionsapi/fetcher" "github.com/edgelesssys/constellation/v2/internal/versionsapi/fetcher"
) )
@ -31,7 +26,9 @@ type upgradeInfo struct {
imageRef string imageRef string
} }
func fetchUpgradeInfo(ctx context.Context, csp cloudprovider.Provider, toImage string) (upgradeInfo, error) { func fetchUpgradeInfo(ctx context.Context, csp cloudprovider.Provider,
attestationVariant variant.Variant, toImage string,
) (upgradeInfo, error) {
info := upgradeInfo{ info := upgradeInfo{
measurements: make(measurements.M), measurements: make(measurements.M),
shortPath: toImage, shortPath: toImage,
@ -43,20 +40,17 @@ func fetchUpgradeInfo(ctx context.Context, csp cloudprovider.Provider, toImage s
return upgradeInfo{}, err return upgradeInfo{}, err
} }
measurementsURL, _, err := versionsapi.MeasurementURL(ver, csp) measurementsURL, _, err := versionsapi.MeasurementURL(ver)
if err != nil { if err != nil {
return upgradeInfo{}, err return upgradeInfo{}, err
} }
fetchedMeasurements, err := fetchMeasurements( fetchedMeasurements := measurements.M{}
if err := fetchedMeasurements.FetchNoVerify(
ctx, http.DefaultClient, ctx, http.DefaultClient,
measurementsURL, measurementsURL,
measurements.WithMetadata{ ver, csp, attestationVariant,
CSP: csp, ); err != nil {
Image: toImage,
},
)
if err != nil {
return upgradeInfo{}, err return upgradeInfo{}, err
} }
info.measurements = fetchedMeasurements info.measurements = fetchedMeasurements
@ -74,56 +68,6 @@ func fetchUpgradeInfo(ctx context.Context, csp cloudprovider.Provider, toImage s
return info, nil return info, nil
} }
// fetchMeasurements is essentially a copy of measurements.FetchAndVerify, but with verification removed.
// This is necessary since the e2e tests may target release images for which the measurements are signed with the release public key.
// It is easier to skip verification than to implement a second bazel target with the enterprise build tag set.
func fetchMeasurements(ctx context.Context, client *http.Client, measurementsURL *url.URL, metadata measurements.WithMetadata) (measurements.M, error) {
measurementsRaw, err := getFromURL(ctx, client, measurementsURL)
if err != nil {
return nil, fmt.Errorf("failed to fetch measurements: %w", err)
}
var mWithMetadata measurements.WithMetadata
if err := json.Unmarshal(measurementsRaw, &mWithMetadata); err != nil {
if yamlErr := yaml.Unmarshal(measurementsRaw, &mWithMetadata); yamlErr != nil {
return nil, errors.Join(
err,
fmt.Errorf("trying yaml format: %w", yamlErr),
)
}
}
if mWithMetadata.CSP != metadata.CSP {
return nil, fmt.Errorf("invalid measurement metadata: CSP mismatch: expected %s, got %s", metadata.CSP, mWithMetadata.CSP)
}
if mWithMetadata.Image != metadata.Image {
return nil, fmt.Errorf("invalid measurement metadata: image mismatch: expected %s, got %s", metadata.Image, mWithMetadata.Image)
}
return mWithMetadata.Measurements, nil
}
func getFromURL(ctx context.Context, client *http.Client, sourceURL *url.URL) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, sourceURL.String(), http.NoBody)
if err != nil {
return []byte{}, err
}
resp, err := client.Do(req)
if err != nil {
return []byte{}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return []byte{}, fmt.Errorf("http status code: %d", resp.StatusCode)
}
content, err := io.ReadAll(resp.Body)
if err != nil {
return []byte{}, err
}
return content, nil
}
func fetchImageRef(ctx context.Context, client *fetcher.Fetcher, csp cloudprovider.Provider, imageInfo versionsapi.ImageInfo) (string, error) { func fetchImageRef(ctx context.Context, client *fetcher.Fetcher, csp cloudprovider.Provider, imageInfo versionsapi.ImageInfo) (string, error) {
imageInfo, err := client.FetchImageInfo(ctx, imageInfo) imageInfo, err := client.FetchImageInfo(ctx, imageInfo)
if err != nil { if err != nil {

View File

@ -240,7 +240,12 @@ func writeUpgradeConfig(require *require.Assertions, image string, kubernetes st
} }
require.NoError(err, longMsg) require.NoError(err, longMsg)
info, err := fetchUpgradeInfo(context.Background(), cfg.GetProvider(), image) info, err := fetchUpgradeInfo(
context.Background(),
cfg.GetProvider(),
cfg.GetAttestationConfig().GetVariant(),
image,
)
require.NoError(err) require.NoError(err)
log.Printf("Setting image version: %s\n", info.shortPath) log.Printf("Setting image version: %s\n", info.shortPath)

View File

@ -16,6 +16,7 @@ go_library(
"//internal/cloud/cloudprovider", "//internal/cloud/cloudprovider",
"//internal/sigstore", "//internal/sigstore",
"//internal/variant", "//internal/variant",
"//internal/versionsapi",
"@com_github_google_go_tpm//tpmutil", "@com_github_google_go_tpm//tpmutil",
"@com_github_siderolabs_talos_pkg_machinery//config/encoder", "@com_github_siderolabs_talos_pkg_machinery//config/encoder",
"@in_gopkg_yaml_v3//:yaml_v3", "@in_gopkg_yaml_v3//:yaml_v3",
@ -28,6 +29,8 @@ go_test(
embed = [":measurements"], embed = [":measurements"],
deps = [ deps = [
"//internal/cloud/cloudprovider", "//internal/cloud/cloudprovider",
"//internal/variant",
"//internal/versionsapi",
"@com_github_siderolabs_talos_pkg_machinery//config/encoder", "@com_github_siderolabs_talos_pkg_machinery//config/encoder",
"@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require", "@com_github_stretchr_testify//require",

View File

@ -82,7 +82,7 @@ func main() {
log.Println("Found", variant) log.Println("Found", variant)
returnStmtCtr++ returnStmtCtr++
// retrieve and validate measurements for the given CSP and image // retrieve and validate measurements for the given CSP and image
measuremnts := mustGetMeasurements(ctx, rekor, []byte(constants.CosignPublicKey), http.DefaultClient, provider, defaultConf.Image) measuremnts := mustGetMeasurements(ctx, rekor, []byte(constants.CosignPublicKey), http.DefaultClient, provider, variant, defaultConf.Image)
// replace the return statement with a composite literal containing the validated measurements // replace the return statement with a composite literal containing the validated measurements
clause.Values[0] = measurementsCompositeLiteral(measuremnts) clause.Values[0] = measurementsCompositeLiteral(measuremnts)
} }
@ -107,12 +107,17 @@ func main() {
} }
// mustGetMeasurements fetches the measurements for the given image and CSP and verifies them. // mustGetMeasurements fetches the measurements for the given image and CSP and verifies them.
func mustGetMeasurements(ctx context.Context, verifier rekorVerifier, cosignPublicKey []byte, client *http.Client, provider cloudprovider.Provider, image string) measurements.M { func mustGetMeasurements(ctx context.Context, verifier rekorVerifier, cosignPublicKey []byte, client *http.Client, provider cloudprovider.Provider, attestationVariant variant.Variant, image string) measurements.M {
measurementsURL, err := measurementURL(provider, image, "measurements.json") measurementsURL, err := measurementURL(image, "measurements.json")
if err != nil { if err != nil {
panic(err) panic(err)
} }
signatureURL, err := measurementURL(provider, image, "measurements.json.sig") signatureURL, err := measurementURL(image, "measurements.json.sig")
if err != nil {
panic(err)
}
imageVersion, err := versionsapi.NewVersionFromShortPath(image, versionsapi.VersionKindImage)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -124,10 +129,9 @@ func mustGetMeasurements(ctx context.Context, verifier rekorVerifier, cosignPubl
measurementsURL, measurementsURL,
signatureURL, signatureURL,
cosignPublicKey, cosignPublicKey,
measurements.WithMetadata{ imageVersion,
CSP: provider, provider,
Image: image, attestationVariant,
},
) )
if err != nil { if err != nil {
panic(err) panic(err)
@ -139,14 +143,14 @@ func mustGetMeasurements(ctx context.Context, verifier rekorVerifier, cosignPubl
} }
// measurementURL returns the URL for the measurements file for the given image and CSP. // measurementURL returns the URL for the measurements file for the given image and CSP.
func measurementURL(provider cloudprovider.Provider, image, file string) (*url.URL, error) { func measurementURL(image, file string) (*url.URL, error) {
version, err := versionsapi.NewVersionFromShortPath(image, versionsapi.VersionKindImage) version, err := versionsapi.NewVersionFromShortPath(image, versionsapi.VersionKindImage)
if err != nil { if err != nil {
return nil, fmt.Errorf("parsing image name: %w", err) return nil, fmt.Errorf("parsing image name: %w", err)
} }
return url.Parse( return url.Parse(
version.ArtifactsURL() + path.Join("/image", "csp", strings.ToLower(provider.String()), file), version.ArtifactsURL("v2") + path.Join("/image", file),
) )
} }

View File

@ -28,12 +28,14 @@ import (
"sort" "sort"
"strconv" "strconv"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/sigstore"
"github.com/edgelesssys/constellation/v2/internal/variant"
"github.com/google/go-tpm/tpmutil" "github.com/google/go-tpm/tpmutil"
"github.com/siderolabs/talos/pkg/machinery/config/encoder" "github.com/siderolabs/talos/pkg/machinery/config/encoder"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/sigstore"
"github.com/edgelesssys/constellation/v2/internal/variant"
"github.com/edgelesssys/constellation/v2/internal/versionsapi"
) )
const ( const (
@ -67,6 +69,22 @@ type WithMetadata struct {
Measurements M `json:"measurements" yaml:"measurements"` Measurements M `json:"measurements" yaml:"measurements"`
} }
// ImageMeasurementsV2 is a struct to hold measurements for a specific image.
// .List contains measurements for all variants of the image.
type ImageMeasurementsV2 struct {
Version string `json:"version" yaml:"version"`
Ref string `json:"ref" yaml:"ref"`
Stream string `json:"stream" yaml:"stream"`
List []ImageMeasurementsV2Entry `json:"list" yaml:"list"`
}
// ImageMeasurementsV2Entry is a struct to hold measurements for one variant of a specific image.
type ImageMeasurementsV2Entry struct {
CSP cloudprovider.Provider `json:"csp" yaml:"csp"`
AttestationVariant string `json:"attestationVariant" yaml:"attestationVariant"`
Measurements M `json:"measurements" yaml:"measurements"`
}
// MarshalYAML returns the YAML encoding of m. // MarshalYAML returns the YAML encoding of m.
func (m M) MarshalYAML() (any, error) { func (m M) MarshalYAML() (any, error) {
// cast to prevent infinite recursion // cast to prevent infinite recursion
@ -86,9 +104,9 @@ func (m M) MarshalYAML() (any, error) {
// The hash of the fetched measurements is returned. // The hash of the fetched measurements is returned.
func (m *M) FetchAndVerify( func (m *M) FetchAndVerify(
ctx context.Context, client *http.Client, measurementsURL, signatureURL *url.URL, ctx context.Context, client *http.Client, measurementsURL, signatureURL *url.URL,
publicKey []byte, metadata WithMetadata, publicKey []byte, version versionsapi.Version, csp cloudprovider.Provider, attestationVariant variant.Variant,
) (string, error) { ) (string, error) {
measurements, err := getFromURL(ctx, client, measurementsURL) measurementsRaw, err := getFromURL(ctx, client, measurementsURL)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to fetch measurements: %w", err) return "", fmt.Errorf("failed to fetch measurements: %w", err)
} }
@ -96,34 +114,38 @@ func (m *M) FetchAndVerify(
if err != nil { if err != nil {
return "", fmt.Errorf("failed to fetch signature: %w", err) return "", fmt.Errorf("failed to fetch signature: %w", err)
} }
if err := sigstore.VerifySignature(measurements, signature, publicKey); err != nil { if err := sigstore.VerifySignature(measurementsRaw, signature, publicKey); err != nil {
return "", err return "", err
} }
var mWithMetadata WithMetadata var measurements ImageMeasurementsV2
if err := json.Unmarshal(measurements, &mWithMetadata); err != nil { if err := json.Unmarshal(measurementsRaw, &measurements); err != nil {
if yamlErr := yaml.Unmarshal(measurements, &mWithMetadata); yamlErr != nil { return "", err
return "", errors.Join(
err,
fmt.Errorf("trying yaml format: %w", yamlErr),
)
}
} }
if err := m.fromImageMeasurementsV2(measurements, version, csp, attestationVariant); err != nil {
if mWithMetadata.CSP != metadata.CSP { return "", err
return "", fmt.Errorf("invalid measurement metadata: CSP mismatch: expected %s, got %s", metadata.CSP, mWithMetadata.CSP)
} }
if mWithMetadata.Image != metadata.Image { shaHash := sha256.Sum256(measurementsRaw)
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 return hex.EncodeToString(shaHash[:]), nil
} }
// FetchNoVerify fetches measurement via provided URLs,
// using client for download. Measurements are not verified.
func (m *M) FetchNoVerify(ctx context.Context, client *http.Client, measurementsURL *url.URL,
version versionsapi.Version, csp cloudprovider.Provider, attestationVariant variant.Variant,
) error {
measurementsRaw, err := getFromURL(ctx, client, measurementsURL)
if err != nil {
return fmt.Errorf("failed to fetch measurements: %w", err)
}
var measurements ImageMeasurementsV2
if err := json.Unmarshal(measurementsRaw, &measurements); err != nil {
return err
}
return m.fromImageMeasurementsV2(measurements, version, csp, attestationVariant)
}
// CopyFrom copies over all values from other. Overwriting existing values, // CopyFrom copies over all values from other. Overwriting existing values,
// but keeping not specified values untouched. // but keeping not specified values untouched.
func (m *M) CopyFrom(other M) { func (m *M) CopyFrom(other M) {
@ -232,6 +254,51 @@ func (m *M) UnmarshalYAML(unmarshal func(any) error) error {
return nil return nil
} }
func (m *M) fromImageMeasurementsV2(
measurements ImageMeasurementsV2, wantVersion versionsapi.Version,
csp cloudprovider.Provider, attestationVariant variant.Variant,
) error {
gotVersion := versionsapi.Version{
Ref: measurements.Ref,
Stream: measurements.Stream,
Version: measurements.Version,
Kind: versionsapi.VersionKindImage,
}
if !wantVersion.Equal(gotVersion) {
return fmt.Errorf("invalid measurement metadata: version mismatch: expected %s, got %s", wantVersion.ShortPath(), gotVersion.ShortPath())
}
// find measurements for requested image in list
var measurementsEntry ImageMeasurementsV2Entry
var found bool
for _, entry := range measurements.List {
gotCSP := entry.CSP
if gotCSP != csp {
continue
}
gotAttestationVariant, err := variant.FromString(entry.AttestationVariant)
if err != nil {
continue
}
if gotAttestationVariant == nil || attestationVariant == nil {
continue
}
if !gotAttestationVariant.Equal(attestationVariant) {
continue
}
measurementsEntry = entry
found = true
break
}
if !found {
return fmt.Errorf("invalid measurement metadata: no measurements found for csp %s, attestationVariant %s and image %s", csp.String(), attestationVariant, wantVersion.ShortPath())
}
*m = measurementsEntry.Measurements
return nil
}
// Measurement wraps expected PCR value and whether it is enforced. // Measurement wraps expected PCR value and whether it is enforced.
type Measurement struct { type Measurement struct {
// Expected measurement value. // Expected measurement value.

View File

@ -15,11 +15,14 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/siderolabs/talos/pkg/machinery/config/encoder" "github.com/siderolabs/talos/pkg/machinery/config/encoder"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/variant"
"github.com/edgelesssys/constellation/v2/internal/versionsapi"
) )
func TestMarshal(t *testing.T) { func TestMarshal(t *testing.T) {
@ -354,7 +357,9 @@ func TestMeasurementsFetchAndVerify(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
measurements string measurements string
metadata WithMetadata csp cloudprovider.Provider
attestationVariant variant.Variant
imageVersion versionsapi.Version
measurementsStatus int measurementsStatus int
signature string signature string
signatureStatus int signatureStatus int
@ -363,70 +368,66 @@ func TestMeasurementsFetchAndVerify(t *testing.T) {
wantError bool wantError bool
}{ }{
"json measurements": { "json measurements": {
measurements: `{"csp":"test","image":"test","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`, measurements: `{"version":"v1.0.0-test","ref":"-","stream":"stable","list":[{"csp":"Unknown","attestationVariant":"dummy","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}]}`,
metadata: WithMetadata{CSP: cloudprovider.Unknown, Image: "test"}, csp: cloudprovider.Unknown,
imageVersion: versionsapi.Version{Ref: "-", Stream: "stable", Version: "v1.0.0-test", Kind: versionsapi.VersionKindImage},
measurementsStatus: http.StatusOK, measurementsStatus: http.StatusOK,
signature: "MEYCIQD1RR91pWPw1BMWXTSmTBHg/JtfKerbZNQ9PJTWDdW0sgIhANQbETJGb67qzQmMVmcq007VUFbHRMtYWKZeeyRf0gVa", signature: "MEUCIHuW2420EqN4Kj6OEaVMmufH7d01vyR1J+SWg8H4elyBAiEA1Ki5Hfq0iI70qpViYbrTFrd8e840NjtdAxGqJKiJgbA=",
signatureStatus: http.StatusOK, signatureStatus: http.StatusOK,
wantMeasurements: M{ wantMeasurements: M{
0: WithAllBytes(0x00, Enforce, PCRMeasurementLength), 0: WithAllBytes(0x00, Enforce, PCRMeasurementLength),
}, },
wantSHA: "c04e13c1312b6f5659303871d14bf49b05c99a6515548763b6322f60bbb61a24", wantSHA: "7269a1e8c6a379b86af605f993352df1d4a289bbf79fe655fd78338bd7549d52",
},
"yaml measurements": {
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: "MEUCIQC9WI2ijlQjBktYFctKpbnqkUTey3U9W99Jp1NTLi5AbQIgNZxxOtiawgTkWPXLoH9D2CxpEjxQrqLn/zWF6NoKxWQ=",
signatureStatus: http.StatusOK,
wantMeasurements: M{
0: WithAllBytes(0x00, Enforce, PCRMeasurementLength),
},
wantSHA: "648fcfd5d22e623a948ab2dd4eb334be2701d8f158231726084323003daab8d4",
}, },
"404 measurements": { "404 measurements": {
measurements: `{"csp":"test","image":"test","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`, measurements: `{"version":"v1.0.0-test","ref":"-","stream":"stable","list":[{"csp":"Unknown","attestationVariant":"dummy","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}]}`,
metadata: WithMetadata{CSP: cloudprovider.Unknown, Image: "test"}, csp: cloudprovider.Unknown,
imageVersion: versionsapi.Version{Ref: "-", Stream: "stable", Version: "v1.0.0-test", Kind: versionsapi.VersionKindImage},
measurementsStatus: http.StatusNotFound, measurementsStatus: http.StatusNotFound,
signature: "MEYCIQD1RR91pWPw1BMWXTSmTBHg/JtfKerbZNQ9PJTWDdW0sgIhANQbETJGb67qzQmMVmcq007VUFbHRMtYWKZeeyRf0gVa", signature: "MEUCIHuW2420EqN4Kj6OEaVMmufH7d01vyR1J+SWg8H4elyBAiEA1Ki5Hfq0iI70qpViYbrTFrd8e840NjtdAxGqJKiJgbA=",
signatureStatus: http.StatusOK, signatureStatus: http.StatusOK,
wantError: true, wantError: true,
}, },
"404 signature": { "404 signature": {
measurements: `{"csp":"test","image":"test","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`, measurements: `{"version":"v1.0.0-test","ref":"-","stream":"stable","list":[{"csp":"Unknown","attestationVariant":"dummy","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}]}`,
metadata: WithMetadata{CSP: cloudprovider.Unknown, Image: "test"}, csp: cloudprovider.Unknown,
imageVersion: versionsapi.Version{Ref: "-", Stream: "stable", Version: "v1.0.0-test", Kind: versionsapi.VersionKindImage},
measurementsStatus: http.StatusOK, measurementsStatus: http.StatusOK,
signature: "MEYCIQD1RR91pWPw1BMWXTSmTBHg/JtfKerbZNQ9PJTWDdW0sgIhANQbETJGb67qzQmMVmcq007VUFbHRMtYWKZeeyRf0gVa", signature: "MEUCIHuW2420EqN4Kj6OEaVMmufH7d01vyR1J+SWg8H4elyBAiEA1Ki5Hfq0iI70qpViYbrTFrd8e840NjtdAxGqJKiJgbA=",
signatureStatus: http.StatusNotFound, signatureStatus: http.StatusNotFound,
wantError: true, wantError: true,
}, },
"broken signature": { "broken signature": {
measurements: `{"csp":"test","image":"test","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`, measurements: `{"version":"v1.0.0-test","ref":"-","stream":"stable","list":[{"csp":"Unknown","attestationVariant":"dummy","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}]}`,
metadata: WithMetadata{CSP: cloudprovider.Unknown, Image: "test"}, csp: cloudprovider.Unknown,
imageVersion: versionsapi.Version{Ref: "-", Stream: "stable", Version: "v1.0.0-test", Kind: versionsapi.VersionKindImage},
measurementsStatus: http.StatusOK, measurementsStatus: http.StatusOK,
signature: "AAAAAAA1RR91pWPw1BMWXTSmTBHg/JtfKerbZNQ9PJTWDdW0sgIhANQbETJGb67qzQmMVmcq007VUFbHRMtYWKZeeyRf0gVa", signature: "AAAAAAA1RR91pWPw1BMWXTSmTBHg/JtfKerbZNQ9PJTWDdW0sgIhANQbETJGb67qzQmMVmcq007VUFbHRMtYWKZeeyRf0gVa",
signatureStatus: http.StatusOK, signatureStatus: http.StatusOK,
wantError: true, wantError: true,
}, },
"metadata CSP mismatch": { "metadata CSP mismatch": {
measurements: `{"csp":"test","image":"test","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`, measurements: `{"version":"v1.0.0-test","ref":"-","stream":"stable","list":[{"csp":"Unknown","attestationVariant":"dummy","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}]}`,
metadata: WithMetadata{CSP: cloudprovider.GCP, Image: "test"}, csp: cloudprovider.GCP,
imageVersion: versionsapi.Version{Ref: "-", Stream: "stable", Version: "v1.0.0-test", Kind: versionsapi.VersionKindImage},
measurementsStatus: http.StatusOK, measurementsStatus: http.StatusOK,
signature: "MEYCIQD1RR91pWPw1BMWXTSmTBHg/JtfKerbZNQ9PJTWDdW0sgIhANQbETJGb67qzQmMVmcq007VUFbHRMtYWKZeeyRf0gVa", signature: "MEUCIHuW2420EqN4Kj6OEaVMmufH7d01vyR1J+SWg8H4elyBAiEA1Ki5Hfq0iI70qpViYbrTFrd8e840NjtdAxGqJKiJgbA=",
signatureStatus: http.StatusOK, signatureStatus: http.StatusOK,
wantError: true, wantError: true,
}, },
"metadata image mismatch": { "metadata image mismatch": {
measurements: `{"csp":"test","image":"test","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`, measurements: `{"version":"v1.0.0-test","ref":"-","stream":"stable","list":[{"csp":"Unknown","attestationVariant":"dummy","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}]}`,
metadata: WithMetadata{CSP: cloudprovider.Unknown, Image: "another-image"}, csp: cloudprovider.Unknown,
imageVersion: versionsapi.Version{Ref: "-", Stream: "stable", Version: "v1.0.0-another-image", Kind: versionsapi.VersionKindImage},
measurementsStatus: http.StatusOK, measurementsStatus: http.StatusOK,
signature: "MEYCIQD1RR91pWPw1BMWXTSmTBHg/JtfKerbZNQ9PJTWDdW0sgIhANQbETJGb67qzQmMVmcq007VUFbHRMtYWKZeeyRf0gVa", signature: "MEUCIHuW2420EqN4Kj6OEaVMmufH7d01vyR1J+SWg8H4elyBAiEA1Ki5Hfq0iI70qpViYbrTFrd8e840NjtdAxGqJKiJgbA=",
signatureStatus: http.StatusOK, signatureStatus: http.StatusOK,
wantError: true, wantError: true,
}, },
"not yaml or json": { "not json": {
measurements: "This is some content to be signed!\n", measurements: "This is some content to be signed!\n",
metadata: WithMetadata{CSP: cloudprovider.Unknown, Image: "test"}, csp: cloudprovider.Unknown,
imageVersion: versionsapi.Version{Ref: "-", Stream: "stable", Version: "v1.0.0-test", Kind: versionsapi.VersionKindImage},
measurementsStatus: http.StatusOK, measurementsStatus: http.StatusOK,
signature: "MEUCIQCGA/lSu5qCJgNNvgMaTKJ9rj6vQMecUDaQo3ukaiAfUgIgWoxXRoDKLY9naN7YgxokM7r2fwnyYk3M2WKJJO1g6yo=", signature: "MEUCIQCGA/lSu5qCJgNNvgMaTKJ9rj6vQMecUDaQo3ukaiAfUgIgWoxXRoDKLY9naN7YgxokM7r2fwnyYk3M2WKJJO1g6yo=",
signatureStatus: http.StatusOK, signatureStatus: http.StatusOK,
@ -441,6 +442,10 @@ func TestMeasurementsFetchAndVerify(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
if tc.attestationVariant == nil {
tc.attestationVariant = variant.Dummy{}
}
client := newTestClient(func(req *http.Request) *http.Response { client := newTestClient(func(req *http.Request) *http.Response {
if req.URL.String() == measurementsURL.String() { if req.URL.String() == measurementsURL.String() {
return &http.Response{ return &http.Response{
@ -469,7 +474,9 @@ func TestMeasurementsFetchAndVerify(t *testing.T) {
context.Background(), client, context.Background(), client,
measurementsURL, signatureURL, measurementsURL, signatureURL,
cosignPublicKey, cosignPublicKey,
tc.metadata, tc.imageVersion,
tc.csp,
tc.attestationVariant,
) )
if tc.wantError { if tc.wantError {

View File

@ -185,8 +185,12 @@ const (
// CDNRepositoryURL is the base URL of the Constellation CDN artifact repository. // CDNRepositoryURL is the base URL of the Constellation CDN artifact repository.
CDNRepositoryURL = "https://cdn.confidential.cloud" CDNRepositoryURL = "https://cdn.confidential.cloud"
// CDNAPIPrefix is the prefix of the Constellation API. // CDNAPIBase is the (un-versioned) prefix of the Constellation API.
CDNAPIPrefix = "constellation/v1" CDNAPIBase = "constellation"
// CDNAPIPrefix is the prefix of the Constellation API (V1).
CDNAPIPrefix = CDNAPIBase + "/v1"
// CDNAPIPrefixV2 is the prefix of the Constellation API (v2).
CDNAPIPrefixV2 = CDNAPIBase + "/v2"
// CDNMeasurementsFile is name of file containing image measurements. // CDNMeasurementsFile is name of file containing image measurements.
CDNMeasurementsFile = "measurements.json" CDNMeasurementsFile = "measurements.json"
// CDNMeasurementsSignature is name of file containing signature for CDNMeasurementsFile. // CDNMeasurementsSignature is name of file containing signature for CDNMeasurementsFile.

View File

@ -47,7 +47,7 @@ func New(ctx context.Context, region, bucket string, log *logger.Logger) (*Archi
// Archive reads the OS image in img and uploads it as key. // Archive reads the OS image in img and uploads it as key.
func (a *Archivist) Archive(ctx context.Context, version versionsapi.Version, csp, variant string, img io.Reader) (string, error) { func (a *Archivist) Archive(ctx context.Context, version versionsapi.Version, csp, variant string, img io.Reader) (string, error) {
key, err := url.JoinPath(version.ArtifactPath(), version.Kind.String(), "csp", csp, variant, "image.raw") key, err := url.JoinPath(version.ArtifactPath("v1"), version.Kind.String(), "csp", csp, variant, "image.raw")
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -14,7 +14,6 @@ go_library(
importpath = "github.com/edgelesssys/constellation/v2/internal/versionsapi", importpath = "github.com/edgelesssys/constellation/v2/internal/versionsapi",
visibility = ["//:__subpackages__"], visibility = ["//:__subpackages__"],
deps = [ deps = [
"//internal/cloud/cloudprovider",
"//internal/constants", "//internal/constants",
"@org_golang_x_mod//semver", "@org_golang_x_mod//semver",
], ],

View File

@ -185,8 +185,8 @@ func (c *Client) DeleteVersion(ctx context.Context, ver versionsapi.Version) err
retErr = errors.Join(retErr, fmt.Errorf("updating latest version: %w", err)) retErr = errors.Join(retErr, fmt.Errorf("updating latest version: %w", err))
} }
c.log.Debugf("Deleting artifact path %s for %s", ver.ArtifactPath(), ver.Version) c.log.Debugf("Deleting artifact path %s for %s", ver.ArtifactPath("v1"), ver.Version)
if err := c.deletePath(ctx, ver.ArtifactPath()); err != nil { if err := c.deletePath(ctx, ver.ArtifactPath("v1")); err != nil {
retErr = errors.Join(retErr, fmt.Errorf("deleting artifact path: %w", err)) retErr = errors.Join(retErr, fmt.Errorf("deleting artifact path: %w", err))
} }

View File

@ -15,9 +15,9 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/constants"
"golang.org/x/mod/semver" "golang.org/x/mod/semver"
"github.com/edgelesssys/constellation/v2/internal/constants"
) )
// ReleaseRef is the ref used for release versions. // ReleaseRef is the ref used for release versions.
@ -82,6 +82,14 @@ func (v Version) Validate() error {
return retErr return retErr
} }
// Equal returns true if the versions are equal.
func (v Version) Equal(other Version) bool {
return v.Ref == other.Ref &&
v.Stream == other.Stream &&
v.Version == other.Version &&
v.Kind == other.Kind
}
// Major returns the major version corresponding to the version. // Major returns the major version corresponding to the version.
// For example, if the version is "v1.2.3", the major version is "v1". // For example, if the version is "v1.2.3", the major version is "v1".
func (v Version) Major() string { func (v Version) Major() string {
@ -146,15 +154,16 @@ func (v Version) ListPath(gran Granularity) string {
// ArtifactsURL returns the URL to the artifacts stored for this version. // ArtifactsURL returns the URL to the artifacts stored for this version.
// The URL points to a directory. // The URL points to a directory.
func (v Version) ArtifactsURL() string { func (v Version) ArtifactsURL(apiVersion string) string {
return constants.CDNRepositoryURL + "/" + v.ArtifactPath() return constants.CDNRepositoryURL + "/" + v.ArtifactPath(apiVersion)
} }
// ArtifactPath returns the path to the artifacts stored for this version. // ArtifactPath returns the path to the artifacts stored for this version.
// The path points to a directory. // The path points to a directory.
func (v Version) ArtifactPath() string { func (v Version) ArtifactPath(apiVersion string) string {
return path.Join( return path.Join(
constants.CDNAPIPrefix, constants.CDNAPIBase,
apiVersion,
"ref", v.Ref, "ref", v.Ref,
"stream", v.Stream, "stream", v.Stream,
v.Version, v.Version,
@ -325,17 +334,18 @@ func ValidateStream(ref, stream string) error {
return fmt.Errorf("stream %q is unknown or not supported on ref %q", stream, ref) return fmt.Errorf("stream %q is unknown or not supported on ref %q", stream, ref)
} }
// MeasurementURL builds the measurement and signature URLs for the given version and CSP. // MeasurementURL builds the measurement and signature URLs for the given version.
func MeasurementURL(version Version, csp cloudprovider.Provider) (measurementURL, signatureURL *url.URL, err error) { func MeasurementURL(version Version) (measurementURL, signatureURL *url.URL, err error) {
const apiVersion = "v2"
if version.Kind != VersionKindImage { if version.Kind != VersionKindImage {
return &url.URL{}, &url.URL{}, fmt.Errorf("kind %q is not supported", version.Kind) return &url.URL{}, &url.URL{}, fmt.Errorf("kind %q is not supported", version.Kind)
} }
measurementPath, err := url.JoinPath(version.ArtifactsURL(), "image", "csp", strings.ToLower(csp.String()), constants.CDNMeasurementsFile) measurementPath, err := url.JoinPath(version.ArtifactsURL(apiVersion), "image", constants.CDNMeasurementsFile)
if err != nil { if err != nil {
return &url.URL{}, &url.URL{}, fmt.Errorf("joining path for measurement: %w", err) return &url.URL{}, &url.URL{}, fmt.Errorf("joining path for measurement: %w", err)
} }
signaturePath, err := url.JoinPath(version.ArtifactsURL(), "image", "csp", strings.ToLower(csp.String()), constants.CDNMeasurementsSignature) signaturePath, err := url.JoinPath(version.ArtifactsURL(apiVersion), "image", constants.CDNMeasurementsSignature)
if err != nil { if err != nil {
return &url.URL{}, &url.URL{}, fmt.Errorf("joining path for signature: %w", err) return &url.URL{}, &url.URL{}, fmt.Errorf("joining path for signature: %w", err)
} }

View File

@ -10,9 +10,10 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/stretchr/testify/assert"
) )
func TestNewVersionFromShortPath(t *testing.T) { func TestNewVersionFromShortPath(t *testing.T) {
@ -468,8 +469,8 @@ func TestVersionArtifactURL(t *testing.T) {
Kind: VersionKindImage, Kind: VersionKindImage,
}, },
csp: cloudprovider.GCP, csp: cloudprovider.GCP,
wantMeasurementURL: constants.CDNRepositoryURL + "/" + constants.CDNAPIPrefix + "/ref/feat-some-feature/stream/nightly/v2.6.0-pre.0.20230217095603-193dd48ca19f/image/csp/gcp/measurements.json", wantMeasurementURL: constants.CDNRepositoryURL + "/" + constants.CDNAPIPrefixV2 + "/ref/feat-some-feature/stream/nightly/v2.6.0-pre.0.20230217095603-193dd48ca19f/image/measurements.json",
wantSignatureURL: constants.CDNRepositoryURL + "/" + constants.CDNAPIPrefix + "/ref/feat-some-feature/stream/nightly/v2.6.0-pre.0.20230217095603-193dd48ca19f/image/csp/gcp/measurements.json.sig", wantSignatureURL: constants.CDNRepositoryURL + "/" + constants.CDNAPIPrefixV2 + "/ref/feat-some-feature/stream/nightly/v2.6.0-pre.0.20230217095603-193dd48ca19f/image/measurements.json.sig",
}, },
"fail for wrong kind": { "fail for wrong kind": {
ver: Version{ ver: Version{
@ -483,7 +484,7 @@ func TestVersionArtifactURL(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
measurementURL, signatureURL, err := MeasurementURL(tc.ver, tc.csp) measurementURL, signatureURL, err := MeasurementURL(tc.ver)
if tc.wantErr { if tc.wantErr {
assert.Error(err) assert.Error(err)
return return
@ -560,9 +561,9 @@ func TestVersionArtifactPathURL(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
path := tc.ver.ArtifactPath() path := tc.ver.ArtifactPath("v1")
assert.Equal(tc.wantPath, path) assert.Equal(tc.wantPath, path)
url := tc.ver.ArtifactsURL() url := tc.ver.ArtifactsURL("v1")
assert.Equal(constants.CDNRepositoryURL+"/"+tc.wantPath, url) assert.Equal(constants.CDNRepositoryURL+"/"+tc.wantPath, url)
}) })
} }

View File

@ -182,9 +182,11 @@ The image measurements are a JSON file that contains sets of measurements for th
"ref": "<REF>", "ref": "<REF>",
"stream": "<STREAM>", "stream": "<STREAM>",
"list": [ "list": [
"csp": "<CSP>", {
"attestationVariant": "<ATTESTATION_VARIANT>", "csp": "<CSP>",
"measurements": {"<PCR_INDEX>": {<MEASUREMENT>}} "attestationVariant": "<ATTESTATION_VARIANT>",
"measurements": {"<PCR_INDEX>": {<MEASUREMENT>}}
}
] ]
} }
``` ```