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
20 changes: 19 additions & 1 deletion cmd/secrets/common/browser_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,11 @@ func (h *Handler) ExecuteBrowserVaultAuthorization(ctx context.Context, method s

// postVaultGatewayWithBearer POSTs the digest-bound JSON-RPC body with the vault JWT and parses the gateway response.
func (h *Handler) postVaultGatewayWithBearer(method string, requestBody []byte, accessToken string) error {
requestID, err := jsonRPCRequestID(requestBody)
if err != nil {
return err
}

ui.Dim("Submitting request to vault gateway...")
respBody, status, err := h.Gw.PostWithBearer(requestBody, accessToken)
if err != nil {
Expand All @@ -248,5 +253,18 @@ func (h *Handler) postVaultGatewayWithBearer(method string, requestBody []byte,
if status != http.StatusOK {
return fmt.Errorf("gateway returned a non-200 status code: status_code=%d, body=%s", status, respBody)
}
return h.ParseVaultGatewayResponse(method, respBody)
return h.ParseVaultGatewayResponse(method, requestID, respBody)
}

func jsonRPCRequestID(body []byte) (string, error) {
var req struct {
ID string `json:"id"`
}
if err := json.Unmarshal(body, &req); err != nil {
return "", fmt.Errorf("failed to parse jsonrpc request id: %w", err)
}
if req.ID == "" {
return "", fmt.Errorf("jsonrpc request id is empty")
}
return req.ID, nil
}
6 changes: 3 additions & 3 deletions cmd/secrets/common/browser_flow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func TestPostVaultGatewayWithBearer_ListParsesResponse(t *testing.T) {
},
}

err := h.postVaultGatewayWithBearer(vaulttypes.MethodSecretsList, []byte(`{}`), "t")
err := h.postVaultGatewayWithBearer(vaulttypes.MethodSecretsList, []byte(`{"jsonrpc":"2.0","id":"1","method":"x"}`), "t")
w.Close()
os.Stdout = oldStdout
var out strings.Builder
Expand All @@ -115,7 +115,7 @@ func TestPostVaultGatewayWithBearer_GatewayNon200(t *testing.T) {
},
}

err := h.postVaultGatewayWithBearer(vaulttypes.MethodSecretsDelete, []byte(`{}`), "t")
err := h.postVaultGatewayWithBearer(vaulttypes.MethodSecretsDelete, []byte(`{"jsonrpc":"2.0","id":"1","method":"x"}`), "t")
require.Error(t, err)
assert.Contains(t, err.Error(), "non-200")
assert.Contains(t, err.Error(), "403")
Expand All @@ -129,7 +129,7 @@ func TestPostVaultGatewayWithBearer_InvalidJSONRPC(t *testing.T) {
},
}

err := h.postVaultGatewayWithBearer(vaulttypes.MethodSecretsUpdate, []byte(`{}`), "t")
err := h.postVaultGatewayWithBearer(vaulttypes.MethodSecretsUpdate, []byte(`{"jsonrpc":"2.0","id":"1","method":"x"}`), "t")
require.Error(t, err)
assert.Contains(t, err.Error(), "unmarshal")
}
14 changes: 9 additions & 5 deletions cmd/secrets/common/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@ func (h *Handler) Execute(
if status != http.StatusOK {
return fmt.Errorf("gateway returned a non-200 status code: status_code=%d, body=%s", status, respBody)
}
return h.ParseVaultGatewayResponse(method, respBody)
return h.ParseVaultGatewayResponse(method, requestID, respBody)
}

