Skip to content

Commit 05ebd04

Browse files
committed
Allow multiple expected measurements per register
Signed-off-by: bakhtin <a@bakhtin.net>
1 parent d9fbc83 commit 05ebd04

File tree

8 files changed

+381
-15
lines changed

8 files changed

+381
-15
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,12 +225,21 @@ Payload
225225
"attestation_type": "azure-tdx",
226226
"measurements": {
227227
"11": {
228-
"expected": "efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7"
228+
"expected": ["efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7", "abc123..."]
229229
},
230+
"4": {
231+
"expected": ["ea92ff762767eae6316794f1641c485d4846bc2b9df2eab6ba7f630ce6f4d66f"]
232+
}
230233
}
231234
}
232235
```
233236

237+
The `expected` field accepts either:
238+
- A list of strings (recommended): `"expected": ["value1", "value2"]` - any matching value is accepted (OR semantics)
239+
- A single string (legacy): `"expected": "value"` - for backwards compatibility
240+
241+
Using a list is recommended as it allows specifying multiple valid measurement values for a single key, which is useful when multiple firmware versions or configurations should be accepted.
242+
234243
Note that only the measurements given are expected, and any non-present will be ignored.
235244

236245
To allow _any_ measurement, use an empty measurements field:

adapters/database/service_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func TestAdminFlow(t *testing.T) {
6363

6464
t.Run("AdminFlow", func(t *testing.T) {
6565
t.Run("add measurement", func(t *testing.T) {
66-
err := dbService.AddMeasurement(context.Background(), *domain.NewMeasurement("test-measurement-1", "test-type", map[string]domain.SingleMeasurement{"test": {Expected: "0x1234"}}), false)
66+
err := dbService.AddMeasurement(context.Background(), *domain.NewMeasurement("test-measurement-1", "test-type", map[string]domain.SingleMeasurement{"test": {Expected: []string{"0x1234"}}}), false)
6767
require.NoError(t, err)
6868
})
6969
t.Run("add builder", func(t *testing.T) {

application/service.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,27 @@ func validateMeasurement(measurement map[string]string, measurementTemplate []do
8787
return "", domain.ErrNotFound
8888
}
8989

90-
// validates that all fields from measurementTemplate are the same in measurement
90+
// validates that all fields from measurementTemplate are the same in measurement.
91+
// For each field, the measurement value must match at least one of the expected values (OR semantics).
9192
func checkMeasurement(measurement map[string]string, measurementTemplate domain.Measurement) bool {
9293
for k, v := range measurementTemplate.Measurement {
93-
if val, ok := measurement[k]; !ok || val != v.Expected {
94+
val, ok := measurement[k]
95+
if !ok {
96+
return false
97+
}
98+
if !matchesAnyExpected(val, v.Expected) {
9499
return false
95100
}
96101
}
97102
return true
98103
}
104+
105+
// matchesAnyExpected returns true if the value matches any of the expected values.
106+
func matchesAnyExpected(value string, expected []string) bool {
107+
for _, exp := range expected {
108+
if value == exp {
109+
return true
110+
}
111+
}
112+
return false
113+
}

application/service_test.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package application
2+
3+
import (
4+
"testing"
5+
6+
"github.com/flashbots/builder-hub/domain"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestCheckMeasurement_SingleExpectedValue(t *testing.T) {
11+
template := domain.Measurement{
12+
Name: "test",
13+
AttestationType: "azure-tdx",
14+
Measurement: map[string]domain.SingleMeasurement{
15+
"8": {Expected: []string{"0000"}},
16+
"11": {Expected: []string{"aaaa"}},
17+
},
18+
}
19+
20+
t.Run("exact match", func(t *testing.T) {
21+
measurement := map[string]string{
22+
"8": "0000",
23+
"11": "aaaa",
24+
}
25+
require.True(t, checkMeasurement(measurement, template))
26+
})
27+
28+
t.Run("wrong value", func(t *testing.T) {
29+
measurement := map[string]string{
30+
"8": "0000",
31+
"11": "bbbb",
32+
}
33+
require.False(t, checkMeasurement(measurement, template))
34+
})
35+
36+
t.Run("missing key", func(t *testing.T) {
37+
measurement := map[string]string{
38+
"8": "0000",
39+
}
40+
require.False(t, checkMeasurement(measurement, template))
41+
})
42+
43+
t.Run("extra key in measurement is allowed", func(t *testing.T) {
44+
measurement := map[string]string{
45+
"8": "0000",
46+
"11": "aaaa",
47+
"99": "extra",
48+
}
49+
require.True(t, checkMeasurement(measurement, template))
50+
})
51+
}
52+
53+
func TestCheckMeasurement_MultipleExpectedValues(t *testing.T) {
54+
template := domain.Measurement{
55+
Name: "test",
56+
AttestationType: "azure-tdx",
57+
Measurement: map[string]domain.SingleMeasurement{
58+
"8": {Expected: []string{"0000", "1111", "2222"}},
59+
"11": {Expected: []string{"aaaa", "bbbb"}},
60+
},
61+
}
62+
63+
t.Run("first expected value matches", func(t *testing.T) {
64+
measurement := map[string]string{
65+
"8": "0000",
66+
"11": "aaaa",
67+
}
68+
require.True(t, checkMeasurement(measurement, template))
69+
})
70+
71+
t.Run("second expected value matches", func(t *testing.T) {
72+
measurement := map[string]string{
73+
"8": "1111",
74+
"11": "bbbb",
75+
}
76+
require.True(t, checkMeasurement(measurement, template))
77+
})
78+
79+
t.Run("third expected value for key 8", func(t *testing.T) {
80+
measurement := map[string]string{
81+
"8": "2222",
82+
"11": "aaaa",
83+
}
84+
require.True(t, checkMeasurement(measurement, template))
85+
})
86+
87+
t.Run("value not in expected list", func(t *testing.T) {
88+
measurement := map[string]string{
89+
"8": "3333",
90+
"11": "aaaa",
91+
}
92+
require.False(t, checkMeasurement(measurement, template))
93+
})
94+
95+
t.Run("one key matches, other does not", func(t *testing.T) {
96+
measurement := map[string]string{
97+
"8": "0000",
98+
"11": "cccc",
99+
}
100+
require.False(t, checkMeasurement(measurement, template))
101+
})
102+
}
103+
104+
func TestValidateMeasurement(t *testing.T) {
105+
templates := []domain.Measurement{
106+
{
107+
Name: "template-1",
108+
AttestationType: "azure-tdx",
109+
Measurement: map[string]domain.SingleMeasurement{
110+
"8": {Expected: []string{"0000"}},
111+
"11": {Expected: []string{"aaaa", "bbbb"}},
112+
},
113+
},
114+
{
115+
Name: "template-2",
116+
AttestationType: "azure-tdx",
117+
Measurement: map[string]domain.SingleMeasurement{
118+
"8": {Expected: []string{"1111"}},
119+
"11": {Expected: []string{"cccc"}},
120+
},
121+
},
122+
}
123+
124+
t.Run("matches first template", func(t *testing.T) {
125+
measurement := map[string]string{
126+
"8": "0000",
127+
"11": "aaaa",
128+
}
129+
name, err := validateMeasurement(measurement, templates)
130+
require.NoError(t, err)
131+
require.Equal(t, "template-1", name)
132+
})
133+
134+
t.Run("matches first template with second expected value", func(t *testing.T) {
135+
measurement := map[string]string{
136+
"8": "0000",
137+
"11": "bbbb",
138+
}
139+
name, err := validateMeasurement(measurement, templates)
140+
require.NoError(t, err)
141+
require.Equal(t, "template-1", name)
142+
})
143+
144+
t.Run("matches second template", func(t *testing.T) {
145+
measurement := map[string]string{
146+
"8": "1111",
147+
"11": "cccc",
148+
}
149+
name, err := validateMeasurement(measurement, templates)
150+
require.NoError(t, err)
151+
require.Equal(t, "template-2", name)
152+
})
153+
154+
t.Run("no match returns error", func(t *testing.T) {
155+
measurement := map[string]string{
156+
"8": "9999",
157+
"11": "zzzz",
158+
}
159+
_, err := validateMeasurement(measurement, templates)
160+
require.ErrorIs(t, err, domain.ErrNotFound)
161+
})
162+
}
163+
164+
func TestMatchesAnyExpected(t *testing.T) {
165+
t.Run("empty list returns false", func(t *testing.T) {
166+
require.False(t, matchesAnyExpected("value", []string{}))
167+
})
168+
169+
t.Run("single value match", func(t *testing.T) {
170+
require.True(t, matchesAnyExpected("value", []string{"value"}))
171+
})
172+
173+
t.Run("single value no match", func(t *testing.T) {
174+
require.False(t, matchesAnyExpected("value", []string{"other"}))
175+
})
176+
177+
t.Run("multiple values first match", func(t *testing.T) {
178+
require.True(t, matchesAnyExpected("a", []string{"a", "b", "c"}))
179+
})
180+
181+
t.Run("multiple values middle match", func(t *testing.T) {
182+
require.True(t, matchesAnyExpected("b", []string{"a", "b", "c"}))
183+
})
184+
185+
t.Run("multiple values last match", func(t *testing.T) {
186+
require.True(t, matchesAnyExpected("c", []string{"a", "b", "c"}))
187+
})
188+
189+
t.Run("multiple values no match", func(t *testing.T) {
190+
require.False(t, matchesAnyExpected("d", []string{"a", "b", "c"}))
191+
})
192+
}

domain/types.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package domain
33

44
import (
5+
"encoding/json"
56
"errors"
67
"net"
78

@@ -24,8 +25,52 @@ type Measurement struct {
2425
Measurement map[string]SingleMeasurement
2526
}
2627

28+
// SingleMeasurement represents a single measurement with one or more expected values.
29+
// When multiple values are provided, any matching value is accepted (OR semantics).
2730
type SingleMeasurement struct {
28-
Expected string `json:"expected"`
31+
Expected []string
32+
}
33+
34+
// UnmarshalJSON implements custom unmarshaling to support both:
35+
// - {"expected": "value"} (backwards compatible single string)
36+
// - {"expected": ["value1", "value2"]} (new list format)
37+
func (s *SingleMeasurement) UnmarshalJSON(data []byte) error {
38+
// Try to unmarshal as object with expected field
39+
var raw struct {
40+
Expected json.RawMessage `json:"expected"`
41+
}
42+
if err := json.Unmarshal(data, &raw); err != nil {
43+
return err
44+
}
45+
46+
// Try to unmarshal expected as a string first (backwards compatibility)
47+
var singleValue string
48+
if err := json.Unmarshal(raw.Expected, &singleValue); err == nil {
49+
s.Expected = []string{singleValue}
50+
return nil
51+
}
52+
53+
// Try to unmarshal expected as an array of strings
54+
var multiValue []string
55+
if err := json.Unmarshal(raw.Expected, &multiValue); err != nil {
56+
return err
57+
}
58+
s.Expected = multiValue
59+
return nil
60+
}
61+
62+
// MarshalJSON implements custom marshaling to output:
63+
// - {"expected": "value"} when there's exactly one value (backwards compatible)
64+
// - {"expected": ["value1", "value2"]} when there are multiple values
65+
func (s SingleMeasurement) MarshalJSON() ([]byte, error) {
66+
if len(s.Expected) == 1 {
67+
return json.Marshal(struct {
68+
Expected string `json:"expected"`
69+
}{Expected: s.Expected[0]})
70+
}
71+
return json.Marshal(struct {
72+
Expected []string `json:"expected"`
73+
}{Expected: s.Expected})
2974
}
3075

3176
func NewMeasurement(name, attestationType string, measurements map[string]SingleMeasurement) *Measurement {

0 commit comments

Comments
 (0)