From 0c701f86795c68fa3d9ba87f42fb6c9ef01b95ce Mon Sep 17 00:00:00 2001 From: Tejaswi Nadahalli Date: Mon, 1 Jun 2026 17:08:33 +0200 Subject: [PATCH 1/4] feat(confidentialrelay): vendor SignedComputeRequest + canonical Hash The relay DON (chainlink/core) cannot import confidential-compute (the dependency runs the other way), so vendor ComputeRequest/SignedComputeRequest and the canonical ComputeRequest.Hash into the confidentialrelay package. This lets the relay verify the Workflow DON's F+1 signatures over a forwarded compute request directly, the basis for replacing the separate WorkflowAuthz blob. Hash reuses this package's existing length-prefix helpers (byte-identical to the source's writeWithLength/writeLengthPrefix), so no duplicate helpers. Tests cover determinism, field-binding, and the intentional exclusion of EncryptedDecryptionKeyShares. Byte-for-byte conformance with the source Hash will be enforced by a test in confidential-compute (which can import this package); common cannot import CC. --- .../confidentialrelay/computerequest.go | 78 +++++++++++++++++++ .../confidentialrelay/computerequest_test.go | 61 +++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 pkg/capabilities/v2/actions/confidentialrelay/computerequest.go create mode 100644 pkg/capabilities/v2/actions/confidentialrelay/computerequest_test.go diff --git a/pkg/capabilities/v2/actions/confidentialrelay/computerequest.go b/pkg/capabilities/v2/actions/confidentialrelay/computerequest.go new file mode 100644 index 0000000000..2acc54a988 --- /dev/null +++ b/pkg/capabilities/v2/actions/confidentialrelay/computerequest.go @@ -0,0 +1,78 @@ +package confidentialrelay + +import "crypto/sha256" + +// computeRequestDomainSeparator is vendored verbatim from confidential-compute +// types.DomainSeparator. It MUST stay byte-identical to the source, or +// ComputeRequest.Hash will not match the digest the Workflow DON nodes signed and +// F+1 verification at the relay DON will fail. chainlink-common cannot import +// confidential-compute, so the byte-for-byte conformance check lives in that repo +// (which can import this package). +const computeRequestDomainSeparator = "CONFIDENTIAL_COMPUTE_PAYLOAD" + +// ComputeRequest is vendored from confidential-compute types.ComputeRequest. The +// relay DON cannot import confidential-compute (the dependency runs the other way), +// so the type and its canonical Hash are copied here. The enclave forwards the +// Workflow-DON-signed compute request to the relay, which reconstructs the hash and +// verifies the F+1 signatures over it. +// +// PublicData carries the marshaled WorkflowExecution (owner, orgid, workflowID, +// executionID); the relay unmarshals it via chainlink-protos to recover the +// authorized identity. +type ComputeRequest struct { + RequestID [32]byte `json:"requestID"` + PublicData []byte `json:"publicData"` + Ciphertexts [][]byte `json:"ciphertexts"` + CiphertextNames []string `json:"CiphertextNames"` + EncryptedDecryptionKeyShares [][][]byte `json:"encryptedDecryptionKeyShares"` + EnclaveEphemeralPublicKey []byte `json:"enclaveEphemeralPublicKey"` + MasterPublicKey []byte `json:"masterPublicKey"` + AppID string `json:"appID"` + Version string `json:"version"` +} + +// Hash mirrors confidential-compute types.ComputeRequest.Hash byte-for-byte. It +// reuses this package's length-prefix helpers (writeBytes/writeString/ +// writeLengthPrefix), which are identical to the source's writeWithLength/ +// writeLengthPrefix. EncryptedDecryptionKeyShares is intentionally excluded, +// matching the source. +func (cr ComputeRequest) Hash() [32]byte { + h := sha256.New() + + h.Write([]byte(computeRequestDomainSeparator)) + h.Write([]byte("\nComputeRequest\n")) + + h.Write(cr.RequestID[:]) + + writeBytes(h, cr.PublicData) + + writeLengthPrefix(h, len(cr.CiphertextNames)) + for _, name := range cr.CiphertextNames { + writeString(h, name) + } + + writeLengthPrefix(h, len(cr.Ciphertexts)) + for _, ciphertext := range cr.Ciphertexts { + writeBytes(h, ciphertext) + } + + writeBytes(h, cr.EnclaveEphemeralPublicKey) + writeBytes(h, cr.MasterPublicKey) + + writeString(h, cr.AppID) + writeString(h, cr.Version) + + var result [32]byte + h.Sum(result[:0]) + return result +} + +// SignedComputeRequest is vendored from confidential-compute +// types.SignedComputeRequest: a ComputeRequest plus one Workflow DON node's +// signature over ComputeRequest.Hash. The enclave forwards the F+1 signed requests +// to the relay DON as the authorization for a secrets request. +type SignedComputeRequest struct { + ComputeRequest + Signature []byte `json:"signature"` + PerNodeData map[string]string `json:"perNodeData,omitempty"` +} diff --git a/pkg/capabilities/v2/actions/confidentialrelay/computerequest_test.go b/pkg/capabilities/v2/actions/confidentialrelay/computerequest_test.go new file mode 100644 index 0000000000..6f33fc6388 --- /dev/null +++ b/pkg/capabilities/v2/actions/confidentialrelay/computerequest_test.go @@ -0,0 +1,61 @@ +package confidentialrelay + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func sampleComputeRequest() ComputeRequest { + var rid [32]byte + for i := range rid { + rid[i] = byte(i) + } + return ComputeRequest{ + RequestID: rid, + PublicData: []byte("public-data"), + Ciphertexts: [][]byte{[]byte("ct-a"), []byte("ct-b")}, + CiphertextNames: []string{"name-a", "name-b"}, + EnclaveEphemeralPublicKey: []byte("ephemeral-pub-key"), + MasterPublicKey: []byte("master-pub-key"), + AppID: "test-app", + Version: "v1.2.3", + } +} + +func TestComputeRequestHash_Deterministic(t *testing.T) { + require.Equal(t, sampleComputeRequest().Hash(), sampleComputeRequest().Hash()) +} + +// Every field the source binds must change the hash. (Conformance with +// confidential-compute's source Hash is enforced by a test in that repo, which can +// import this package; chainlink-common cannot import confidential-compute.) +func TestComputeRequestHash_BindsFields(t *testing.T) { + base := sampleComputeRequest().Hash() + + mutations := map[string]func(*ComputeRequest){ + "requestID": func(c *ComputeRequest) { c.RequestID = [32]byte{0xff} }, + "publicData": func(c *ComputeRequest) { c.PublicData = []byte("other") }, + "ciphertextNames": func(c *ComputeRequest) { c.CiphertextNames = []string{"x"} }, + "ciphertexts": func(c *ComputeRequest) { c.Ciphertexts = [][]byte{[]byte("x")} }, + "ephemeralKey": func(c *ComputeRequest) { c.EnclaveEphemeralPublicKey = []byte("x") }, + "masterKey": func(c *ComputeRequest) { c.MasterPublicKey = []byte("x") }, + "appID": func(c *ComputeRequest) { c.AppID = "other" }, + "version": func(c *ComputeRequest) { c.Version = "other" }, + } + for name, mutate := range mutations { + t.Run(name, func(t *testing.T) { + c := sampleComputeRequest() + mutate(&c) + require.NotEqual(t, base, c.Hash(), "hash must change when %s changes", name) + }) + } +} + +// EncryptedDecryptionKeyShares is intentionally excluded from the hash, matching the +// source; this pins that so a future copy edit can't silently start binding it. +func TestComputeRequestHash_IgnoresEncryptedShares(t *testing.T) { + withShares := sampleComputeRequest() + withShares.EncryptedDecryptionKeyShares = [][][]byte{{[]byte("share")}} + require.Equal(t, sampleComputeRequest().Hash(), withShares.Hash()) +} From 59752c385e81580aa494654b369442255e2e162c Mon Sep 17 00:00:00 2001 From: Tejaswi Nadahalli Date: Mon, 1 Jun 2026 17:12:57 +0200 Subject: [PATCH 2/4] feat(confidentialrelay): carry SignedComputeRequests on SecretsRequestParams Add SignedComputeRequests []SignedComputeRequest to SecretsRequestParams: the F+1 Workflow-DON-signed compute requests the enclave forwards for the relay DON to verify. Excluded from the response hash, matching Attestation/EnclaveConfig. --- pkg/capabilities/v2/actions/confidentialrelay/types.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/capabilities/v2/actions/confidentialrelay/types.go b/pkg/capabilities/v2/actions/confidentialrelay/types.go index 0d629aa82b..a89a57b571 100644 --- a/pkg/capabilities/v2/actions/confidentialrelay/types.go +++ b/pkg/capabilities/v2/actions/confidentialrelay/types.go @@ -61,6 +61,13 @@ type SecretsRequestParams struct { // the EnclaveConfig type doc-comment for the threat model. EnclaveConfig EnclaveConfig `json:"enclave_config"` Attestation string `json:"attestation,omitempty"` + + // SignedComputeRequests carries the F+1 Workflow-DON-signed compute requests the + // enclave forwards verbatim. The relay DON verifies the signatures over + // ComputeRequest.Hash() against its Workflow DON signer set and reads the + // authorized identity from PublicData (the WorkflowExecution proto). Like + // Attestation, it is authorization input and is excluded from the response hash. + SignedComputeRequests []SignedComputeRequest `json:"signed_compute_requests,omitempty"` } // SecretEntry is a single secret in the relay DON's response. From 021edff993d9a14b10cf11726e672f7d978741e7 Mon Sep 17 00:00:00 2001 From: Tejaswi Nadahalli Date: Mon, 1 Jun 2026 19:41:41 +0200 Subject: [PATCH 3/4] feat(confidentialrelay): add SignedComputeRequest signature-payload helper The relay DON verifies the F+1 Workflow DON signatures over a forwarded SignedComputeRequest by reconstructing the peerid domain-separated payload the nodes signed. Adds SignedComputeRequestSignaturePayload + the vendored prefix CONFIDENTIAL_COMPUTE_PAYLOAD_ (from CC util.GetConfidentialComputePayloadPrefix). The relay cannot import confidential-compute, so this lives here. --- .../confidentialrelay/computerequest.go | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/pkg/capabilities/v2/actions/confidentialrelay/computerequest.go b/pkg/capabilities/v2/actions/confidentialrelay/computerequest.go index 2acc54a988..cea4c2ae95 100644 --- a/pkg/capabilities/v2/actions/confidentialrelay/computerequest.go +++ b/pkg/capabilities/v2/actions/confidentialrelay/computerequest.go @@ -1,6 +1,10 @@ package confidentialrelay -import "crypto/sha256" +import ( + "crypto/sha256" + + "github.com/smartcontractkit/libocr/ragep2p/peeridhelper" +) // computeRequestDomainSeparator is vendored verbatim from confidential-compute // types.DomainSeparator. It MUST stay byte-identical to the source, or @@ -10,6 +14,21 @@ import "crypto/sha256" // (which can import this package). const computeRequestDomainSeparator = "CONFIDENTIAL_COMPUTE_PAYLOAD" +// signedComputeRequestSignaturePrefix is vendored verbatim from confidential-compute +// util.GetConfidentialComputePayloadPrefix(). Each Workflow DON node signs the peerid +// domain-separated payload over ComputeRequest.Hash() using this prefix; the relay DON +// reconstructs the same payload (via SignedComputeRequestSignaturePayload) to verify the +// F+1 signatures against the Workflow DON signer set. Note the trailing underscore: this +// is the signature prefix, distinct from computeRequestDomainSeparator (the hash prefix). +const signedComputeRequestSignaturePrefix = "CONFIDENTIAL_COMPUTE_PAYLOAD_" + +// SignedComputeRequestSignaturePayload reconstructs the exact payload a Workflow DON node +// signed over a ComputeRequest hash, so the relay DON can verify the signature with the +// node's public key. +func SignedComputeRequestSignaturePayload(computeRequestHash [32]byte) []byte { + return peeridhelper.MakePeerIDSignatureDomainSeparatedPayload(signedComputeRequestSignaturePrefix, computeRequestHash[:]) +} + // ComputeRequest is vendored from confidential-compute types.ComputeRequest. The // relay DON cannot import confidential-compute (the dependency runs the other way), // so the type and its canonical Hash are copied here. The enclave forwards the From a368226dadd54faed620735d42175606774c4850 Mon Sep 17 00:00:00 2001 From: Tejaswi Nadahalli Date: Thu, 11 Jun 2026 14:17:08 +0200 Subject: [PATCH 4/4] feat(confidentialrelay): add relay response bundle types Add SignedSecretsResponseBundle and SignedCapabilityResponseBundle, the gateway->enclave envelope for forwarding every relay node's signed response without the gateway merging, trusting, or deciding quorum over them. The enclave groups by response hash, verifies each signature against the relay-DON signer set, and accepts the result backed by F+1 valid distinct signers. Add a single Signature field to the per-node signed-result types (a node signs only its own response) and deprecate the Signatures array. The array is kept and still populated for backward compatibility until chainlink and confidential-compute migrate to Signature. --- .../v2/actions/confidentialrelay/types.go | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/pkg/capabilities/v2/actions/confidentialrelay/types.go b/pkg/capabilities/v2/actions/confidentialrelay/types.go index c9b4d67fb4..c55548ed5f 100644 --- a/pkg/capabilities/v2/actions/confidentialrelay/types.go +++ b/pkg/capabilities/v2/actions/confidentialrelay/types.go @@ -338,18 +338,50 @@ type RelayResponseSignature struct { Signature []byte `json:"signature"` } -// SignedSecretsResponseResult wraps a logical secrets response with the relay -// signatures that attest to it. +// SignedSecretsResponseResult is one relay-DON node's signed secrets response: +// the logical result plus that single node's signature over the response hash. +// A node signs only its own response, so it carries exactly one signature; the +// gateway forwards a SignedSecretsResponseBundle of these without merging or +// trusting them, and the enclave verifies each against the relay-DON signer set. type SignedSecretsResponseResult struct { - Result SecretsResponseResult `json:"result"` - Signatures []RelayResponseSignature `json:"signatures"` + Result SecretsResponseResult `json:"result"` + // Deprecated: use Signature. A relay node signs only its own response, so this + // array always carries exactly one entry. Retained for backward compatibility + // while chainlink and confidential-compute migrate to the single-signature + // field; it will be removed once nothing reads it. + Signatures []RelayResponseSignature `json:"signatures,omitempty"` + // Signature is this relay node's single signature over the response hash. + Signature RelayResponseSignature `json:"signature"` } -// SignedCapabilityResponseResult wraps a logical capability response with the -// relay signatures that attest to it. +// SignedCapabilityResponseResult is one relay-DON node's signed capability +// response: the logical result plus that single node's signature over the +// response hash. See SignedSecretsResponseResult for the trust model. type SignedCapabilityResponseResult struct { - Result CapabilityResponseResult `json:"result"` - Signatures []RelayResponseSignature `json:"signatures"` + Result CapabilityResponseResult `json:"result"` + // Deprecated: use Signature. A relay node signs only its own response, so this + // array always carries exactly one entry. Retained for backward compatibility + // while chainlink and confidential-compute migrate to the single-signature + // field; it will be removed once nothing reads it. + Signatures []RelayResponseSignature `json:"signatures,omitempty"` + // Signature is this relay node's single signature over the response hash. + Signature RelayResponseSignature `json:"signature"` +} + +// SignedSecretsResponseBundle is the gateway's response to the enclave: the +// unverified set of per-node signed responses the gateway collected. The gateway +// makes no quorum decision and holds no signer keys; it is a dumb fan-in. The +// enclave groups the responses by their canonical hash, verifies each signature +// against the relay-DON signer set, and accepts the result backed by F+1 valid +// distinct signers. Invalid or foreign signatures are skipped, not fatal. +type SignedSecretsResponseBundle struct { + Responses []SignedSecretsResponseResult `json:"responses"` +} + +// SignedCapabilityResponseBundle is the gateway's response to the enclave for a +// capability execution. See SignedSecretsResponseBundle for the trust model. +type SignedCapabilityResponseBundle struct { + Responses []SignedCapabilityResponseResult `json:"responses"` } // RelayResponseSignaturePayload prepares a relay response hash for signing with