if txOut == nil && allowlisted {
Expand Down Expand Up @@ -683,10 +683,10 @@ func (h *Handler) Execute(
return nil
}

// ParseVaultGatewayResponse parses the JSON-RPC response, decodes the SignedOCRResponse payload
// into the appropriate proto type (CreateSecretsResponse, UpdateSecretsResponse, DeleteSecretsResponse),
// and logs one line per secret with id/owner/namespace/success/error.
func (h *Handler) ParseVaultGatewayResponse(method string, respBody []byte) error {
// ParseVaultGatewayResponse parses the JSON-RPC response, optionally verifies OCR signatures
// and the JSON-RPC response id, decodes the SignedOCRResponse payload into the appropriate proto
// type, and logs one line per secret with id/owner/namespace/success/error.
func (h *Handler) ParseVaultGatewayResponse(method, requestID string, respBody []byte) error {
// Unmarshal JSON-RPC envelope with SignedOCRResponse result
var rpcResp jsonrpc2.Response[vaulttypes.SignedOCRResponse]
if err := json.Unmarshal(respBody, &rpcResp); err != nil {
Expand All @@ -699,6 +699,10 @@ func (h *Handler) ParseVaultGatewayResponse(method string, respBody []byte) erro
return fmt.Errorf("gateway returned JSON-RPC error: %s", string(b))
}

if err := h.verifyVaultGatewayResponse(h.execCtx, &rpcResp, requestID); err != nil {
return err
}

// Ensure we have a result payload
if len(rpcResp.Result.Payload) == 0 {
return fmt.Errorf("empty SignedOCRResponse payload")
Expand Down
31 changes: 18 additions & 13 deletions cmd/secrets/common/parse_response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package common

import (
"bytes"
"context"
"encoding/json"
"io"
"os"
Expand Down Expand Up @@ -41,7 +42,11 @@ func encodeRPCBodyFromPayload(payload []byte) []byte {

func newTestHandler(buf *bytes.Buffer) *Handler {
logger := zerolog.New(buf)
return &Handler{Log: &logger}
return &Handler{
Log: &logger,
execCtx: context.Background(),
skipVaultValidation: true,
}
}

// Build the payload using the real proto types
Expand Down Expand Up @@ -129,7 +134,7 @@ func TestParseVaultGatewayResponse_Create_LogsPerItem(t *testing.T) {
h := newTestHandler(&buf)

body := encodeRPCBodyFromPayload(buildCreatePayloadProto(t))
if err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsCreate, body); err != nil {
if err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsCreate, "", body); err != nil {
t.Fatalf("unexpected error: %v", err)
}

Expand Down Expand Up @@ -174,7 +179,7 @@ func TestParseVaultGatewayResponse_Update_Success(t *testing.T) {
h := newTestHandler(nil)

body := encodeRPCBodyFromPayload(buildUpdatePayloadProto(t))
if err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsUpdate, body); err != nil {
if err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsUpdate, "", body); err != nil {
t.Fatalf("unexpected error: %v", err)
}

Expand Down Expand Up @@ -203,7 +208,7 @@ func TestParseVaultGatewayResponse_Delete_Success(t *testing.T) {
h := newTestHandler(&buf)

body := encodeRPCBodyFromPayload(buildDeletePayloadProto(t))
if err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsDelete, body); err != nil {
if err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsDelete, "", body); err != nil {
t.Fatalf("unexpected error: %v", err)
}

Expand All @@ -226,7 +231,7 @@ func TestParseVaultGatewayResponse_JSONRPCError(t *testing.T) {
h := newTestHandler(&buf)

body := encodeRPCBodyFromError(-32000, "upstream failed")
err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsCreate, body)
err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsCreate, "", body)
if err == nil || !strings.Contains(err.Error(), "gateway returned JSON-RPC error") ||
!strings.Contains(err.Error(), "upstream failed") {
t.Fatalf("expected JSON-RPC error surfaced, got: %v", err)
Expand All @@ -239,7 +244,7 @@ func TestParseVaultGatewayResponse_EmptyPayload(t *testing.T) {

// Omit payload entirely -> handler should report "empty SignedOCRResponse payload"
raw := encodeRPCBodyFromPayload(nil)
err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsUpdate, raw)
err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsUpdate, "", raw)
if err == nil || !strings.Contains(err.Error(), "empty SignedOCRResponse payload") {
t.Fatalf("expected empty payload error, got: %v", err)
}
Expand All @@ -250,7 +255,7 @@ func TestParseVaultGatewayResponse_MalformedTopLevelJSON(t *testing.T) {
h := newTestHandler(&buf)

raw := []byte(`{"jsonrpc":"2.0","id":"1","result": this is not valid}`)
err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsUpdate, raw)
err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsUpdate, "", raw)
if err == nil || !strings.Contains(err.Error(), "failed to unmarshal JSON-RPC response") {
t.Fatalf("expected unmarshal error, got: %v", err)
}
Expand All @@ -262,7 +267,7 @@ func TestParseVaultGatewayResponse_BadPayloadForCreate(t *testing.T) {

// Wrong shape for the proto: responses should be an array.
raw := encodeRPCBodyFromPayload([]byte(`{"responses":"not-an-array"}`))
err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsCreate, raw)
err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsCreate, "", raw)
if err == nil || !strings.Contains(err.Error(), "failed to decode create payload") {
t.Fatalf("expected proto decode error for create, got: %v", err)
}
Expand All @@ -274,7 +279,7 @@ func TestParseVaultGatewayResponse_UnsupportedMethod_Warns(t *testing.T) {

// Non-empty payload so it passes "empty payload" check; method is unknown -> warn.
raw := encodeRPCBodyFromPayload([]byte(`{"anything":"ok"}`))
if err := h.ParseVaultGatewayResponse("totally.unknown.method", raw); err != nil {
if err := h.ParseVaultGatewayResponse("totally.unknown.method", "", raw); err != nil {
t.Fatalf("unexpected error: %v", err)
}
out := buf.String()
Expand Down Expand Up @@ -337,7 +342,7 @@ func TestParseVaultGatewayResponse_List_SuccessWithIdentifiers(t *testing.T) {
h := newTestHandler(&buf)

body := encodeRPCBodyFromPayload(buildListPayloadProtoSuccessWithItems(t))
if err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsList, body); err != nil {
if err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsList, "", body); err != nil {
t.Fatalf("unexpected error: %v", err)
}

Expand Down Expand Up @@ -369,7 +374,7 @@ func TestParseVaultGatewayResponse_List_EmptySuccess(t *testing.T) {
h := newTestHandler(&buf)

body := encodeRPCBodyFromPayload(buildListPayloadProtoEmptySuccess(t))
if err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsList, body); err != nil {
if err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsList, "", body); err != nil {
t.Fatalf("unexpected error: %v", err)
}

Expand Down Expand Up @@ -398,7 +403,7 @@ func TestParseVaultGatewayResponse_List_Failure(t *testing.T) {
h := newTestHandler(&buf)

body := encodeRPCBodyFromPayload(buildListPayloadProtoFailure(t, "boom"))
if err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsList, body); err != nil {
if err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsList, "", body); err != nil {
t.Fatalf("unexpected error: %v", err)
}

Expand All @@ -425,7 +430,7 @@ func TestParseVaultGatewayResponse_BadPayloadForList(t *testing.T) {

// Wrong shape: identifiers should be an array
raw := encodeRPCBodyFromPayload([]byte(`{"identifiers":"not-an-array"}`))
err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsList, raw)
err := h.ParseVaultGatewayResponse(vaulttypes.MethodSecretsList, "", raw)
if err == nil || !strings.Contains(err.Error(), "failed to decode list payload") {
t.Fatalf("expected decode error for list payload, got: %v", err)
}
Expand Down
53 changes: 53 additions & 0 deletions cmd/secrets/common/vault_response_verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package common

import (
"context"
"fmt"

"github.com/smartcontractkit/chainlink-common/pkg/jsonrpc2"
"github.com/smartcontractkit/chainlink/v2/core/capabilities/vault/vaulttypes"

"github.com/smartcontractkit/cre-cli/internal/onchain/capabilitiesregistry"
)

func (h *Handler) verifyVaultGatewayResponse(
ctx context.Context,
rpcResp *jsonrpc2.Response[vaulttypes.SignedOCRResponse],
requestID string,
) error {
if h.SkipVaultValidation() {
return nil
}
if requestID == "" {
return fmt.Errorf("missing request ID for vault response verification")
}
if rpcResp.ID != requestID {
return fmt.Errorf("jsonrpc id mismatch: got %q want %q", rpcResp.ID, requestID)
}

resolver, ok := h.VaultDONResolver()
if !ok {
return nil
}

signed := rpcResp.Result
if signed == nil {
return fmt.Errorf("empty SignedOCRResponse result")
}
// TODO(DEVSVCS-5365)
if len(signed.Signatures) == 0 {
return nil
}
Comment thread
anirudhwarrier marked this conversation as resolved.

v, err := resolver.ResolveVaultDON(ctx)
if err != nil {
return fmt.Errorf("resolve vault DON for signature verification: %w", err)
}

signers := capabilitiesregistry.OCRSignerAddresses(v.Nodes)
minSigs := capabilitiesregistry.MinOCRSignatures(v.DON.F)
if err := vaulttypes.ValidateSignatures(signed, signers, minSigs); err != nil {
return fmt.Errorf("vault response signature verification failed: %w", err)
}
return nil
}
Loading
Loading