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

@ -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.