Skip to content

Commit 4ebe6ba

Browse files
EnsureRightLabelOnSecret: dual label support for workflow_owner and org_id (#21680)
* EnsureRightLabelOnSecret: dual label support for workflow_owner and org_id Update EnsureRightLabelOnSecret to accept explicit workflowOwner and orgID parameters, supporting both ETH address (left-padded) and SHA256(org_id) label encodings. Centralize all label utilities in vaultutils/labels.go. Made-with: Cursor * fix testifylint: use require.Error for error assertions Made-with: Cursor * fix: use dedicated label functions for workflowOwner and orgID Replace generic OwnerToLabel (which auto-detected type via IsHexAddress) with WorkflowOwnerToLabel and OrgIDToLabel to preserve backward compat with callers that pass non-address strings through HexToAddress. Made-with: Cursor * gate orgID label check behind VaultOrgIdAsSecretOwnerEnabled limiter Wire workflowOwner and orgID from request-level fields on GetSecretsRequest/CreateSecretsRequest/UpdateSecretsRequest instead of secretRequest.Id.Owner. Only check orgID label when the gate limiter is enabled; rename OrgId -> OrgID per Go naming convention. Made-with: Cursor * fix goimports ordering in system-tests vault.go Made-with: Cursor
1 parent 7fcfab4 commit 4ebe6ba

6 files changed

Lines changed: 560 additions & 34 deletions

File tree

core/capabilities/vault/validator.go

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import (
77
"fmt"
88
"strconv"
99

10-
"github.com/ethereum/go-ethereum/common"
1110
"github.com/smartcontractkit/tdh2/go/tdh2/tdh2easy"
1211

1312
vaultcommon "github.com/smartcontractkit/chainlink-common/pkg/capabilities/actions/vault"
1413
pkgconfig "github.com/smartcontractkit/chainlink-common/pkg/config"
1514
"github.com/smartcontractkit/chainlink-common/pkg/settings/limits"
1615
"github.com/smartcontractkit/chainlink/v2/core/capabilities/vault/vaulttypes"
16+
"github.com/smartcontractkit/chainlink/v2/core/capabilities/vault/vaultutils"
1717
)
1818

1919
type RequestValidator struct {
@@ -65,7 +65,7 @@ func (r *RequestValidator) validateWriteRequest(publicKey *tdh2easy.PublicKey, i
6565
if err := r.validateCiphertextSize(req.EncryptedValue); err != nil {
6666
return fmt.Errorf("secret encrypted value at index %d is invalid: %w", idx, err)
6767
}
68-
err := EnsureRightLabelOnSecret(publicKey, req.EncryptedValue, req.Id.Owner)
68+
err := EnsureRightLabelOnSecret(publicKey, req.EncryptedValue, req.Id.Owner, "")
6969
if err != nil {
7070
return errors.New("Encrypted Secret at index [" + strconv.Itoa(idx) + "] doesn't have owner as the label. Error: " + err.Error())
7171
}
@@ -159,27 +159,40 @@ func NewRequestValidator(
159159
}
160160
}
161161

162-
func EnsureRightLabelOnSecret(publicKey *tdh2easy.PublicKey, secret, owner string) error {
162+
// EnsureRightLabelOnSecret verifies that the TDH2 ciphertext label matches either the
163+
// workflowOwner (Ethereum address, left-padded) or the orgID (SHA256 hash). Either
164+
// parameter can be empty to skip that check. The function succeeds if the label matches
165+
// at least one non-empty owner.
166+
func EnsureRightLabelOnSecret(publicKey *tdh2easy.PublicKey, secret string, workflowOwner string, orgID string) error {
163167
cipherText := &tdh2easy.Ciphertext{}
164168
cipherBytes, err := hex.DecodeString(secret)
165169
if err != nil {
166170
return errors.New("failed to decode encrypted value:" + err.Error())
167171
}
168172
if publicKey == nil {
169-
// Public key can be nil if gateway cache isn't populated yet(immediately after gateway reboots)
170-
// Ok to not validate in such cases, since this validation also runs on Vault Nodes
173+
// Public key can be nil if gateway cache isn't populated yet (immediately after gateway reboots).
174+
// Ok to not validate in such cases, since this validation also runs on Vault Nodes.
171175
return nil
172176
}
173177
err = cipherText.UnmarshalVerify(cipherBytes, publicKey)
174178
if err != nil {
175179
return errors.New("failed to verify encrypted value:" + err.Error())
176180
}
177181
secretLabel := cipherText.Label()
178-
ownerAddr := common.HexToAddress(owner)
179-
var ownerLabel [32]byte
180-
copy(ownerLabel[12:], ownerAddr.Bytes()) // left-pad with 12 zero
181-
if secretLabel != ownerLabel {
182-
return errors.New("secret label [" + hex.EncodeToString(secretLabel[:]) + "] does not match owner label [" + hex.EncodeToString(ownerLabel[:]) + "]")
182+
183+
if workflowOwner != "" {
184+
expected := vaultutils.WorkflowOwnerToLabel(workflowOwner)
185+
if secretLabel == expected {
186+
return nil
187+
}
183188
}
184-
return nil
189+
190+
if orgID != "" {
191+
expected := vaultutils.OrgIDToLabel(orgID)
192+
if secretLabel == expected {
193+
return nil
194+
}
195+
}
196+
197+
return errors.New("secret label [" + hex.EncodeToString(secretLabel[:]) + "] does not match any of the provided owner labels")
185198
}

core/capabilities/vault/validator_test.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,214 @@
11
package vault
22

33
import (
4+
"crypto/sha256"
45
"encoding/hex"
56
"testing"
67

8+
"github.com/ethereum/go-ethereum/common"
9+
"github.com/smartcontractkit/tdh2/go/tdh2/tdh2easy"
10+
"github.com/stretchr/testify/assert"
711
"github.com/stretchr/testify/require"
812

913
vaultcommon "github.com/smartcontractkit/chainlink-common/pkg/capabilities/actions/vault"
1014
pkgconfig "github.com/smartcontractkit/chainlink-common/pkg/config"
1115
"github.com/smartcontractkit/chainlink-common/pkg/settings/limits"
16+
"github.com/smartcontractkit/chainlink/v2/core/capabilities/vault/vaultutils"
1217
)
1318

19+
func generateTestKeys(t *testing.T) (*tdh2easy.PublicKey, []*tdh2easy.PrivateShare) {
20+
t.Helper()
21+
_, pk, shares, err := tdh2easy.GenerateKeys(1, 3)
22+
require.NoError(t, err)
23+
return pk, shares
24+
}
25+
26+
func encryptWithEthAddressLabel(t *testing.T, pk *tdh2easy.PublicKey, owner string) string {
27+
t.Helper()
28+
encrypted, err := vaultutils.EncryptSecretWithWorkflowOwner("test-secret", pk, common.HexToAddress(owner))
29+
require.NoError(t, err)
30+
return encrypted
31+
}
32+
33+
func encryptWithOrgIDLabel(t *testing.T, pk *tdh2easy.PublicKey, orgID string) string {
34+
t.Helper()
35+
encrypted, err := vaultutils.EncryptSecretWithOrgID("test-secret", pk, orgID)
36+
require.NoError(t, err)
37+
return encrypted
38+
}
39+
40+
func TestWorkflowOwnerToLabel(t *testing.T) {
41+
t.Run("ethereum address with 0x prefix", func(t *testing.T) {
42+
addr := "0x0001020304050607080900010203040506070809"
43+
label := vaultutils.WorkflowOwnerToLabel(addr)
44+
45+
var expected [32]byte
46+
copy(expected[12:], common.HexToAddress(addr).Bytes())
47+
assert.Equal(t, expected, label)
48+
})
49+
50+
t.Run("ethereum address without 0x prefix", func(t *testing.T) {
51+
addr := "0001020304050607080900010203040506070809"
52+
label := vaultutils.WorkflowOwnerToLabel(addr)
53+
54+
var expected [32]byte
55+
copy(expected[12:], common.HexToAddress(addr).Bytes())
56+
assert.Equal(t, expected, label)
57+
})
58+
59+
t.Run("checksummed ethereum address", func(t *testing.T) {
60+
addr := "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B"
61+
label := vaultutils.WorkflowOwnerToLabel(addr)
62+
63+
var expected [32]byte
64+
copy(expected[12:], common.HexToAddress(addr).Bytes())
65+
assert.Equal(t, expected, label)
66+
})
67+
}
68+
69+
func TestOrgIDToLabel(t *testing.T) {
70+
t.Run("org_id produces SHA256 label", func(t *testing.T) {
71+
orgID := "org_2xAbCdEfGhIjKlMnOpQrStUvWxYz"
72+
label := vaultutils.OrgIDToLabel(orgID)
73+
74+
expected := sha256.Sum256([]byte(orgID))
75+
assert.Equal(t, expected, label)
76+
})
77+
78+
t.Run("short string", func(t *testing.T) {
79+
orgID := "my-org-id"
80+
label := vaultutils.OrgIDToLabel(orgID)
81+
82+
expected := sha256.Sum256([]byte(orgID))
83+
assert.Equal(t, expected, label)
84+
})
85+
}
86+
87+
func TestEnsureRightLabelOnSecret_WorkflowOwnerOnly(t *testing.T) {
88+
pk, _ := generateTestKeys(t)
89+
owner := "0x0001020304050607080900010203040506070809"
90+
secret := encryptWithEthAddressLabel(t, pk, owner)
91+
92+
err := EnsureRightLabelOnSecret(pk, secret, owner, "")
93+
assert.NoError(t, err)
94+
}
95+
96+
func TestEnsureRightLabelOnSecret_OrgIDOnly(t *testing.T) {
97+
pk, _ := generateTestKeys(t)
98+
orgID := "org_2xAbCdEfGhIjKlMnOpQrStUvWxYz"
99+
secret := encryptWithOrgIDLabel(t, pk, orgID)
100+
101+
err := EnsureRightLabelOnSecret(pk, secret, "", orgID)
102+
assert.NoError(t, err)
103+
}
104+
105+
func TestEnsureRightLabelOnSecret_DualMatchesWorkflowOwner(t *testing.T) {
106+
pk, _ := generateTestKeys(t)
107+
ethAddr := "0x0001020304050607080900010203040506070809"
108+
orgID := "org_2xAbCdEfGhIjKlMnOpQrStUvWxYz"
109+
secret := encryptWithEthAddressLabel(t, pk, ethAddr)
110+
111+
err := EnsureRightLabelOnSecret(pk, secret, ethAddr, orgID)
112+
assert.NoError(t, err)
113+
}
114+
115+
func TestEnsureRightLabelOnSecret_DualMatchesOrgID(t *testing.T) {
116+
pk, _ := generateTestKeys(t)
117+
ethAddr := "0x0001020304050607080900010203040506070809"
118+
orgID := "org_2xAbCdEfGhIjKlMnOpQrStUvWxYz"
119+
secret := encryptWithOrgIDLabel(t, pk, orgID)
120+
121+
err := EnsureRightLabelOnSecret(pk, secret, ethAddr, orgID)
122+
assert.NoError(t, err)
123+
}
124+
125+
func TestEnsureRightLabelOnSecret_NeitherMatches(t *testing.T) {
126+
pk, _ := generateTestKeys(t)
127+
ethAddr := "0x0001020304050607080900010203040506070809"
128+
wrongAddr := "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
129+
wrongOrgID := "org_wrong"
130+
secret := encryptWithEthAddressLabel(t, pk, ethAddr)
131+
132+
err := EnsureRightLabelOnSecret(pk, secret, wrongAddr, wrongOrgID)
133+
require.Error(t, err)
134+
assert.Contains(t, err.Error(), "does not match any of the provided owner labels")
135+
}
136+
137+
func TestEnsureRightLabelOnSecret_BothEmpty(t *testing.T) {
138+
pk, _ := generateTestKeys(t)
139+
ethAddr := "0x0001020304050607080900010203040506070809"
140+
secret := encryptWithEthAddressLabel(t, pk, ethAddr)
141+
142+
err := EnsureRightLabelOnSecret(pk, secret, "", "")
143+
require.Error(t, err)
144+
assert.Contains(t, err.Error(), "does not match any of the provided owner labels")
145+
}
146+
147+
func TestEnsureRightLabelOnSecret_NilPublicKey(t *testing.T) {
148+
pk, _ := generateTestKeys(t)
149+
ethAddr := "0x0001020304050607080900010203040506070809"
150+
secret := encryptWithEthAddressLabel(t, pk, ethAddr)
151+
152+
err := EnsureRightLabelOnSecret(nil, secret, ethAddr, "")
153+
assert.NoError(t, err)
154+
}
155+
156+
func TestEnsureRightLabelOnSecret_InvalidHexSecret(t *testing.T) {
157+
pk, _ := generateTestKeys(t)
158+
159+
err := EnsureRightLabelOnSecret(pk, "not-valid-hex!", "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "")
160+
require.Error(t, err)
161+
assert.Contains(t, err.Error(), "failed to decode encrypted value")
162+
}
163+
164+
func TestEnsureRightLabelOnSecret_InvalidCiphertext(t *testing.T) {
165+
pk, _ := generateTestKeys(t)
166+
167+
err := EnsureRightLabelOnSecret(pk, hex.EncodeToString([]byte("garbage")), "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "")
168+
require.Error(t, err)
169+
assert.Contains(t, err.Error(), "failed to verify encrypted value")
170+
}
171+
172+
func TestEnsureRightLabelOnSecret_WrongPublicKey(t *testing.T) {
173+
pk, _ := generateTestKeys(t)
174+
wrongPK, _ := generateTestKeys(t)
175+
ethAddr := "0x0001020304050607080900010203040506070809"
176+
secret := encryptWithEthAddressLabel(t, pk, ethAddr)
177+
178+
err := EnsureRightLabelOnSecret(wrongPK, secret, ethAddr, "")
179+
require.Error(t, err)
180+
assert.Contains(t, err.Error(), "failed to verify encrypted value")
181+
}
182+
183+
func TestEnsureRightLabelOnSecret_BackwardCompatSingleOwner(t *testing.T) {
184+
pk, _ := generateTestKeys(t)
185+
owner := "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B"
186+
secret := encryptWithEthAddressLabel(t, pk, owner)
187+
188+
err := EnsureRightLabelOnSecret(pk, secret, owner, "")
189+
assert.NoError(t, err)
190+
}
191+
192+
func TestEnsureRightLabelOnSecret_LegacySecretReadViaNewFlow(t *testing.T) {
193+
pk, _ := generateTestKeys(t)
194+
workflowOwner := "0x0001020304050607080900010203040506070809"
195+
orgID := "org_2xAbCdEfGhIjKlMnOpQrStUvWxYz"
196+
197+
secret := encryptWithEthAddressLabel(t, pk, workflowOwner)
198+
err := EnsureRightLabelOnSecret(pk, secret, workflowOwner, orgID)
199+
assert.NoError(t, err)
200+
}
201+
202+
func TestEnsureRightLabelOnSecret_NewSecretReadViaNewFlow(t *testing.T) {
203+
pk, _ := generateTestKeys(t)
204+
orgID := "org_2xAbCdEfGhIjKlMnOpQrStUvWxYz"
205+
workflowOwner := "0x0001020304050607080900010203040506070809"
206+
207+
secret := encryptWithOrgIDLabel(t, pk, orgID)
208+
err := EnsureRightLabelOnSecret(pk, secret, workflowOwner, orgID)
209+
assert.NoError(t, err)
210+
}
211+
14212
func TestRequestValidator_CiphertextSizeLimit(t *testing.T) {
15213
validator := NewRequestValidator(
16214
limits.NewUpperBoundLimiter(10),
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package vaultutils
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/hex"
6+
"fmt"
7+
8+
"github.com/ethereum/go-ethereum/common"
9+
"github.com/smartcontractkit/tdh2/go/tdh2/tdh2easy"
10+
)
11+
12+
// WorkflowOwnerToLabel converts a workflow owner string to a 32-byte TDH2 ciphertext
13+
// label using the Ethereum address encoding: 12 zero bytes followed by the 20-byte address.
14+
// This matches the legacy label format used when secrets are encrypted with a workflow owner.
15+
func WorkflowOwnerToLabel(owner string) [32]byte {
16+
var label [32]byte
17+
addr := common.HexToAddress(owner)
18+
copy(label[12:], addr.Bytes())
19+
return label
20+
}
21+
22+
// OrgIDToLabel converts an org_id string to a 32-byte TDH2 ciphertext label
23+
// using SHA256 hashing.
24+
func OrgIDToLabel(orgID string) [32]byte {
25+
return sha256.Sum256([]byte(orgID))
26+
}
27+
28+
// EncryptSecretWithWorkflowOwner encrypts a secret using a TDH2 public key with a label
29+
// derived from a workflow owner's Ethereum address (left-padded to 32 bytes).
30+
func EncryptSecretWithWorkflowOwner(secret string, masterPublicKey *tdh2easy.PublicKey, owner common.Address) (string, error) {
31+
var label [32]byte
32+
copy(label[12:], owner.Bytes())
33+
return encryptWithLabel(secret, masterPublicKey, label)
34+
}
35+
36+
// EncryptSecretWithOrgID encrypts a secret using a TDH2 public key with a label
37+
// derived from an org_id (SHA256 hash of the org_id string).
38+
func EncryptSecretWithOrgID(secret string, masterPublicKey *tdh2easy.PublicKey, orgID string) (string, error) {
39+
label := sha256.Sum256([]byte(orgID))
40+
return encryptWithLabel(secret, masterPublicKey, label)
41+
}
42+
43+
func encryptWithLabel(secret string, masterPublicKey *tdh2easy.PublicKey, label [32]byte) (string, error) {
44+
cipher, err := tdh2easy.EncryptWithLabel(masterPublicKey, []byte(secret), label)
45+
if err != nil {
46+
return "", fmt.Errorf("failed to encrypt secret: %w", err)
47+
}
48+
cipherBytes, err := cipher.Marshal()
49+
if err != nil {
50+
return "", fmt.Errorf("failed to marshal encrypted secret: %w", err)
51+
}
52+
return hex.EncodeToString(cipherBytes), nil
53+
}

0 commit comments

Comments
 (0)