Skip to content

Commit 4bc1e67

Browse files
authored
Update measurements.json to v2 format, and add a helper to load and compare (#26)
* CLI tool to print verified measurements from an aTLS server * use HTTP GET request, and save to file * cleanup * rename to attested-get * feat(get-measurements): compare against known measurements * docs * backwards-compatible loading of v1 measurements * cleanup * support legacy format * package name: multimeasurements * cleanup
1 parent 816dcde commit 4bc1e67

File tree

6 files changed

+280
-52
lines changed

6 files changed

+280
-52
lines changed

cmd/attested-get/main.go

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ package main
1414
//
1515
// go run cmd/attested-get/main.go --addr=https://instance_ip:port --out-measurements=measurements.json --out-response=response.txt
1616
//
17+
// You can also compare the resulting measurements with a list of expected measurements:
18+
//
19+
// go run cmd/get-measurements/main.go --addr=https://instance_ip:port --expected-measurements=measurements.json
20+
//
1721

1822
import (
1923
"encoding/asn1"
20-
"encoding/hex"
2124
"encoding/json"
2225
"errors"
2326
"fmt"
@@ -33,6 +36,7 @@ import (
3336
"github.com/flashbots/cvm-reverse-proxy/internal/attestation/measurements"
3437
"github.com/flashbots/cvm-reverse-proxy/internal/attestation/variant"
3538
"github.com/flashbots/cvm-reverse-proxy/internal/config"
39+
"github.com/flashbots/cvm-reverse-proxy/multimeasurements"
3640
"github.com/flashbots/cvm-reverse-proxy/proxy"
3741
"github.com/urfave/cli/v2" // imports as package "cli"
3842
)
@@ -58,6 +62,11 @@ var flags []cli.Flag = []cli.Flag{
5862
Value: string(proxy.AttestationAzureTDX),
5963
Usage: "type of attestation to present (currently only azure-tdx)",
6064
},
65+
&cli.StringFlag{
66+
Name: "expected-measurements",
67+
Value: "",
68+
Usage: "File or URL with known measurements (to compare against)",
69+
},
6170
&cli.BoolFlag{
6271
Name: "log-debug",
6372
Value: false,
@@ -84,6 +93,7 @@ func runClient(cCtx *cli.Context) (err error) {
8493
outMeasurements := cCtx.String("out-measurements")
8594
outResponse := cCtx.String("out-response")
8695
attestationTypeStr := cCtx.String("attestation-type")
96+
expectedMeasurementsPath := cCtx.String("expected-measurements")
8797

8898
// Setup logging
8999
log := common.SetupLogger(&common.LoggingOpts{
@@ -93,6 +103,7 @@ func runClient(cCtx *cli.Context) (err error) {
93103
Version: common.Version,
94104
})
95105

106+
// Sanity-check addr
96107
if !strings.HasPrefix(addr, "https://") {
97108
return errors.New("address needs to start with https://")
98109
}
@@ -117,6 +128,16 @@ func runClient(cCtx *cli.Context) (err error) {
117128
return errors.New("currently only azure-tdx attestation is supported")
118129
}
119130

131+
// Load expected measurements from file or URL (if provided)
132+
var expectedMeasurements *multimeasurements.MultiMeasurements
133+
if expectedMeasurementsPath != "" {
134+
log.Info("Loading expected measurements from " + expectedMeasurementsPath + " ...")
135+
expectedMeasurements, err = multimeasurements.New(expectedMeasurementsPath)
136+
if err != nil {
137+
return err
138+
}
139+
}
140+
120141
// Prepare aTLS stuff
121142
issuer, err := proxy.CreateAttestationIssuer(log, proxy.AttestationAzureTDX)
122143
if err != nil {
@@ -150,24 +171,34 @@ func runClient(cCtx *cli.Context) (err error) {
150171
return err
151172
}
152173

153-
measurementsInHeaderFormat := make(map[uint32]string, len(extractedMeasurements))
154-
for pcr, value := range extractedMeasurements {
155-
measurementsInHeaderFormat[pcr] = hex.EncodeToString(value)
174+
printableMeasurements := make(map[uint32]string)
175+
for k, v := range extractedMeasurements {
176+
printableMeasurements[k] = fmt.Sprintf("%x", v)
156177
}
157178

158-
marshaledPcrs, err := json.MarshalIndent(measurementsInHeaderFormat, "", " ")
179+
marshaledPcrs, err := json.MarshalIndent(printableMeasurements, "", " ")
159180
if err != nil {
160181
return errors.New("could not marshal measurement extracted from tls extension")
161182
}
162183

163-
log.Info(fmt.Sprintf("Measurements for %s with %d entries:", atlsVariant.String(), len(measurementsInHeaderFormat)))
184+
log.Info(fmt.Sprintf("Measurements for %s with %d entries:", atlsVariant.String(), len(extractedMeasurements)))
164185
fmt.Println(string(marshaledPcrs))
165186
if outMeasurements != "" {
166187
if err := os.WriteFile(outMeasurements, marshaledPcrs, 0o644); err != nil {
167188
return err
168189
}
169190
}
170191

192+
// Compare against expected measurements
193+
if expectedMeasurements != nil {
194+
found, foundMeasurement := expectedMeasurements.Contains(extractedMeasurements)
195+
if found {
196+
log.With("matchedMeasurements", foundMeasurement.MeasurementID).Info("Measurements match expected measurements ✅")
197+
} else {
198+
log.Error("Measurements do not match expected measurements! ❌")
199+
}
200+
}
201+
171202
// Print the response body
172203
msg, err := io.ReadAll(resp.Body)
173204
if err != nil {

measurements.json

Lines changed: 50 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,50 @@
1-
{
2-
"azure-tdx-example": {
3-
"11": {
4-
"expected": "efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7"
5-
},
6-
"12": {
7-
"expected": "0000000000000000000000000000000000000000000000000000000000000000"
8-
},
9-
"13": {
10-
"expected": "0000000000000000000000000000000000000000000000000000000000000000"
11-
},
12-
"15": {
13-
"expected": "0000000000000000000000000000000000000000000000000000000000000000"
14-
},
15-
"4": {
16-
"expected": "ea92ff762767eae6316794f1641c485d4846bc2b9df2eab6ba7f630ce6f4d66f"
17-
},
18-
"8": {
19-
"expected": "0000000000000000000000000000000000000000000000000000000000000000"
20-
},
21-
"9": {
22-
"expected": "c9f429296634072d1063a03fb287bed0b2d177b0a504755ad9194cffd90b2489"
23-
}
24-
},
25-
"dcap-tdx-example": {
26-
"0": {
27-
"expected": "5d56080eb9ef8ce0bbaf6bdcdadeeb06e7c5b0a4d1ec16be868a85a953babe0c5e54d01c8e050a54fe1ca078372530d2"
28-
},
29-
"1": {
30-
"expected": "4216e925f796f4e282cfa6e72d4c77a80560987afa29155a61fdc33adb80eab0d4112abd52387e5e25a60deefb8a5287"
31-
},
32-
"2": {
33-
"expected": "4274fefb79092c164000b571b64ecb432fa2357adb421fd1c77a867168d7d7f7fe82796d1eba092c7bab35cf43f5ec55"
34-
},
35-
"3": {
36-
"expected": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
37-
},
38-
"4": {
39-
"expected": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
40-
}
41-
}
42-
}
1+
[
2+
{
3+
"measurement_id": "azure-tdx-example-01",
4+
"attestation_type": "azure-tdx",
5+
"measurements": {
6+
"4": {
7+
"expected": "ea92ff762767eae6316794f1641c485d4846bc2b9df2eab6ba7f630ce6f4d66f"
8+
},
9+
"9": {
10+
"expected": "c9f429296634072d1063a03fb287bed0b2d177b0a504755ad9194cffd90b2489"
11+
},
12+
"11": {
13+
"expected": "efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7"
14+
}
15+
}
16+
},
17+
{
18+
"measurement_id": "cvm-image-azure-tdx.rootfs-20241107200854.wic.vhd",
19+
"attestation_type": "azure-tdx",
20+
"measurements": {
21+
"4": {
22+
"expected": "1b8cd655f5ebdf50bedabfb5db6b896a0a7c56de54f318103a2de1e7cea57b6b"
23+
},
24+
"9": {
25+
"expected": "992465f922102234c196f596fdaba86ea16eaa4c264dc425ec26bc2d1c364472"
26+
}
27+
}
28+
},
29+
{
30+
"measurement_id": "dcap-tdx-example-02",
31+
"attestation_type": "dcap-tdx",
32+
"measurements": {
33+
"0": {
34+
"expected": "5d56080eb9ef8ce0bbaf6bdcdadeeb06e7c5b0a4d1ec16be868a85a953babe0c5e54d01c8e050a54fe1ca078372530d2"
35+
},
36+
"1": {
37+
"expected": "4216e925f796f4e282cfa6e72d4c77a80560987afa29155a61fdc33adb80eab0d4112abd52387e5e25a60deefb8a5287"
38+
},
39+
"2": {
40+
"expected": "4274fefb79092c164000b571b64ecb432fa2357adb421fd1c77a867168d7d7f7fe82796d1eba092c7bab35cf43f5ec55"
41+
},
42+
"3": {
43+
"expected": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
44+
},
45+
"4": {
46+
"expected": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
47+
}
48+
}
49+
}
50+
]
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Package multimeasurements contains a helper to load a file with multiple measurements
2+
// and compare provided measurements against them.
3+
//
4+
// Compatible with measurements data schema v2 (see measurements.json) as well as the
5+
// legacy v1 schema.
6+
package multimeasurements
7+
8+
import (
9+
"bytes"
10+
"encoding/json"
11+
"io"
12+
"net/http"
13+
"os"
14+
"strings"
15+
16+
"github.com/flashbots/cvm-reverse-proxy/internal/attestation/measurements"
17+
)
18+
19+
// MultiMeasurements holds several known measurements, and can check if
20+
// given measurements match known ones.
21+
type MultiMeasurements struct {
22+
Measurements []MeasurementsContainer
23+
}
24+
25+
type MeasurementsContainer struct {
26+
MeasurementID string `json:"measurement_id"`
27+
AttestationType string `json:"attestation_type"`
28+
Measurements measurements.M `json:"measurements"`
29+
}
30+
31+
type LegacyMultiMeasurements map[string]measurements.M
32+
33+
// New returns a MultiMeasurements instance, with the measurements
34+
// loaded from a file or URL.
35+
func New(path string) (m *MultiMeasurements, err error) {
36+
var data []byte
37+
if strings.HasPrefix(path, "http") {
38+
// load from URL
39+
resp, err := http.Get(path)
40+
if err != nil {
41+
return nil, err
42+
}
43+
defer resp.Body.Close()
44+
data, err = io.ReadAll(resp.Body)
45+
if err != nil {
46+
return nil, err
47+
}
48+
} else {
49+
// load from file
50+
data, err = os.ReadFile(path)
51+
if err != nil {
52+
return nil, err
53+
}
54+
}
55+
56+
m = &MultiMeasurements{}
57+
58+
// Try to load the v2 data schema, if that fails fall back to legacy v1 schema
59+
if err = json.Unmarshal(data, &m.Measurements); err != nil {
60+
var legacyData LegacyMultiMeasurements
61+
err = json.Unmarshal(data, &legacyData)
62+
for measurementID, measurements := range legacyData {
63+
container := MeasurementsContainer{
64+
MeasurementID: measurementID,
65+
AttestationType: "azure-tdx",
66+
Measurements: measurements,
67+
}
68+
m.Measurements = append(m.Measurements, container)
69+
}
70+
}
71+
72+
return m, err
73+
}
74+
75+
// Contains checks if the provided measurements match one of the known measurements. Any keys in the provided
76+
// measurements which are not in the known measurements are ignored.
77+
func (m *MultiMeasurements) Contains(measurements map[uint32][]byte) (found bool, foundMeasurement *MeasurementsContainer) {
78+
// For every known container, all known measurements match (and additional ones are ignored)
79+
for _, container := range m.Measurements {
80+
allMatch := true
81+
for key, value := range container.Measurements {
82+
if !bytes.Equal(value.Expected, measurements[key]) {
83+
allMatch = false
84+
break
85+
}
86+
}
87+
88+
if allMatch {
89+
return true, &container
90+
}
91+
}
92+
93+
return false, nil
94+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package multimeasurements
2+
3+
import (
4+
"encoding/hex"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// TestMeasurements is kept simple: map[pcr]measurement
13+
type TestMeasurements map[uint32][]byte
14+
15+
func mustBytesFromHex(hexValue string) []byte {
16+
bytes, err := hex.DecodeString(hexValue)
17+
if err != nil {
18+
panic(err)
19+
}
20+
return bytes
21+
}
22+
23+
// Measurements V1 (legacy) JSON (from https://github.com/flashbots/cvm-reverse-proxy/blob/837588b9f87ee49d1bb6dca4712a1c2844eb1ecc/measurements.json)
24+
var measurementsV1JSON = []byte(`{"azure-tdx-example":{"11":{"expected":"efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7"},"12":{"expected":"0000000000000000000000000000000000000000000000000000000000000000"},"13":{"expected":"0000000000000000000000000000000000000000000000000000000000000000"},"15":{"expected":"0000000000000000000000000000000000000000000000000000000000000000"},"4":{"expected":"ea92ff762767eae6316794f1641c485d4846bc2b9df2eab6ba7f630ce6f4d66f"},"8":{"expected":"0000000000000000000000000000000000000000000000000000000000000000"},"9":{"expected":"c9f429296634072d1063a03fb287bed0b2d177b0a504755ad9194cffd90b2489"}},"dcap-tdx-example":{"0":{"expected":"5d56080eb9ef8ce0bbaf6bdcdadeeb06e7c5b0a4d1ec16be868a85a953babe0c5e54d01c8e050a54fe1ca078372530d2"},"1":{"expected":"4216e925f796f4e282cfa6e72d4c77a80560987afa29155a61fdc33adb80eab0d4112abd52387e5e25a60deefb8a5287"},"2":{"expected":"4274fefb79092c164000b571b64ecb432fa2357adb421fd1c77a867168d7d7f7fe82796d1eba092c7bab35cf43f5ec55"},"3":{"expected":"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"4":{"expected":"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}}}`)
25+
26+
// TestMultiMeasurementsV2 tests the v2 data schema
27+
func TestMultiMeasurementsV2(t *testing.T) {
28+
// Load expected measurements from JSON file (in V2 format)
29+
m, err := New("../measurements.json")
30+
require.NoError(t, err)
31+
require.Len(t, m.Measurements, 3)
32+
33+
// Setup test measurements (matching cvm-image-azure-tdx.rootfs-20241107200854.wic.vhd)
34+
testMeasurements := TestMeasurements{
35+
4: mustBytesFromHex("1b8cd655f5ebdf50bedabfb5db6b896a0a7c56de54f318103a2de1e7cea57b6b"),
36+
9: mustBytesFromHex("992465f922102234c196f596fdaba86ea16eaa4c264dc425ec26bc2d1c364472"),
37+
}
38+
39+
// Ensure matching entries works, and that additional fields are ignored
40+
testMeasurements[11] = testMeasurements[4]
41+
exists, foundMeasurement := m.Contains(testMeasurements)
42+
require.True(t, exists)
43+
require.Equal(t, "cvm-image-azure-tdx.rootfs-20241107200854.wic.vhd", foundMeasurement.MeasurementID)
44+
require.Equal(t, "azure-tdx", foundMeasurement.AttestationType)
45+
46+
// Ensure check fails with a missing required key
47+
delete(testMeasurements, 4)
48+
exists, _ = m.Contains(testMeasurements)
49+
require.False(t, exists)
50+
51+
// Double-check it works again
52+
testMeasurements[4] = testMeasurements[11]
53+
exists, _ = m.Contains(testMeasurements)
54+
require.True(t, exists)
55+
56+
// Any changed value should make it fail
57+
testMeasurements[4] = testMeasurements[9]
58+
exists, _ = m.Contains(testMeasurements)
59+
require.False(t, exists)
60+
61+
// Check for another set of known measurements (dcap-tdx-example)
62+
testMeasurements = TestMeasurements{
63+
0: mustBytesFromHex("5d56080eb9ef8ce0bbaf6bdcdadeeb06e7c5b0a4d1ec16be868a85a953babe0c5e54d01c8e050a54fe1ca078372530d2"),
64+
1: mustBytesFromHex("4216e925f796f4e282cfa6e72d4c77a80560987afa29155a61fdc33adb80eab0d4112abd52387e5e25a60deefb8a5287"),
65+
2: mustBytesFromHex("4274fefb79092c164000b571b64ecb432fa2357adb421fd1c77a867168d7d7f7fe82796d1eba092c7bab35cf43f5ec55"),
66+
3: mustBytesFromHex("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"),
67+
4: mustBytesFromHex("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"),
68+
}
69+
exists, foundMeasurement = m.Contains(testMeasurements)
70+
require.True(t, exists)
71+
require.Equal(t, "dcap-tdx-example-02", foundMeasurement.MeasurementID)
72+
}
73+
74+
func TestMultiMeasurementsV1(t *testing.T) {
75+
tempDir := t.TempDir()
76+
err := os.WriteFile(filepath.Join(tempDir, "measurements.json"), measurementsV1JSON, 0644)
77+
require.NoError(t, err)
78+
79+
// Load expected measurements from JSON file
80+
m, err := New(filepath.Join(tempDir, "measurements.json"))
81+
require.NoError(t, err)
82+
require.Len(t, m.Measurements, 2)
83+
84+
testMeasurements := TestMeasurements{
85+
0: mustBytesFromHex("5d56080eb9ef8ce0bbaf6bdcdadeeb06e7c5b0a4d1ec16be868a85a953babe0c5e54d01c8e050a54fe1ca078372530d2"),
86+
1: mustBytesFromHex("4216e925f796f4e282cfa6e72d4c77a80560987afa29155a61fdc33adb80eab0d4112abd52387e5e25a60deefb8a5287"),
87+
2: mustBytesFromHex("4274fefb79092c164000b571b64ecb432fa2357adb421fd1c77a867168d7d7f7fe82796d1eba092c7bab35cf43f5ec55"),
88+
3: mustBytesFromHex("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"),
89+
4: mustBytesFromHex("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"),
90+
}
91+
exists, foundMeasurement := m.Contains(testMeasurements)
92+
require.True(t, exists)
93+
require.Equal(t, "dcap-tdx-example", foundMeasurement.MeasurementID)
94+
}

0 commit comments

Comments
 (0)