Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ This repository contains a sample [measurements.json](./measurements.json) file
## Measurements

Attestation verification requires the expected measurements which you pass through the `--{client, server}-measurements` flag.
The measurements are expected to be a JSON map, and multiple valid measurements can be provided. The verifier will attempt to verify with each of the provided measurements, and if any succeeds, the attestation is assumed valid.
Measurements can be provided in the legacy JSON map format, the current measurement container format, or as raw GCP DCAP measurement JSON output by [flashbots/dstack-mr-gcp](https://github.com/flashbots/dstack-mr-gcp). Multiple valid measurements can be provided. The verifier will attempt to verify with each of the provided measurements, and if any succeeds, the attestation is assumed valid.

The (single) validated measurement is json-marshalled and forwarded (returned in the case of client) as "X-Flashbots-Measurement" header, and the type of attestation as "X-Flashbots-Attestation-Type" header. For mapping attestation types to OIDs and issuers, see [internal/attestation/variant/variant.go](./internal/attestation/variant/variant.go).
To only validate and forward the measurement (as opposed to also authorizing the measurement against an expected one), simply provide an empty expected measurements object.
Expand Down
103 changes: 97 additions & 6 deletions multimeasurements/multimeasurements.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ package multimeasurements
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"

"github.com/flashbots/cvm-reverse-proxy/internal/attestation/measurements"
"github.com/flashbots/cvm-reverse-proxy/internal/encoding"
)

// MultiMeasurements holds several known measurements, and can check if
Expand All @@ -30,6 +32,19 @@ type MeasurementsContainer struct {

type LegacyMultiMeasurements map[string]measurements.M

// Caps expansion of dstack-mr-gcp measurements
const maxGCPMeasurementContainers = 10_000

// Structure used by the dstack-mr-gcp output. mrtd and rtmr0 hold one entry per
// possible value; rtmr1-3 are single values.
type rawGCPMeasurements struct {
MRTD []encoding.HexBytes `json:"mrtd"`
RTMR0 []encoding.HexBytes `json:"rtmr0"`
RTMR1 encoding.HexBytes `json:"rtmr1"`
RTMR2 encoding.HexBytes `json:"rtmr2"`
RTMR3 encoding.HexBytes `json:"rtmr3"`
}

// New returns a MultiMeasurements instance, with the measurements
// loaded from a file or URL.
func New(path string) (m *MultiMeasurements, err error) {
Expand All @@ -53,12 +68,19 @@ func New(path string) (m *MultiMeasurements, err error) {
}
}

m = &MultiMeasurements{}
return NewFromBytes(data)
}

// NewFromBytes returns a MultiMeasurements instance loaded from JSON bytes
func NewFromBytes(data []byte) (*MultiMeasurements, error) {
m := &MultiMeasurements{}

if err := json.Unmarshal(data, &m.Measurements); err == nil {
return m, nil
}

// Try to load the v2 data schema, if that fails fall back to legacy v1 schema
if err = json.Unmarshal(data, &m.Measurements); err != nil {
var legacyData LegacyMultiMeasurements
err = json.Unmarshal(data, &legacyData)
var legacyData LegacyMultiMeasurements
if err := json.Unmarshal(data, &legacyData); err == nil {
for measurementID, measurements := range legacyData {
container := MeasurementsContainer{
MeasurementID: measurementID,
Expand All @@ -67,9 +89,78 @@ func New(path string) (m *MultiMeasurements, err error) {
}
m.Measurements = append(m.Measurements, container)
}
return m, nil
}

rawGCPContainers, err := parseRawGCPMeasurements(data)
if err != nil {
return nil, err
}
m.Measurements = rawGCPContainers
return m, nil
}

// parseRawGCPMeasurements expands dstack-mr-gcp JSON into DCAP TDX measurement containers
func parseRawGCPMeasurements(data []byte) ([]MeasurementsContainer, error) {
var raw rawGCPMeasurements
if err := json.Unmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("parsing raw GCP measurements: %w", err)
}

if raw.RTMR3 == nil {
raw.RTMR3 = make(encoding.HexBytes, measurements.TDXMeasurementLength)
}
if err := validateGCPMeasurements(raw); err != nil {
return nil, err
}

total := len(raw.MRTD) * len(raw.RTMR0)
if total > maxGCPMeasurementContainers {
return nil, fmt.Errorf("parsing raw GCP measurements: cartesian product of %d containers exceeds limit of %d", total, maxGCPMeasurementContainers)
}

containers := make([]MeasurementsContainer, 0, total)
for mrtdIdx, mrtd := range raw.MRTD {
for rtmr0Idx, rtmr0 := range raw.RTMR0 {
containers = append(containers, MeasurementsContainer{
MeasurementID: fmt.Sprintf("dstack-mr-gcp-%d-%d", mrtdIdx, rtmr0Idx),
AttestationType: "dcap-tdx",
Measurements: measurements.M{
0: {Expected: mrtd, ValidationOpt: measurements.Enforce},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to not support optional measurements for dstack-mr format, for two reasons:

  1. this is a very temporary fix and the current usage does not need it
  2. if someone is using dstack-mr specifically, they get all of the values and should enforce all of the values semantically

If someone needs to ignore a specific measurement register, there's no good way of doing it in dstack-mr format and I'd argue it's a better idea to not allow ignoring measurements in this format.

1: {Expected: rtmr0, ValidationOpt: measurements.Enforce},
2: {Expected: raw.RTMR1, ValidationOpt: measurements.Enforce},
3: {Expected: raw.RTMR2, ValidationOpt: measurements.Enforce},
4: {Expected: raw.RTMR3, ValidationOpt: measurements.Enforce},
},
})
}
}

return containers, nil
}

// validateGCPMeasurements checks that all dstack-mr-gcp fields are present and have valid TDX measurement lengths
func validateGCPMeasurements(raw rawGCPMeasurements) error {
lists := map[string][]encoding.HexBytes{"mrtd": raw.MRTD, "rtmr0": raw.RTMR0}
for field, values := range lists {
if len(values) == 0 {
return fmt.Errorf("parsing raw GCP measurements: %q must not be empty", field)
}
for idx, value := range values {
if len(value) != measurements.TDXMeasurementLength {
return fmt.Errorf("parsing raw GCP measurements: %q[%d] has invalid length %d", field, idx, len(value))
}
}
}

scalars := map[string]encoding.HexBytes{"rtmr1": raw.RTMR1, "rtmr2": raw.RTMR2, "rtmr3": raw.RTMR3}
for field, value := range scalars {
if len(value) != measurements.TDXMeasurementLength {
return fmt.Errorf("parsing raw GCP measurements: %q has invalid length %d", field, len(value))
}
}

return m, err
return nil
}

// Contains checks if the provided measurements match one of the known measurements. Any keys in the provided
Expand Down
153 changes: 142 additions & 11 deletions multimeasurements/multimeasurements_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/hex"
"os"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/require"
Expand All @@ -23,6 +24,67 @@ func mustBytesFromHex(hexValue string) []byte {
// Measurements V1 (legacy) JSON (from https://github.com/flashbots/cvm-reverse-proxy/blob/837588b9f87ee49d1bb6dca4712a1c2844eb1ecc/measurements.json)
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"}}}`)

const rawGCPChunkedJSON = `{
"rtmr1": "cdf855b56d27967473b885164b3910ab4d81f3db0bd50e114593bd5fd91cf55760de7776c93f4724cefeaf5ac0843e62",
"rtmr2": "438337a98c597535940941a3d9913e04a76d84e4ebf69dbb89e1addc8bae7183579685f0ef3144875dba7d933d9dcabf",
"rtmr3": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"rtmr0": [
"c07820997dc2a5e1cc67b05e89852c1a72289e0ec82034bee5b3605cd759328853a758a346522651956afe9222914235",
"c6975b4a5d66fd88bce4d449ed161e0800ce0f5bcd6a3246f2c83407e230474ff56e6e8d5bc9e9d95cf692d458257954",
"8256ae5bb15489ebb0181f9935fe00625751e879d4e3b0a111aaba48da31e5cdd82f379cf4b97411618b02ce1deba1fd",
"46af3396ed9969f670f69142f7b74514598b0f25bb66fc3094402c2de37f4f7493d83cc0860416d3e50f7dda1c34f658",
"c0f93df02880d6c1dc1a5104e04ef691bc41cdc9da49f8834cc1cdc10acdb557758371d19d2466b560be7a66643953fd",
"6927bd0230ca2dba4ad21ee68c1f7e018660d5ad6a99e185eac8adc1f05dd6eeb10178d2a744d22cd14b8945b454712b",
"b90ca4eb0badc04128035ad62f1d1e792f1ca40c99ce25e3cce13f8167eb6265e890c65097518e9f8f5af91519d60d73",
"c3940a1a1f6709fed6d90c34698cb91767fa6261f4469fdcf6da5e36c6f2493fdfd34bc2d1a9f726a17ed88a79b33561",
"8530903ec5cb9aad1737c2bf0e9df958ec0a3ede63fe556415990192b4def86a50d8f6869b6283141b13dd4848b0cea2",
"9efb7193464610d63fbd948901998eda998b3e47e9a0abb72857ba948dcbacd3a17ee75e5081455dccaae208b8294bd9",
"5c942d2f4a08acd594b7d8914362835dcbb12994aedc2128abecf5585807cb8886faf7b9b32611cd4e63eac269632560",
"284f209fece0d49331f2e411c46f6debc3be698bd8587264c90ab0dbdf651046aef3badaae9a7983ea0855a7dabdfa00",
"8d5f7f704ccba0a63157f1ec1214c7f043005b045adae261e0581951965a96350d6196f38751b5dd0e72fca181817efa",
"123ad184172b44083b191b12557f3c923416d8e654ffb390736db331ff2a5bce6c89d14d62cf70e113b98d8f13e78519",
"8ae3d7af48afa0f30fd700a58ca84cd5e0054fbe011d9ed228e30a17db456987e63c6dfd71437aac33ffb9d796088d70",
"3fdfcb2bbf25c9e535f7e4724b1cee79666824cab1565f985d2e1e0218818d538cf6f3bfa5c623c13d6226ae51ea8cd6",
"640b92712990cc8aab4f3786611a8acc3180525abd42a31c06eb7611b8c54a72247dfe8a7a93d3c922f771797a7932de",
"7c8fc1dd62391d416ac64174b833f821b59738d816d96168483300127608e0cf3345840b5bd9325c125dd6b2f595f1b0",
"a191d8250215e05e31fc42fa00f4b7a8729e1fb83b3dacb3def3989b9eaa3f8d199b96759477ce20bbd47c909b6b984c",
"633eeb1778affe65d1b3633527395763602c06e9d7aea52a2a6d5073c33ee1fe78f3a83aeb58edd036de681eee3d1f0f",
"b372a4eac4561e3a8d92028a38e8860a63d7e69c7fcab250aa49d1c951c94b49d0abbe87c353fcd14651f64ac5dde055",
"90d7dbdb795d66669ff44aff1f8ea0de13f5362f1dec68f17fca60364fcc019de18b246c9e173c09102360442dba3261",
"8292abcf17f665c5f63e158a5fd7f2e160ac5b5ae4811532d93c3b5f38a53adebebddaf531aca4ef91d9fb68fb4312a8",
"e1d0235496f93f9475bf0b26d33da5c15831cfc94104d6bea7ab82db027c5f1e917d47dda6953eefae7dcb20ab6f75c4"
],
"mrtd": [
"a5844e88897b70c318bef929ef4dfd6c7304c52c4bc9c3f39132f0fdccecf3eb5bab70110ee42a12509a31c037288694",
"8370d8f6d02f2d13e211e91c93fde923049522b241425a29a7bf0071ef49b250af4ef49d852fa3e10065d1b51dfce8fb",
"feb7486608382c1ff0e15b4648ddc0acea6ca974eb53e3529f4c4bd5ffbaa20bf335cb75965cea65fe473aed9647c162"
],
"mrconfigid": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"xfam": "e700060000000000",
"tdattributes": "0000001000000000"
}`

// rawGCPScalarJSON returns a single-value dstack-mr-gcp fixture
func rawGCPScalarJSON() []byte {
return []byte(`{
"mrtd": ["` + strings.Repeat("11", 48) + `"],
"rtmr0": ["` + strings.Repeat("22", 48) + `"],
"rtmr1": "` + strings.Repeat("33", 48) + `",
"rtmr2": "` + strings.Repeat("44", 48) + `",
"mr_aggregated": "` + strings.Repeat("aa", 32) + `",
"mr_image": "` + strings.Repeat("bb", 32) + `"
}`)
}

// writeMeasurementsFile writes a temporary measurements fixture
func writeMeasurementsFile(t *testing.T, data []byte) string {
t.Helper()
path := filepath.Join(t.TempDir(), "measurements.json")
err := os.WriteFile(path, data, 0644)
require.NoError(t, err)
return path
}

// TestMultiMeasurementsV2 tests the v2 data schema
func TestMultiMeasurementsV2(t *testing.T) {
// Load expected measurements from JSON file (in V2 format)
Expand Down Expand Up @@ -58,26 +120,22 @@ func TestMultiMeasurementsV2(t *testing.T) {
exists, _ = m.Contains(testMeasurements)
require.False(t, exists)

// Check for another set of known measurements (dcap-tdx-example)
// Check for another set of known measurements (dcap-tdx-dummy)
testMeasurements = TestMeasurements{
0: mustBytesFromHex("5d56080eb9ef8ce0bbaf6bdcdadeeb06e7c5b0a4d1ec16be868a85a953babe0c5e54d01c8e050a54fe1ca078372530d2"),
1: mustBytesFromHex("4216e925f796f4e282cfa6e72d4c77a80560987afa29155a61fdc33adb80eab0d4112abd52387e5e25a60deefb8a5287"),
2: mustBytesFromHex("4274fefb79092c164000b571b64ecb432fa2357adb421fd1c77a867168d7d7f7fe82796d1eba092c7bab35cf43f5ec55"),
3: mustBytesFromHex("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"),
0: mustBytesFromHex("47a1cc074b914df8596bad0ed13d50d561ad1effc7f7cc530ab86da7ea49ffc03e57e7da829f8cba9c629c3970505323"),
1: mustBytesFromHex("da6e07866635cb34a9ffcdc26ec6622f289e625c42c39b320f29cdf1dc84390b4f89dd0b073be52ac38ca7b0a0f375bb"),
2: mustBytesFromHex("a7157e7c5f932e9babac9209d4527ec9ed837b8e335a931517677fa746db51ee56062e3324e266e3f39ec26a516f4f71"),
3: mustBytesFromHex("e63560e50830e22fbc9b06cdce8afe784bf111e4251256cf104050f1347cd4ad9f30da408475066575145da0b098a124"),
4: mustBytesFromHex("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"),
}
exists, foundMeasurement = m.Contains(testMeasurements)
require.True(t, exists)
require.Equal(t, "dcap-tdx-example-02", foundMeasurement.MeasurementID)
require.Equal(t, "dcap-tdx-dummy", foundMeasurement.MeasurementID)
}

func TestMultiMeasurementsV1(t *testing.T) {
tempDir := t.TempDir()
err := os.WriteFile(filepath.Join(tempDir, "measurements.json"), measurementsV1JSON, 0644)
require.NoError(t, err)

// Load expected measurements from JSON file
m, err := New(filepath.Join(tempDir, "measurements.json"))
m, err := New(writeMeasurementsFile(t, measurementsV1JSON))
require.NoError(t, err)
require.Len(t, m.Measurements, 2)

Expand All @@ -92,3 +150,76 @@ func TestMultiMeasurementsV1(t *testing.T) {
require.True(t, exists)
require.Equal(t, "dcap-tdx-example", foundMeasurement.MeasurementID)
}

// TestMultiMeasurementsRawGCPScalar tests scalar-valued dstack-mr-gcp measurements
func TestMultiMeasurementsRawGCPScalar(t *testing.T) {
m, err := New(writeMeasurementsFile(t, rawGCPScalarJSON()))
require.NoError(t, err)
require.Len(t, m.Measurements, 1)

container := m.Measurements[0]
require.Equal(t, "dcap-tdx", container.AttestationType)
require.Equal(t, mustBytesFromHex(strings.Repeat("11", 48)), container.Measurements[0].Expected)
require.Equal(t, mustBytesFromHex(strings.Repeat("22", 48)), container.Measurements[1].Expected)
require.Equal(t, mustBytesFromHex(strings.Repeat("33", 48)), container.Measurements[2].Expected)
require.Equal(t, mustBytesFromHex(strings.Repeat("44", 48)), container.Measurements[3].Expected)
require.Equal(t, mustBytesFromHex(strings.Repeat("00", 48)), container.Measurements[4].Expected)

exists, foundMeasurement := m.Contains(TestMeasurements{
0: mustBytesFromHex(strings.Repeat("11", 48)),
1: mustBytesFromHex(strings.Repeat("22", 48)),
2: mustBytesFromHex(strings.Repeat("33", 48)),
3: mustBytesFromHex(strings.Repeat("44", 48)),
4: mustBytesFromHex(strings.Repeat("00", 48)),
})
require.True(t, exists)
require.Equal(t, container.MeasurementID, foundMeasurement.MeasurementID)
}

// TestMultiMeasurementsRawGCPChunked tests list-valued dstack-mr-gcp measurements
func TestMultiMeasurementsRawGCPChunked(t *testing.T) {
m, err := New(writeMeasurementsFile(t, []byte(rawGCPChunkedJSON)))
require.NoError(t, err)
require.Len(t, m.Measurements, 72)

exists, foundMeasurement := m.Contains(TestMeasurements{
0: mustBytesFromHex("feb7486608382c1ff0e15b4648ddc0acea6ca974eb53e3529f4c4bd5ffbaa20bf335cb75965cea65fe473aed9647c162"),
1: mustBytesFromHex("c6975b4a5d66fd88bce4d449ed161e0800ce0f5bcd6a3246f2c83407e230474ff56e6e8d5bc9e9d95cf692d458257954"),
2: mustBytesFromHex("cdf855b56d27967473b885164b3910ab4d81f3db0bd50e114593bd5fd91cf55760de7776c93f4724cefeaf5ac0843e62"),
3: mustBytesFromHex("438337a98c597535940941a3d9913e04a76d84e4ebf69dbb89e1addc8bae7183579685f0ef3144875dba7d933d9dcabf"),
4: mustBytesFromHex(strings.Repeat("00", 48)),
})
require.True(t, exists)
require.Equal(t, "dcap-tdx", foundMeasurement.AttestationType)
}

// TestMultiMeasurementsRawGCPMalformed tests malformed dstack-mr-gcp measurements
func TestMultiMeasurementsRawGCPMalformed(t *testing.T) {
_, err := New(writeMeasurementsFile(t, []byte(`{
"mrtd": ["not-hex"],
"rtmr0": ["`+strings.Repeat("22", 48)+`"],
"rtmr1": "`+strings.Repeat("33", 48)+`",
"rtmr2": "`+strings.Repeat("44", 48)+`"
}`)))
require.Error(t, err)
require.Contains(t, err.Error(), "parsing raw GCP measurements")
}

// TestMultiMeasurementsRawGCPCartesianLimit tests that oversized cartesian products are rejected
func TestMultiMeasurementsRawGCPCartesianLimit(t *testing.T) {
// 101 values for both mrtd and rtmr0 -> 101*101 = 10,201 > 10,000 limit
manyValues := make([]string, 101)
for i := range manyValues {
manyValues[i] = `"` + strings.Repeat("ab", 48) + `"`
}
valuesJSON := "[" + strings.Join(manyValues, ",") + "]"

_, err := NewFromBytes([]byte(`{
"mrtd": ` + valuesJSON + `,
"rtmr0": ` + valuesJSON + `,
"rtmr1": "` + strings.Repeat("33", 48) + `",
"rtmr2": "` + strings.Repeat("44", 48) + `"
}`))
require.Error(t, err)
require.Contains(t, err.Error(), "exceeds limit")
}
10 changes: 2 additions & 8 deletions proxy/atls_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ package proxy
import (
"context"
"crypto/x509/pkix"
"encoding/json"
"errors"
"fmt"
"log/slog"
Expand Down Expand Up @@ -89,16 +88,11 @@ func CreateAttestationValidatorsFromFile(log *slog.Logger, jsonMeasurementsPath
return nil, nil
}

jsonMeasurements, err := os.ReadFile(jsonMeasurementsPath)
if err != nil {
return nil, err
}

var parsedMeasurements []multimeasurements.MeasurementsContainer
err = json.Unmarshal(jsonMeasurements, &parsedMeasurements)
multiMeasurements, err := multimeasurements.New(jsonMeasurementsPath)
if err != nil {
return nil, err
}
parsedMeasurements := multiMeasurements.Measurements

// Group validators by attestation type
validatorsByType := make(map[AttestationType][]atls.Validator)
Expand Down
Loading
Loading