diff --git a/internal/attestation/measurements/measurements.go b/internal/attestation/measurements/measurements.go index 2364dc4e5..ef446a9ef 100644 --- a/internal/attestation/measurements/measurements.go +++ b/internal/attestation/measurements/measurements.go @@ -17,10 +17,13 @@ import ( "io" "net/http" "net/url" + "sort" + "strconv" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/sigstore" "github.com/google/go-tpm/tpmutil" + "github.com/talos-systems/talos/pkg/machinery/config/encoder" "go.uber.org/multierr" "gopkg.in/yaml.v3" ) @@ -45,6 +48,20 @@ type WithMetadata struct { Measurements M `json:"measurements" yaml:"measurements"` } +// MarshalYAML returns the YAML encoding of m. +func (m M) MarshalYAML() (any, error) { + // cast to prevent infinite recursion + node, err := encoder.NewEncoder(map[uint32]Measurement(m)).Marshal() + if err != nil { + return nil, err + } + + // sort keys numerically + sort.Sort(mYamlContent(node.Content)) + + return node, nil +} + // FetchAndVerify fetches measurement and signature files via provided URLs, // using client for download. The publicKey is used to verify the measurements. // The hash of the fetched measurements is returned. @@ -338,3 +355,30 @@ type encodedMeasurement struct { Expected string `json:"expected" yaml:"expected"` WarnOnly bool `json:"warnOnly" yaml:"warnOnly"` } + +// mYamlContent is the Content of a yaml.Node encoding of an M. It implements sort.Interface. +// The slice is filled like {key1, value1, key2, value2, ...}. +type mYamlContent []*yaml.Node + +func (c mYamlContent) Len() int { + return len(c) / 2 +} + +func (c mYamlContent) Less(i, j int) bool { + lhs, err := strconv.Atoi(c[2*i].Value) + if err != nil { + panic(err) + } + rhs, err := strconv.Atoi(c[2*j].Value) + if err != nil { + panic(err) + } + return lhs < rhs +} + +func (c mYamlContent) Swap(i, j int) { + // The slice is filled like {key1, value1, key2, value2, ...}. + // We need to swap both key and value. + c[2*i], c[2*j] = c[2*j], c[2*i] + c[2*i+1], c[2*j+1] = c[2*j+1], c[2*i+1] +} diff --git a/internal/attestation/measurements/measurements_test.go b/internal/attestation/measurements/measurements_test.go index 86f815ce0..3ca162348 100644 --- a/internal/attestation/measurements/measurements_test.go +++ b/internal/attestation/measurements/measurements_test.go @@ -18,6 +18,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/talos-systems/talos/pkg/machinery/config/encoder" "gopkg.in/yaml.v3" ) @@ -179,6 +180,59 @@ func TestUnmarshal(t *testing.T) { } } +func TestEncodeM(t *testing.T) { + testCases := map[string]struct { + m M + want string + }{ + "basic": { + m: M{ + 1: WithAllBytes(1, false), + 2: WithAllBytes(2, true), + }, + want: `1: + expected: "0101010101010101010101010101010101010101010101010101010101010101" + warnOnly: false +2: + expected: "0202020202020202020202020202020202020202020202020202020202020202" + warnOnly: true +`, + }, + "output is sorted": { + m: M{ + 3: {}, + 1: {}, + 11: {}, + 2: {}, + }, + want: `1: + expected: "0000000000000000000000000000000000000000000000000000000000000000" + warnOnly: false +2: + expected: "0000000000000000000000000000000000000000000000000000000000000000" + warnOnly: false +3: + expected: "0000000000000000000000000000000000000000000000000000000000000000" + warnOnly: false +11: + expected: "0000000000000000000000000000000000000000000000000000000000000000" + warnOnly: false +`, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + encoded, err := encoder.NewEncoder(tc.m).Encode() + require.NoError(err) + assert.Equal(tc.want, string(encoded)) + }) + } +} + func TestMeasurementsCopyFrom(t *testing.T) { testCases := map[string]struct { current M