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")
imageVersion, err := versionsapi.NewVersionFromShortPath(conf.Image, versionsapi.VersionKindImage)
if err != nil {
return err
}
var fetchedMeasurements measurements.M
hash, err := fetchedMeasurements.FetchAndVerify(
ctx, client,
flags.measurementsURL,
flags.signatureURL,
cosignPublicKey,
measurements.WithMetadata{
CSP: conf.GetProvider(),
Image: conf.Image,
},
imageVersion,
conf.GetProvider(),
conf.GetAttestationConfig().GetVariant(),
)
if err != nil {
return err
@ -182,7 +185,7 @@ func (f *fetchMeasurementsFlags) updateURLs(conf *config.Config) error {
if err != nil {
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 {
return err
}

View File

@ -119,8 +119,8 @@ func TestUpdateURLs(t *testing.T) {
},
},
flags: &fetchMeasurementsFlags{},
wantMeasurementsURL: ver.ArtifactsURL() + "/image/csp/gcp/measurements.json",
wantMeasurementsSigURL: ver.ArtifactsURL() + "/image/csp/gcp/measurements.json.sig",
wantMeasurementsURL: ver.ArtifactsURL("v2") + "/image/measurements.json",
wantMeasurementsSigURL: ver.ArtifactsURL("v2") + "/image/measurements.json.sig",
},
"both set by user": {
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-----")
measurements := `{
"csp": "gcp",
"image": "v999.999.999",
"measurements": {
"0": "0000000000000000000000000000000000000000000000000000000000000000",
"1": "1111111111111111111111111111111111111111111111111111111111111111",
"2": "2222222222222222222222222222222222222222222222222222222222222222",
"3": "3333333333333333333333333333333333333333333333333333333333333333",
"4": "4444444444444444444444444444444444444444444444444444444444444444",
"5": "5555555555555555555555555555555555555555555555555555555555555555",
"6": "6666666666666666666666666666666666666666666666666666666666666666"
}
"version": "v999.999.999",
"ref": "-",
"stream": "stable",
"list": [
{
"csp": "GCP",
"attestationVariant":"gcp-sev-es",
"measurements": {
"0": {
"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 {
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{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(measurements)),
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{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(signature)),

View File

@ -26,6 +26,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl"
conSemver "github.com/edgelesssys/constellation/v2/internal/semver"
"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/versionsapi"
"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)
// get current image version of the cluster
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())
if err != nil {
@ -167,7 +169,7 @@ func (u *upgradeCheckCmd) upgradeCheck(cmd *cobra.Command, fileHandler file.Hand
semver.Sort(newKubernetes)
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 {
return err
}
@ -240,7 +242,7 @@ type collector interface {
currentVersions(ctx context.Context) (currentVersionInfo, error)
supportedVersions(ctx context.Context, version, currentK8sVersion string) (supportedVersionInfo, 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)
newCLIVersions(ctx context.Context) ([]string, error)
filterCompatibleCLIVersions(ctx context.Context, cliPatchVersions []string, currentK8sVersion string) ([]string, error)
@ -259,9 +261,9 @@ type versionCollector struct {
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
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 {
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.
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) {
upgrades := make(map[string]measurements.M)
for _, version := range versions {
log.Debugf("Fetching measurements for image: %s", version)
shortPath := version.ShortPath()
measurementsURL, signatureURL, err := versionsapi.MeasurementURL(version, csp)
measurementsURL, signatureURL, err := versionsapi.MeasurementURL(version)
if err != nil {
return nil, err
}
@ -542,10 +544,9 @@ func getCompatibleImageMeasurements(ctx context.Context, writer io.Writer, clien
measurementsURL,
signatureURL,
pubK,
measurements.WithMetadata{
CSP: csp,
Image: shortPath,
},
version,
csp,
attestationVariant,
)
if 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)
csp := cloudprovider.Azure
attestationVariant := variant.AzureSEVSNP{}
zero := versionsapi.Version{
Ref: "-",
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-----")
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)
for _, measurement := range upgrades {
@ -297,7 +298,7 @@ type stubVersionCollector struct {
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
}

View File

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

View File

@ -10,17 +10,12 @@ package upgrade
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"gopkg.in/yaml.v3"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"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/fetcher"
)
@ -31,7 +26,9 @@ type upgradeInfo struct {
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{
measurements: make(measurements.M),
shortPath: toImage,
@ -43,20 +40,17 @@ func fetchUpgradeInfo(ctx context.Context, csp cloudprovider.Provider, toImage s
return upgradeInfo{}, err
}
measurementsURL, _, err := versionsapi.MeasurementURL(ver, csp)
measurementsURL, _, err := versionsapi.MeasurementURL(ver)
if err != nil {
return upgradeInfo{}, err
}
fetchedMeasurements, err := fetchMeasurements(
fetchedMeasurements := measurements.M{}
if err := fetchedMeasurements.FetchNoVerify(
ctx, http.DefaultClient,
measurementsURL,
measurements.WithMetadata{
CSP: csp,
Image: toImage,
},
)
if err != nil {
ver, csp, attestationVariant,
); err != nil {
return upgradeInfo{}, err
}
info.measurements = fetchedMeasurements
@ -74,56 +68,6 @@ func fetchUpgradeInfo(ctx context.Context, csp cloudprovider.Provider, toImage s
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) {
imageInfo, err := client.FetchImageInfo(ctx, imageInfo)
if err != nil {

View File

@ -240,7 +240,12 @@ func writeUpgradeConfig(require *require.Assertions, image string, kubernetes st
}
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)
log.Printf("Setting image version: %s\n", info.shortPath)

View File

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

View File

@ -82,7 +82,7 @@ func main() {
log.Println("Found", variant)
returnStmtCtr++
// 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
clause.Values[0] = measurementsCompositeLiteral(measuremnts)
}
@ -107,12 +107,17 @@ func main() {
}
// 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 {
measurementsURL, err := measurementURL(provider, image, "measurements.json")
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(image, "measurements.json")
if err != nil {
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 {
panic(err)
}
@ -124,10 +129,9 @@ func mustGetMeasurements(ctx context.Context, verifier rekorVerifier, cosignPubl
measurementsURL,
signatureURL,
cosignPublicKey,
measurements.WithMetadata{
CSP: provider,
Image: image,
},
imageVersion,
provider,
attestationVariant,
)
if err != nil {
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.
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)
if err != nil {
return nil, fmt.Errorf("parsing image name: %w", err)
}
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"
"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/siderolabs/talos/pkg/machinery/config/encoder"
"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 (
@ -67,6 +69,22 @@ type WithMetadata struct {
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.
func (m M) MarshalYAML() (any, error) {
// cast to prevent infinite recursion
@ -86,9 +104,9 @@ func (m M) MarshalYAML() (any, error) {
// The hash of the fetched measurements is returned.
func (m *M) FetchAndVerify(
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) {
measurements, err := getFromURL(ctx, client, measurementsURL)
measurementsRaw, err := getFromURL(ctx, client, measurementsURL)
if err != nil {
return "", fmt.Errorf("failed to fetch measurements: %w", err)
}
@ -96,34 +114,38 @@ func (m *M) FetchAndVerify(
if err != nil {
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
}
var mWithMetadata WithMetadata
if err := json.Unmarshal(measurements, &mWithMetadata); err != nil {
if yamlErr := yaml.Unmarshal(measurements, &mWithMetadata); yamlErr != nil {
return "", errors.Join(
err,
fmt.Errorf("trying yaml format: %w", yamlErr),
)
}
var measurements ImageMeasurementsV2
if err := json.Unmarshal(measurementsRaw, &measurements); err != nil {
return "", err
}
if mWithMetadata.CSP != metadata.CSP {
return "", fmt.Errorf("invalid measurement metadata: CSP mismatch: expected %s, got %s", metadata.CSP, mWithMetadata.CSP)
if err := m.fromImageMeasurementsV2(measurements, version, csp, attestationVariant); err != nil {
return "", err
}
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)
shaHash := sha256.Sum256(measurementsRaw)
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,
// but keeping not specified values untouched.
func (m *M) CopyFrom(other M) {
@ -232,6 +254,51 @@ func (m *M) UnmarshalYAML(unmarshal func(any) error) error {
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.
type Measurement struct {
// Expected measurement value.

View File

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

View File

@ -185,8 +185,12 @@ const (
// CDNRepositoryURL is the base URL of the Constellation CDN artifact repository.
CDNRepositoryURL = "https://cdn.confidential.cloud"
// CDNAPIPrefix is the prefix of the Constellation API.
CDNAPIPrefix = "constellation/v1"
// CDNAPIBase is the (un-versioned) prefix of the Constellation API.
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 = "measurements.json"
// 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.
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 {
return "", err
}

View File

@ -14,7 +14,6 @@ go_library(
importpath = "github.com/edgelesssys/constellation/v2/internal/versionsapi",
visibility = ["//:__subpackages__"],
deps = [
"//internal/cloud/cloudprovider",
"//internal/constants",
"@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))
}
c.log.Debugf("Deleting artifact path %s for %s", ver.ArtifactPath(), ver.Version)
if err := c.deletePath(ctx, ver.ArtifactPath()); err != nil {
c.log.Debugf("Deleting artifact path %s for %s", ver.ArtifactPath("v1"), ver.Version)
if err := c.deletePath(ctx, ver.ArtifactPath("v1")); err != nil {
retErr = errors.Join(retErr, fmt.Errorf("deleting artifact path: %w", err))
}

View File

@ -15,9 +15,9 @@ import (
"regexp"
"strings"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/constants"
"golang.org/x/mod/semver"
"github.com/edgelesssys/constellation/v2/internal/constants"
)
// ReleaseRef is the ref used for release versions.
@ -82,6 +82,14 @@ func (v Version) Validate() error {
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.
// For example, if the version is "v1.2.3", the major version is "v1".
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.
// The URL points to a directory.
func (v Version) ArtifactsURL() string {
return constants.CDNRepositoryURL + "/" + v.ArtifactPath()
func (v Version) ArtifactsURL(apiVersion string) string {
return constants.CDNRepositoryURL + "/" + v.ArtifactPath(apiVersion)
}
// ArtifactPath returns the path to the artifacts stored for this version.
// The path points to a directory.
func (v Version) ArtifactPath() string {
func (v Version) ArtifactPath(apiVersion string) string {
return path.Join(
constants.CDNAPIPrefix,
constants.CDNAPIBase,
apiVersion,
"ref", v.Ref,
"stream", v.Stream,
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)
}
// MeasurementURL builds the measurement and signature URLs for the given version and CSP.
func MeasurementURL(version Version, csp cloudprovider.Provider) (measurementURL, signatureURL *url.URL, err error) {
// MeasurementURL builds the measurement and signature URLs for the given version.
func MeasurementURL(version Version) (measurementURL, signatureURL *url.URL, err error) {
const apiVersion = "v2"
if version.Kind != VersionKindImage {
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 {
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 {
return &url.URL{}, &url.URL{}, fmt.Errorf("joining path for signature: %w", err)
}

View File

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