mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-09-27 12:01:04 -04:00
cli: image measurements (v2)
This commit is contained in:
parent
2ebc0cf2c8
commit
e5b394db87
18 changed files with 274 additions and 195 deletions
|
@ -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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue