Skip to content
Merged
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
6 changes: 5 additions & 1 deletion core/capabilities/vault/capability.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,13 +269,17 @@ func NewCapability(
if err != nil {
return nil, fmt.Errorf("could not create request batch size limiter: %w", err)
}
ciphertextLimiter, err := limits.MakeUpperBoundLimiter(limitsFactory, cresettings.Default.VaultCiphertextSizeLimit)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Close ciphertext limiter in capability teardown

The new ciphertextLimiter created for RequestValidator is never closed, while Close() only shuts down MaxRequestBatchSizeLimiter. In deployments where the capability is restarted (tests, reconfiguration, process lifecycle), this leaves the limiter's underlying resources/subscriptions alive and can accumulate goroutines or watchers over time. Add MaxCiphertextLengthLimiter.Close() to (*Capability).Close() alongside the existing limiter cleanup.

Useful? React with 👍 / 👎.

if err != nil {
return nil, fmt.Errorf("could not create ciphertext size limiter: %w", err)
}
return &Capability{
lggr: logger.Named(lggr, "VaultCapability"),
clock: clock,
expiresAfter: expiresAfter,
handler: handler,
capabilitiesRegistry: capabilitiesRegistry,
publicKey: publicKey,
RequestValidator: NewRequestValidator(limiter),
RequestValidator: NewRequestValidator(limiter, ciphertextLimiter),
}, nil
}
26 changes: 25 additions & 1 deletion core/capabilities/vault/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import (
"github.com/smartcontractkit/tdh2/go/tdh2/tdh2easy"

vaultcommon "github.com/smartcontractkit/chainlink-common/pkg/capabilities/actions/vault"
pkgconfig "github.com/smartcontractkit/chainlink-common/pkg/config"
"github.com/smartcontractkit/chainlink-common/pkg/settings/limits"
"github.com/smartcontractkit/chainlink/v2/core/capabilities/vault/vaulttypes"
)

type RequestValidator struct {
MaxRequestBatchSizeLimiter limits.BoundLimiter[int]
MaxCiphertextLengthLimiter limits.BoundLimiter[pkgconfig.Size]
}

