|
1 | 1 | package confidentialrelay |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "bytes" |
4 | 5 | "context" |
5 | 6 | "encoding/base64" |
6 | 7 | "encoding/hex" |
7 | 8 | "encoding/json" |
8 | 9 | "errors" |
9 | 10 | "fmt" |
| 11 | + "sort" |
10 | 12 | "time" |
11 | 13 |
|
12 | 14 | "github.com/ethereum/go-ethereum/common" |
@@ -220,6 +222,20 @@ func (h *Handler) handleSecretsGet(ctx context.Context, gatewayID string, req *j |
220 | 222 | if err := h.verifyAttestationHash(ctx, att, params, confidentialrelaytypes.DomainSecretsGet); err != nil { |
221 | 223 | return h.errorResponse(ctx, gatewayID, req, jsonrpc.ErrInternal, err) |
222 | 224 | } |
| 225 | + // Fetch the local node once: it provides both the WorkflowDON snapshot for |
| 226 | + // the enclave-config check below and the DON metadata on the vault request. |
| 227 | + localNode, err := h.capRegistry.LocalNode(ctx) |
| 228 | + if err != nil { |
| 229 | + return h.errorResponse(ctx, gatewayID, req, jsonrpc.ErrInternal, fmt.Errorf("failed to get local node: %w", err)) |
| 230 | + } |
| 231 | + // Verify the enclave's reported config matches the onchain DON state |
| 232 | + // before treating the attested request as trusted: the Nitro attestation |
| 233 | + // binds the request hash, but a malicious host can produce a |
| 234 | + // genuinely-attested request over a forged enclave config unless we |
| 235 | + // compare the config value against the DON reference. |
| 236 | + if err = h.verifyEnclaveConfigMatchesDON(localNode, params.EnclaveConfig); err != nil { |
| 237 | + return h.errorResponse(ctx, gatewayID, req, jsonrpc.ErrInternal, err) |
| 238 | + } |
223 | 239 |
|
224 | 240 | vaultCap, err := h.capRegistry.GetExecutable(ctx, vault.CapabilityID) |
225 | 241 | if err != nil { |
@@ -255,11 +271,6 @@ func (h *Handler) handleSecretsGet(ctx context.Context, gatewayID string, req *j |
255 | 271 | return h.errorResponse(ctx, gatewayID, req, jsonrpc.ErrInternal, fmt.Errorf("failed to wrap vault request: %w", err)) |
256 | 272 | } |
257 | 273 |
|
258 | | - localNode, err := h.capRegistry.LocalNode(ctx) |
259 | | - if err != nil { |
260 | | - return h.errorResponse(ctx, gatewayID, req, jsonrpc.ErrInternal, fmt.Errorf("failed to get local node: %w", err)) |
261 | | - } |
262 | | - |
263 | 274 | metadata := capabilities.RequestMetadata{ |
264 | 275 | WorkflowID: params.WorkflowID, |
265 | 276 | WorkflowOwner: params.Owner, |
@@ -389,6 +400,13 @@ func (h *Handler) handleCapabilityExecute(ctx context.Context, gatewayID string, |
389 | 400 | if err := h.verifyAttestationHash(ctx, att, params, confidentialrelaytypes.DomainCapabilityExec); err != nil { |
390 | 401 | return h.errorResponse(ctx, gatewayID, req, jsonrpc.ErrInternal, err) |
391 | 402 | } |
| 403 | + localNode, err := h.capRegistry.LocalNode(ctx) |
| 404 | + if err != nil { |
| 405 | + return h.errorResponse(ctx, gatewayID, req, jsonrpc.ErrInternal, fmt.Errorf("failed to get local node: %w", err)) |
| 406 | + } |
| 407 | + if err = h.verifyEnclaveConfigMatchesDON(localNode, params.EnclaveConfig); err != nil { |
| 408 | + return h.errorResponse(ctx, gatewayID, req, jsonrpc.ErrInternal, err) |
| 409 | + } |
392 | 410 |
|
393 | 411 | capability, err := h.capRegistry.GetExecutable(ctx, params.CapabilityID) |
394 | 412 | if err != nil { |
@@ -461,6 +479,51 @@ func (h *Handler) handleCapabilityExecute(ctx context.Context, gatewayID string, |
461 | 479 | return h.jsonResponse(req, signedResult) |
462 | 480 | } |
463 | 481 |
|
| 482 | +// verifyEnclaveConfigMatchesDON compares the enclave's reported EnclaveConfig |
| 483 | +// against the local node's WorkflowDON membership and fault tolerance. The |
| 484 | +// relay DON runs on the same nodes as the workflow DON, so |
| 485 | +// localNode.WorkflowDON.Members is the right comparison target. |
| 486 | +// |
| 487 | +// PRIV-458: the Nitro attestation binds the request hash but does not on its |
| 488 | +// own prove the config matches the DON, so a malicious host could produce a |
| 489 | +// genuinely-attested request over a forged config. Comparing the attested |
| 490 | +// config against onchain DON state closes that gap. |
| 491 | +// |
| 492 | +// localNode is passed in so each request fetches it once (it feeds request |
| 493 | +// metadata too); the caller's lookup is an O(1) in-memory read populated by |
| 494 | +// the registry syncer, so this stays off the RPC hot path. |
| 495 | +// |
| 496 | +// cfg is optional: a nil EnclaveConfig (sender on an older protocol that does |
| 497 | +// not include it) is accepted and skips the check. The config is verified |
| 498 | +// only when present. |
| 499 | +func (h *Handler) verifyEnclaveConfigMatchesDON(localNode capabilities.Node, cfg *confidentialrelaytypes.EnclaveConfig) error { |
| 500 | + if cfg == nil { |
| 501 | + return nil |
| 502 | + } |
| 503 | + expectedF := uint32(localNode.WorkflowDON.F) |
| 504 | + if cfg.F != expectedF { |
| 505 | + return fmt.Errorf("enclave config F mismatch: enclave reports %d, expected %d", cfg.F, expectedF) |
| 506 | + } |
| 507 | + if len(cfg.Signers) != len(localNode.WorkflowDON.Members) { |
| 508 | + return fmt.Errorf("enclave config signers count mismatch: enclave reports %d, expected %d", |
| 509 | + len(cfg.Signers), len(localNode.WorkflowDON.Members)) |
| 510 | + } |
| 511 | + expected := make([][]byte, len(localNode.WorkflowDON.Members)) |
| 512 | + for i := range localNode.WorkflowDON.Members { |
| 513 | + expected[i] = localNode.WorkflowDON.Members[i][:] |
| 514 | + } |
| 515 | + actual := append([][]byte(nil), cfg.Signers...) |
| 516 | + sort.Slice(actual, func(i, j int) bool { return bytes.Compare(actual[i], actual[j]) < 0 }) |
| 517 | + sort.Slice(expected, func(i, j int) bool { return bytes.Compare(expected[i], expected[j]) < 0 }) |
| 518 | + for i := range actual { |
| 519 | + if !bytes.Equal(actual[i], expected[i]) { |
| 520 | + return fmt.Errorf("enclave config signer mismatch at sorted index %d: enclave reports %x, expected %x", |
| 521 | + i, actual[i], expected[i]) |
| 522 | + } |
| 523 | + } |
| 524 | + return nil |
| 525 | +} |
| 526 | + |
464 | 527 | // getEnclaveAttestationConfig reads the enclave pool configuration from the |
465 | 528 | // capabilities registry and returns trusted measurement sets and CA roots |
466 | 529 | // for attestation validation. Called per-request so the config stays fresh |
|
0 commit comments