From 0c701f86795c68fa3d9ba87f42fb6c9ef01b95ce Mon Sep 17 00:00:00 2001 From: Tejaswi Nadahalli Date: Mon, 1 Jun 2026 17:08:33 +0200 Subject: [PATCH 1/3] 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/3] 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/3] 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