func (r *RequestValidator) ValidateCreateSecretsRequest(publicKey *tdh2easy.PublicKey, request *vaultcommon.CreateSecretsRequest) error {
Expand Down Expand Up @@ -60,6 +62,9 @@ func (r *RequestValidator) validateWriteRequest(publicKey *tdh2easy.PublicKey, i
if req.EncryptedValue == "" {
return errors.New("secret must have encrypted value set at index " + strconv.Itoa(idx) + ":" + req.Id.String())
}
if err := r.validateCiphertextSize(req.EncryptedValue); err != nil {
return fmt.Errorf("secret encrypted value at index %d is invalid: %w", idx, err)
}
err := EnsureRightLabelOnSecret(publicKey, req.EncryptedValue, req.Id.Owner)
if err != nil {
return errors.New("Encrypted Secret at index [" + strconv.Itoa(idx) + "] doesn't have owner as the label. Error: " + err.Error())
Expand All @@ -75,6 +80,21 @@ func (r *RequestValidator) validateWriteRequest(publicKey *tdh2easy.PublicKey, i
return nil
}

func (r *RequestValidator) validateCiphertextSize(encryptedValue string) error {
rawCiphertext, err := hex.DecodeString(encryptedValue)
if err != nil {
return fmt.Errorf("failed to decode encrypted value: %w", err)
}
if err := r.MaxCiphertextLengthLimiter.Check(context.Background(), pkgconfig.Size(len(rawCiphertext))*pkgconfig.Byte); err != nil {
var errBoundLimited limits.ErrorBoundLimited[pkgconfig.Size]
if errors.As(err, &errBoundLimited) {
return fmt.Errorf("ciphertext size exceeds maximum allowed size: %s", errBoundLimited.Limit)
}
return fmt.Errorf("failed to check ciphertext size limit: %w", err)
}
return nil
}

func (r *RequestValidator) ValidateGetSecretsRequest(request *vaultcommon.GetSecretsRequest) error {
if len(request.Requests) == 0 {
return errors.New("no GetSecret request specified in request")
Expand Down Expand Up @@ -129,9 +149,13 @@ func (r *RequestValidator) ValidateDeleteSecretsRequest(request *vaultcommon.Del
return nil
}

func NewRequestValidator(maxRequestBatchSizeLimiter limits.BoundLimiter[int]) *RequestValidator {
func NewRequestValidator(
maxRequestBatchSizeLimiter limits.BoundLimiter[int],
maxCiphertextLengthLimiter limits.BoundLimiter[pkgconfig.Size],
) *RequestValidator {
return &RequestValidator{
MaxRequestBatchSizeLimiter: maxRequestBatchSizeLimiter,
MaxCiphertextLengthLimiter: maxCiphertextLengthLimiter,
}
}

Expand Down
96 changes: 96 additions & 0 deletions core/capabilities/vault/validator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package vault

import (
"encoding/hex"
"testing"

"github.com/stretchr/testify/require"

vaultcommon "github.com/smartcontractkit/chainlink-common/pkg/capabilities/actions/vault"
pkgconfig "github.com/smartcontractkit/chainlink-common/pkg/config"
"github.com/smartcontractkit/chainlink-common/pkg/settings/limits"
)

func TestRequestValidator_CiphertextSizeLimit(t *testing.T) {
validator := NewRequestValidator(
limits.NewUpperBoundLimiter(10),
limits.NewUpperBoundLimiter[pkgconfig.Size](10*pkgconfig.Byte),
)

id := &vaultcommon.SecretIdentifier{
Key: "key",
Namespace: "namespace",
Owner: "0x1111111111111111111111111111111111111111",
}

tests := []struct {
name string
call func(*testing.T, *RequestValidator, string) error
value string
errSubstr string
}{
{
name: "create accepts ciphertext at the limit",
call: func(t *testing.T, validator *RequestValidator, value string) error {
return validator.ValidateCreateSecretsRequest(nil, &vaultcommon.CreateSecretsRequest{
RequestId: "request-id",
EncryptedSecrets: []*vaultcommon.EncryptedSecret{
{Id: id, EncryptedValue: value},
},
})
},
value: hex.EncodeToString(make([]byte, 10)),
},
{
name: "create rejects ciphertext above the limit",
call: func(t *testing.T, validator *RequestValidator, value string) error {
return validator.ValidateCreateSecretsRequest(nil, &vaultcommon.CreateSecretsRequest{
RequestId: "request-id",
EncryptedSecrets: []*vaultcommon.EncryptedSecret{
{Id: id, EncryptedValue: value},
},
})
},
value: hex.EncodeToString(make([]byte, 11)),
errSubstr: "ciphertext size exceeds maximum allowed size",
},
{
name: "update accepts ciphertext at the limit",
call: func(t *testing.T, validator *RequestValidator, value string) error {
return validator.ValidateUpdateSecretsRequest(nil, &vaultcommon.UpdateSecretsRequest{
RequestId: "request-id",
EncryptedSecrets: []*vaultcommon.EncryptedSecret{
{Id: id, EncryptedValue: value},
},
})
},
value: hex.EncodeToString(make([]byte, 10)),
},
{
name: "update rejects ciphertext above the limit",
call: func(t *testing.T, validator *RequestValidator, value string) error {
return validator.ValidateUpdateSecretsRequest(nil, &vaultcommon.UpdateSecretsRequest{
RequestId: "request-id",
EncryptedSecrets: []*vaultcommon.EncryptedSecret{
{Id: id, EncryptedValue: value},
},
})
},
value: hex.EncodeToString(make([]byte, 11)),
errSubstr: "ciphertext size exceeds maximum allowed size",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.call(t, validator, tt.value)
if tt.errSubstr == "" {
require.NoError(t, err)
return
}

require.Error(t, err)
require.ErrorContains(t, err, tt.errSubstr)
})
}
}
6 changes: 5 additions & 1 deletion core/services/gateway/handlers/vault/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ func NewHandler(methodConfig json.RawMessage, donConfig *config.DONConfig, don g
if err != nil {
return nil, fmt.Errorf("could not create request batch size limiter: %w", err)
}
ciphertextLimiter, err := limits.MakeUpperBoundLimiter(limitsFactory, cresettings.Default.VaultCiphertextSizeLimit)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Release ciphertext limiter in gateway handler close path

This adds a second limiter (ciphertextLimiter) to the handler's validator, but (*handler).Close() still only closes writeMethodsEnabled and MaxRequestBatchSizeLimiter. If the gateway handler is started/stopped repeatedly, the unclosed ciphertext limiter can leak internal limiter resources and degrade long-running processes. Include MaxCiphertextLengthLimiter.Close() in the errors.Join(...) cleanup list.

Useful? React with 👍 / 👎.

if err != nil {
return nil, fmt.Errorf("could not create ciphertext size limiter: %w", err)
}

writeMethodsEnabled, err := limits.MakeGateLimiter(limitsFactory, cresettings.Default.GatewayVaultManagementEnabled)
if err != nil {
Expand All @@ -218,7 +222,7 @@ func NewHandler(methodConfig json.RawMessage, donConfig *config.DONConfig, don g
metrics: metrics,
aggregator: &baseAggregator{capabilitiesRegistry: capabilitiesRegistry},
clock: clock,
RequestValidator: vaultcap.NewRequestValidator(limiter),
RequestValidator: vaultcap.NewRequestValidator(limiter, ciphertextLimiter),
}, nil
}

Expand Down
Loading