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
97 changes: 97 additions & 0 deletions pkg/capabilities/v2/actions/confidentialrelay/computerequest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package confidentialrelay

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
// 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"

// 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
// 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"`
Comment on lines +42 to +46

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Intentional. This type is vendored verbatim from confidential-compute types.ComputeRequest, where the tag is json:"CiphertextNames" (capital C). The relay must marshal and unmarshal exactly what the enclave emits, so the tag has to match the source byte for byte. The capital C is a quirk in the source, but matching it is what preserves wire compatibility; changing it here is what would break it. Leaving as-is.

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"`
}
Original file line number Diff line number Diff line change
@@ -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())
}
55 changes: 47 additions & 8 deletions pkg/capabilities/v2/actions/confidentialrelay/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ type SecretsRequestParams struct {
// that omit the field. When present, it is validated and hash-bound.
EnclaveConfig *EnclaveConfig `json:"enclave_config,omitempty"`
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.
Expand Down Expand Up @@ -331,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
Expand Down
Loading