From adc571214a00b87769b399acdb8904786d069232 Mon Sep 17 00:00:00 2001 From: giogam <151543+giogam@users.noreply.github.com> Date: Fri, 7 Nov 2025 22:59:19 +0100 Subject: [PATCH 01/13] feat: adds HMAC authentication support for catalog remote Implements KMS-based HMAC authentication for catalog gRPC connections: - Add HMACAuthConfig to catalog configuration - Implement AWS KMS HMAC signature generation using SHA-256 - Add HMAC signature and timestamp to gRPC metadata headers - Refactor catalog stores to support HMAC-enabled DataAccess calls --- datastore/catalog/remote/address_ref_store.go | 70 ++++---- .../catalog/remote/address_ref_store_test.go | 2 +- .../catalog/remote/chain_metadata_store.go | 39 +++-- .../remote/chain_metadata_store_test.go | 2 +- .../catalog/remote/contract_metadata_store.go | 39 +++-- .../remote/contract_metadata_store_test.go | 2 +- datastore/catalog/remote/datastore_tx_test.go | 5 +- .../catalog/remote/env_metadata_store.go | 26 +-- .../catalog/remote/env_metadata_store_test.go | 2 +- datastore/catalog/remote/grpc.go | 165 ++++++++++++++++-- datastore/catalog/remote/grpc_test.go | 3 +- datastore/catalog/remote/utils.go | 4 +- datastore/catalog_syncer_integration_test.go | 59 ++++++- engine/cld/catalog/catalog.go | 46 ++++- engine/cld/catalog/catalog_test.go | 77 +++++++- engine/cld/config/env/config.go | 9 +- go.mod | 27 +-- go.sum | 28 +++ 18 files changed, 472 insertions(+), 133 deletions(-) diff --git a/datastore/catalog/remote/address_ref_store.go b/datastore/catalog/remote/address_ref_store.go index b41e1d0a6..21dbada35 100644 --- a/datastore/catalog/remote/address_ref_store.go +++ b/datastore/catalog/remote/address_ref_store.go @@ -53,12 +53,6 @@ func (s *catalogAddressRefStore) get( ignoreTransaction bool, key datastore.AddressRefKey, ) (datastore.AddressRef, error) { - // Create a bidirectional stream - stream, err := s.client.DataAccess() - if err != nil { - return datastore.AddressRef{}, fmt.Errorf("failed to create data access stream: %w", err) - } - // Create the find request with the key converted to a filter filter := s.keyToFilter(key) findRequest := &pb.AddressReferenceFindRequest{ @@ -66,13 +60,19 @@ func (s *catalogAddressRefStore) get( IgnoreTransaction: ignoreTransaction, } - // Send the request + // Create the request request := &pb.DataAccessRequest{ Operation: &pb.DataAccessRequest_AddressReferenceFindRequest{ AddressReferenceFindRequest: findRequest, }, } + // Create a bidirectional stream with the initial request for HMAC + stream, err := s.client.DataAccess(request) + if err != nil { + return datastore.AddressRef{}, fmt.Errorf("failed to create data access stream: %w", err) + } + if sendErr := stream.Send(request); sendErr != nil { return datastore.AddressRef{}, fmt.Errorf("failed to send find request: %w", sendErr) } @@ -120,12 +120,6 @@ func (s *catalogAddressRefStore) get( // Fetch returns a copy of all AddressRef in the catalog. func (s *catalogAddressRefStore) Fetch(_ context.Context) ([]datastore.AddressRef, error) { - // Create a bidirectional stream - stream, err := s.client.DataAccess() - if err != nil { - return nil, fmt.Errorf("failed to create data access stream: %w", err) - } - // Create the find request with an empty filter to get all records // We only filter by domain and environment to get all records for this store's scope filter := &pb.AddressReferenceKeyFilter{ @@ -138,13 +132,19 @@ func (s *catalogAddressRefStore) Fetch(_ context.Context) ([]datastore.AddressRe KeyFilter: filter, } - // Send the request + // Create the request request := &pb.DataAccessRequest{ Operation: &pb.DataAccessRequest_AddressReferenceFindRequest{ AddressReferenceFindRequest: findRequest, }, } + // Create a bidirectional stream with the initial request for HMAC + stream, err := s.client.DataAccess(request) + if err != nil { + return nil, fmt.Errorf("failed to create data access stream: %w", err) + } + if sendErr := stream.Send(request); sendErr != nil { return nil, fmt.Errorf("failed to send find request: %w", sendErr) } @@ -212,12 +212,6 @@ func (s *catalogAddressRefStore) Filter( } func (s *catalogAddressRefStore) Add(_ context.Context, record datastore.AddressRef) error { - // Create a bidirectional stream - stream, err := s.client.DataAccess() - if err != nil { - return fmt.Errorf("failed to create data access stream: %w", err) - } - // Convert the datastore record to protobuf protoRef := s.addressRefToProto(record) @@ -227,13 +221,19 @@ func (s *catalogAddressRefStore) Add(_ context.Context, record datastore.Address Semantics: pb.EditSemantics_SEMANTICS_INSERT, } - // Send the edit request + // Create the request editReq := &pb.DataAccessRequest{ Operation: &pb.DataAccessRequest_AddressReferenceEditRequest{ AddressReferenceEditRequest: editRequest, }, } + // Create a bidirectional stream with the initial request for HMAC + stream, err := s.client.DataAccess(editReq) + if err != nil { + return fmt.Errorf("failed to create data access stream: %w", err) + } + if sendErr := stream.Send(editReq); sendErr != nil { return fmt.Errorf("failed to send edit request: %w", sendErr) } @@ -259,12 +259,6 @@ func (s *catalogAddressRefStore) Add(_ context.Context, record datastore.Address } func (s *catalogAddressRefStore) Upsert(_ context.Context, record datastore.AddressRef) error { - // Create a bidirectional stream - stream, err := s.client.DataAccess() - if err != nil { - return fmt.Errorf("failed to create data access stream: %w", err) - } - // Convert the datastore record to protobuf protoRef := s.addressRefToProto(record) @@ -274,13 +268,19 @@ func (s *catalogAddressRefStore) Upsert(_ context.Context, record datastore.Addr Semantics: pb.EditSemantics_SEMANTICS_UPSERT, } - // Send the edit request + // Create the request request := &pb.DataAccessRequest{ Operation: &pb.DataAccessRequest_AddressReferenceEditRequest{ AddressReferenceEditRequest: editRequest, }, } + // Create a bidirectional stream with the initial request for HMAC + stream, err := s.client.DataAccess(request) + if err != nil { + return fmt.Errorf("failed to create data access stream: %w", err) + } + if sendErr := stream.Send(request); sendErr != nil { return fmt.Errorf("failed to send edit request: %w", sendErr) } @@ -319,12 +319,6 @@ func (s *catalogAddressRefStore) Update(ctx context.Context, record datastore.Ad } // Record exists, proceed with updating it - // Create a bidirectional stream - stream, streamErr := s.client.DataAccess() - if streamErr != nil { - return fmt.Errorf("failed to create data access stream: %w", streamErr) - } - // Convert the datastore record to protobuf protoRef := s.addressRefToProto(record) @@ -334,13 +328,19 @@ func (s *catalogAddressRefStore) Update(ctx context.Context, record datastore.Ad Semantics: pb.EditSemantics_SEMANTICS_UPDATE, } - // Send the edit request + // Create the request editReq := &pb.DataAccessRequest{ Operation: &pb.DataAccessRequest_AddressReferenceEditRequest{ AddressReferenceEditRequest: editRequest, }, } + // Create a bidirectional stream with the initial request for HMAC + stream, streamErr := s.client.DataAccess(editReq) + if streamErr != nil { + return fmt.Errorf("failed to create data access stream: %w", streamErr) + } + if sendErr := stream.Send(editReq); sendErr != nil { return fmt.Errorf("failed to send edit request: %w", sendErr) } diff --git a/datastore/catalog/remote/address_ref_store_test.go b/datastore/catalog/remote/address_ref_store_test.go index 5c6a0e927..c6d1326af 100644 --- a/datastore/catalog/remote/address_ref_store_test.go +++ b/datastore/catalog/remote/address_ref_store_test.go @@ -637,7 +637,7 @@ func setupTestStore(t *testing.T, domain, environment string) *catalogAddressRef } // Test if the service is actually available by making a simple call - _, err = catalogClient.DataAccess() + _, err = catalogClient.DataAccess(&pb.DataAccessRequest{}) if err != nil { t.Skipf("gRPC service not available at %s: %v. Skipping integration tests.", address, err) return nil diff --git a/datastore/catalog/remote/chain_metadata_store.go b/datastore/catalog/remote/chain_metadata_store.go index f4ac99fe1..b83abc49f 100644 --- a/datastore/catalog/remote/chain_metadata_store.go +++ b/datastore/catalog/remote/chain_metadata_store.go @@ -119,12 +119,7 @@ func (s *catalogChainMetadataStore) Get( } func (s *catalogChainMetadataStore) get(ignoreTransaction bool, key datastore.ChainMetadataKey) (datastore.ChainMetadata, error) { - stream, err := s.client.DataAccess() - if err != nil { - return datastore.ChainMetadata{}, fmt.Errorf("failed to create gRPC stream: %w", err) - } - - // Send find request + // Create find request findReq := &pb.DataAccessRequest{ Operation: &pb.DataAccessRequest_ChainMetadataFindRequest{ ChainMetadataFindRequest: &pb.ChainMetadataFindRequest{ @@ -134,6 +129,12 @@ func (s *catalogChainMetadataStore) get(ignoreTransaction bool, key datastore.Ch }, } + // Create stream with the initial request for HMAC + stream, err := s.client.DataAccess(findReq) + if err != nil { + return datastore.ChainMetadata{}, fmt.Errorf("failed to create gRPC stream: %w", err) + } + if sendErr := stream.Send(findReq); sendErr != nil { return datastore.ChainMetadata{}, fmt.Errorf("failed to send find request: %w", sendErr) } @@ -181,12 +182,7 @@ func (s *catalogChainMetadataStore) get(ignoreTransaction bool, key datastore.Ch // Fetch returns a copy of all ChainMetadata in the catalog. func (s *catalogChainMetadataStore) Fetch(_ context.Context) ([]datastore.ChainMetadata, error) { - stream, err := s.client.DataAccess() - if err != nil { - return nil, fmt.Errorf("failed to create gRPC stream: %w", err) - } - - // Send find request with domain and environment filter only (fetch all) + // Create find request with domain and environment filter only (fetch all) findReq := &pb.DataAccessRequest{ Operation: &pb.DataAccessRequest_ChainMetadataFindRequest{ ChainMetadataFindRequest: &pb.ChainMetadataFindRequest{ @@ -198,6 +194,12 @@ func (s *catalogChainMetadataStore) Fetch(_ context.Context) ([]datastore.ChainM }, } + // Create stream with the initial request for HMAC + stream, err := s.client.DataAccess(findReq) + if err != nil { + return nil, fmt.Errorf("failed to create gRPC stream: %w", err) + } + if sendErr := stream.Send(findReq); sendErr != nil { return nil, fmt.Errorf("failed to send find request: %w", sendErr) } @@ -347,16 +349,11 @@ func (s *catalogChainMetadataStore) Delete(_ context.Context, _ datastore.ChainM // editRecord is a helper method that handles Add, Upsert, and Update operations func (s *catalogChainMetadataStore) editRecord(record datastore.ChainMetadata, semantics pb.EditSemantics) error { - stream, err := s.client.DataAccess() - if err != nil { - return fmt.Errorf("failed to create gRPC stream: %w", err) - } - // Get the current version for this record key := record.Key() version := s.getVersion(key) - // Send edit request + // Create edit request editReq := &pb.DataAccessRequest{ Operation: &pb.DataAccessRequest_ChainMetadataEditRequest{ ChainMetadataEditRequest: &pb.ChainMetadataEditRequest{ @@ -366,6 +363,12 @@ func (s *catalogChainMetadataStore) editRecord(record datastore.ChainMetadata, s }, } + // Create stream with the initial request for HMAC + stream, err := s.client.DataAccess(editReq) + if err != nil { + return fmt.Errorf("failed to create gRPC stream: %w", err) + } + if sendErr := stream.Send(editReq); sendErr != nil { return fmt.Errorf("failed to send edit request: %w", sendErr) } diff --git a/datastore/catalog/remote/chain_metadata_store_test.go b/datastore/catalog/remote/chain_metadata_store_test.go index 995814167..30a7ed08c 100644 --- a/datastore/catalog/remote/chain_metadata_store_test.go +++ b/datastore/catalog/remote/chain_metadata_store_test.go @@ -60,7 +60,7 @@ func setupTestChainStore(t *testing.T, domain, environment string) *catalogChain } // Test if the service is actually available by making a simple call - _, err = catalogClient.DataAccess() + _, err = catalogClient.DataAccess(&pb.DataAccessRequest{}) if err != nil { t.Skipf("gRPC service not available at %s: %v. Skipping integration tests.", address, err) return nil diff --git a/datastore/catalog/remote/contract_metadata_store.go b/datastore/catalog/remote/contract_metadata_store.go index 40974bf9c..f65ddda6c 100644 --- a/datastore/catalog/remote/contract_metadata_store.go +++ b/datastore/catalog/remote/contract_metadata_store.go @@ -123,12 +123,7 @@ func (s *catalogContractMetadataStore) Get( } func (s *catalogContractMetadataStore) get(ignoreTransaction bool, key datastore.ContractMetadataKey) (datastore.ContractMetadata, error) { - stream, err := s.client.DataAccess() - if err != nil { - return datastore.ContractMetadata{}, fmt.Errorf("failed to create gRPC stream: %w", err) - } - - // Send find request + // Create find request findReq := &pb.DataAccessRequest{ Operation: &pb.DataAccessRequest_ContractMetadataFindRequest{ ContractMetadataFindRequest: &pb.ContractMetadataFindRequest{ @@ -138,6 +133,12 @@ func (s *catalogContractMetadataStore) get(ignoreTransaction bool, key datastore }, } + // Create stream with the initial request for HMAC + stream, err := s.client.DataAccess(findReq) + if err != nil { + return datastore.ContractMetadata{}, fmt.Errorf("failed to create gRPC stream: %w", err) + } + if sendErr := stream.Send(findReq); sendErr != nil { return datastore.ContractMetadata{}, fmt.Errorf("failed to send find request: %w", sendErr) } @@ -185,12 +186,7 @@ func (s *catalogContractMetadataStore) get(ignoreTransaction bool, key datastore // Fetch returns a copy of all ContractMetadata in the catalog. func (s *catalogContractMetadataStore) Fetch(_ context.Context) ([]datastore.ContractMetadata, error) { - stream, err := s.client.DataAccess() - if err != nil { - return nil, fmt.Errorf("failed to create gRPC stream: %w", err) - } - - // Send find request with domain and environment filter only (fetch all) + // Create find request with domain and environment filter only (fetch all) findReq := &pb.DataAccessRequest{ Operation: &pb.DataAccessRequest_ContractMetadataFindRequest{ ContractMetadataFindRequest: &pb.ContractMetadataFindRequest{ @@ -202,6 +198,12 @@ func (s *catalogContractMetadataStore) Fetch(_ context.Context) ([]datastore.Con }, } + // Create stream with the initial request for HMAC + stream, err := s.client.DataAccess(findReq) + if err != nil { + return nil, fmt.Errorf("failed to create gRPC stream: %w", err) + } + if sendErr := stream.Send(findReq); sendErr != nil { return nil, fmt.Errorf("failed to send find request: %w", sendErr) } @@ -354,16 +356,11 @@ func (s *catalogContractMetadataStore) Delete(_ context.Context, _ datastore.Con // editRecord is a helper method that handles Add, Upsert, and Update operations func (s *catalogContractMetadataStore) editRecord(record datastore.ContractMetadata, semantics pb.EditSemantics) error { - stream, err := s.client.DataAccess() - if err != nil { - return fmt.Errorf("failed to create gRPC stream: %w", err) - } - // Get the current version for this record key := record.Key() version := s.getVersion(key) - // Send edit request + // Create edit request editReq := &pb.DataAccessRequest{ Operation: &pb.DataAccessRequest_ContractMetadataEditRequest{ ContractMetadataEditRequest: &pb.ContractMetadataEditRequest{ @@ -373,6 +370,12 @@ func (s *catalogContractMetadataStore) editRecord(record datastore.ContractMetad }, } + // Create stream with the initial request for HMAC + stream, err := s.client.DataAccess(editReq) + if err != nil { + return fmt.Errorf("failed to create gRPC stream: %w", err) + } + if sendErr := stream.Send(editReq); sendErr != nil { return fmt.Errorf("failed to send edit request: %w", sendErr) } diff --git a/datastore/catalog/remote/contract_metadata_store_test.go b/datastore/catalog/remote/contract_metadata_store_test.go index 568898336..7f04f8011 100644 --- a/datastore/catalog/remote/contract_metadata_store_test.go +++ b/datastore/catalog/remote/contract_metadata_store_test.go @@ -60,7 +60,7 @@ func setupTestContractStore(t *testing.T, domain, environment string) *catalogCo } // Test if the gRPC service is actually available by making a simple call - _, err = catalogClient.DataAccess() + _, err = catalogClient.DataAccess(&pb.DataAccessRequest{}) if err != nil { t.Skipf("gRPC service not available at %s: %v. Skipping integration tests.", address, err) return nil diff --git a/datastore/catalog/remote/datastore_tx_test.go b/datastore/catalog/remote/datastore_tx_test.go index 598b27418..edaf0aaf1 100644 --- a/datastore/catalog/remote/datastore_tx_test.go +++ b/datastore/catalog/remote/datastore_tx_test.go @@ -11,9 +11,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/grpc/credentials/insecure" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + + pb "github.com/smartcontractkit/chainlink-protos/op-catalog/v1/datastore" ) //nolint:paralleltest @@ -448,7 +451,7 @@ func setupStore(t *testing.T, ctx context.Context) (*catalogDataStore, error) { return nil, fmt.Errorf("failed to connect to gRPC server at %s: %w. Skipping integration tests", address, err) } // Test if the service is actually available by making a simple call - _, err = catalogClient.DataAccess() + _, err = catalogClient.DataAccess(&pb.DataAccessRequest{}) if err != nil { return nil, fmt.Errorf("gRPC service not available at %s: %w. Skipping integration tests", address, err) } diff --git a/datastore/catalog/remote/env_metadata_store.go b/datastore/catalog/remote/env_metadata_store.go index 3550b523d..268dbc1ad 100644 --- a/datastore/catalog/remote/env_metadata_store.go +++ b/datastore/catalog/remote/env_metadata_store.go @@ -117,12 +117,7 @@ func (s *catalogEnvMetadataStore) Get( } func (s *catalogEnvMetadataStore) get(ignoreTransaction bool) (datastore.EnvMetadata, error) { - stream, err := s.client.DataAccess() - if err != nil { - return datastore.EnvMetadata{}, fmt.Errorf("failed to create gRPC stream: %w", err) - } - - // Send find request + // Create find request findReq := &pb.DataAccessRequest{ Operation: &pb.DataAccessRequest_EnvironmentMetadataFindRequest{ EnvironmentMetadataFindRequest: &pb.EnvironmentMetadataFindRequest{ @@ -132,6 +127,12 @@ func (s *catalogEnvMetadataStore) get(ignoreTransaction bool) (datastore.EnvMeta }, } + // Create stream with the initial request for HMAC + stream, err := s.client.DataAccess(findReq) + if err != nil { + return datastore.EnvMetadata{}, fmt.Errorf("failed to create gRPC stream: %w", err) + } + if sendErr := stream.Send(findReq); sendErr != nil { return datastore.EnvMetadata{}, fmt.Errorf("failed to send find request: %w", sendErr) } @@ -224,12 +225,7 @@ func (s *catalogEnvMetadataStore) editRecord(record datastore.EnvMetadata) error // Create the protobuf record protoRecord := s.envMetadataToProto(record, version) - // Send edit request with UPSERT semantics (since Set should always work) - stream, err := s.client.DataAccess() - if err != nil { - return fmt.Errorf("failed to create gRPC stream: %w", err) - } - + // Create edit request with UPSERT semantics (since Set should always work) editReq := &pb.DataAccessRequest{ Operation: &pb.DataAccessRequest_EnvironmentMetadataEditRequest{ EnvironmentMetadataEditRequest: &pb.EnvironmentMetadataEditRequest{ @@ -239,6 +235,12 @@ func (s *catalogEnvMetadataStore) editRecord(record datastore.EnvMetadata) error }, } + // Create stream with the initial request for HMAC + stream, err := s.client.DataAccess(editReq) + if err != nil { + return fmt.Errorf("failed to create gRPC stream: %w", err) + } + if sendErr := stream.Send(editReq); sendErr != nil { return fmt.Errorf("failed to send edit request: %w", sendErr) } diff --git a/datastore/catalog/remote/env_metadata_store_test.go b/datastore/catalog/remote/env_metadata_store_test.go index fbf8286dc..d069c0dcc 100644 --- a/datastore/catalog/remote/env_metadata_store_test.go +++ b/datastore/catalog/remote/env_metadata_store_test.go @@ -55,7 +55,7 @@ func setupTestEnvStore(t *testing.T, domain, environment string) *catalogEnvMeta } // Test if the gRPC service is actually available by making a simple call - _, err = catalogClient.DataAccess() + _, err = catalogClient.DataAccess(&pb.DataAccessRequest{}) if err != nil { t.Skipf("gRPC service not available at %s: %v. Skipping integration tests.", address, err) return nil diff --git a/datastore/catalog/remote/grpc.go b/datastore/catalog/remote/grpc.go index 93fffac07..e395d8d4c 100644 --- a/datastore/catalog/remote/grpc.go +++ b/datastore/catalog/remote/grpc.go @@ -2,12 +2,21 @@ package remote import ( "context" + "crypto/sha256" + "encoding/hex" "fmt" + "strconv" + "time" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/kms" + "github.com/aws/aws-sdk-go-v2/service/kms/types" + pb "github.com/smartcontractkit/chainlink-protos/op-catalog/v1/datastore" "google.golang.org/grpc" "google.golang.org/grpc/credentials" - - pb "github.com/smartcontractkit/chainlink-protos/op-catalog/v1/datastore" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/proto" ) type CatalogClient struct { @@ -23,11 +32,22 @@ type CatalogClient struct { //nolint:containedctx ctx context.Context cachedStream grpc.BidiStreamingClient[pb.DataAccessRequest, pb.DataAccessResponse] + hmacConfig *HMACAuthConfig } -func (c *CatalogClient) DataAccess() (grpc.BidiStreamingClient[pb.DataAccessRequest, pb.DataAccessResponse], error) { +func (c *CatalogClient) DataAccess(req proto.Message) (grpc.BidiStreamingClient[pb.DataAccessRequest, pb.DataAccessResponse], error) { if c.cachedStream == nil { - stream, err := c.protoClient.DataAccess(c.ctx) + // Apply HMAC signature if enabled + ctx := c.ctx + if c.hmacConfig != nil { + var err error + ctx, err = c.prepareHMACContext(c.ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to prepare HMAC context: %w", err) + } + } + + stream, err := c.protoClient.DataAccess(ctx) if err != nil { return nil, err } @@ -51,22 +71,40 @@ func (c *CatalogClient) CloseStream() error { } type CatalogConfig struct { - GRPC string - Creds credentials.TransportCredentials + GRPC string + Creds credentials.TransportCredentials + HMACConfig *HMACAuthConfig } // NewCatalogClient creates a new CatalogClient with the provided configuration. +// +// Example usage: +// +// cfg := CatalogConfig{ +// GRPC: "op-catalog.example.com:443", +// Creds: credentials.NewTLS(&tls.Config{}), +// HMACConfig: &HMACAuthConfig{ +// KeyID: "kms-key-id", +// KeyRegion: "us-west-2", +// Authority: "op-catalog.example.com", +// }, +// } +// client, err := NewCatalogClient(ctx, cfg) func NewCatalogClient(ctx context.Context, cfg CatalogConfig) (*CatalogClient, error) { + client := &CatalogClient{ + ctx: ctx, + hmacConfig: cfg.HMACConfig, + } + + // Create connection with the configured options conn, err := newCatalogConnection(cfg) if err != nil { - return &CatalogClient{}, fmt.Errorf("failed to connect Catalog service. Err: %w", err) - } - client := CatalogClient{ - ctx: ctx, - protoClient: pb.NewDatastoreClient(conn), + return nil, fmt.Errorf("failed to connect Catalog service. Err: %w", err) } - return &client, err + client.protoClient = pb.NewDatastoreClient(conn) + + return client, nil } // newCatalogConnection creates a new gRPC connection to the Catalog service. @@ -78,6 +116,14 @@ func newCatalogConnection(cfg CatalogConfig) (*grpc.ClientConn, error) { opts = append(opts, grpc.WithTransportCredentials(cfg.Creds)) } + if cfg.HMACConfig != nil { + // Force authority header to be set to match what's used in the HMAC signature. + // This ensures the server verifies against the same authority we signed with. + // see: https://github.com/grpc/grpc-go/blob/7472d578b15f718cbe8ca0f5f5a3713093c47b03/internal/transport/http2_client.go#L653 + // see: https://github.com/grpc/grpc-go/blob/7472d578b15f718cbe8ca0f5f5a3713093c47b03/internal/transport/http2_client.go#L533 + opts = append(opts, grpc.WithAuthority(cfg.HMACConfig.Authority)) + } + if len(interceptors) > 0 { opts = append(opts, grpc.WithChainUnaryInterceptor(interceptors...)) } @@ -89,3 +135,98 @@ func newCatalogConnection(cfg CatalogConfig) (*grpc.ClientConn, error) { return conn, nil } + +const ( + // dataAccessMethod is the full gRPC method name for DataAccess + dataAccessMethod = "/op_catalog.v1.datastore.Datastore/DataAccess" +) + +// HMACAuthConfig holds HMAC authentication configuration. +type HMACAuthConfig struct { + KeyID string + KeyRegion string + Authority string // The gRPC authority (hostname without port) used for HMAC signing +} + +// prepareHMACContext prepares the context with HMAC authentication metadata. +// It loads AWS KMS configuration, creates a KMS client, generates an HMAC signature, +// and attaches it to the outgoing gRPC metadata. +func (c *CatalogClient) prepareHMACContext(ctx context.Context, req proto.Message) (context.Context, error) { + // Load AWS configuration + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(c.hmacConfig.KeyRegion)) + if err != nil { + return nil, fmt.Errorf("failed to load AWS config: %w", err) + } + + // Create KMS client + kmsClient := kms.NewFromConfig(cfg) + + // Create HMAC helper + hmacHelper := &kmsHMACClientHelper{ + kmsClient: kmsClient, + keyID: c.hmacConfig.KeyID, + } + + // Marshal the message to bytes for HMAC + payload, err := proto.Marshal(req) + if err != nil { + return nil, fmt.Errorf("failed to marshal message for HMAC: %w", err) + } + + // Generate HMAC signature and timestamp + signature, timestamp, err := hmacHelper.generateHMACSignature(ctx, dataAccessMethod, c.hmacConfig.Authority, payload) + if err != nil { + return nil, fmt.Errorf("failed to generate HMAC signature: %w", err) + } + + // Add HMAC authentication to gRPC metadata + md := metadata.Pairs( + "x-hmac-signature", signature, + "x-hmac-timestamp", timestamp, + ) + + // Merge with existing metadata if present + if existingMd, ok := metadata.FromOutgoingContext(ctx); ok { + md = metadata.Join(existingMd, md) + } + + return metadata.NewOutgoingContext(ctx, md), nil +} + +// kmsHMACClientHelper helps clients generate HMAC signatures using AWS KMS. +type kmsHMACClientHelper struct { + kmsClient *kms.Client + keyID string +} + +// generateHMACSignature generates an HMAC signature and timestamp for the given request. +// Returns the hex-encoded signature and Unix timestamp as strings. +// The caller is responsible for adding these to transport-specific metadata/headers. +func (h *kmsHMACClientHelper) generateHMACSignature(ctx context.Context, method string, authority string, payload []byte) (signature string, timestamp string, err error) { + timestamp = strconv.FormatInt(time.Now().Unix(), 10) + + // Hash the payload with SHA-256 to stay within KMS message size limits (4096 bytes) + // and to have a predictable signature length + payloadHash := sha256.Sum256(payload) + + // Construct HMAC message using method path, authority, timestamp, and payload hash + // Format: method\nauthority\ntimestamp\nsha256(payload) + messagePrefix := fmt.Sprintf("%s\n%s\n%s\n", method, authority, timestamp) + fullMessage := append([]byte(messagePrefix), payloadHash[:]...) + + // Generate MAC using KMS with HMAC_SHA_256 + generateInput := &kms.GenerateMacInput{ + KeyId: aws.String(h.keyID), + Message: fullMessage, + MacAlgorithm: types.MacAlgorithmSpecHmacSha256, + } + + generateOutput, err := h.kmsClient.GenerateMac(ctx, generateInput) + if err != nil { + return "", "", fmt.Errorf("failed to generate MAC: %w", err) + } + + signature = hex.EncodeToString(generateOutput.Mac) + + return signature, timestamp, nil +} diff --git a/datastore/catalog/remote/grpc_test.go b/datastore/catalog/remote/grpc_test.go index 49addbe88..28c739119 100644 --- a/datastore/catalog/remote/grpc_test.go +++ b/datastore/catalog/remote/grpc_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "google.golang.org/grpc/credentials/insecure" "github.com/smartcontractkit/chainlink-deployments-framework/datastore/catalog/remote" @@ -48,7 +49,7 @@ func TestNewCatalogClient_Success(t *testing.T) { if tt.expectError { require.Error(t, err) require.Contains(t, err.Error(), tt.errorContains) - require.Equal(t, &remote.CatalogClient{}, client) + require.Nil(t, client) } else { require.NoError(t, err) require.NotNil(t, client) diff --git a/datastore/catalog/remote/utils.go b/datastore/catalog/remote/utils.go index 0f86ccd0e..12671cced 100644 --- a/datastore/catalog/remote/utils.go +++ b/datastore/catalog/remote/utils.go @@ -10,8 +10,8 @@ func ThrowAndCatch( catalog *catalogDataStore, request *pb.DataAccessRequest, ) (*pb.DataAccessResponse, error) { - // Create a bidirectional stream - stream, err := catalog.client.DataAccess() + // Create a bidirectional stream with the initial request for HMAC + stream, err := catalog.client.DataAccess(request) if err != nil { return nil, fmt.Errorf("failed to create data access stream: %w", err) } diff --git a/datastore/catalog_syncer_integration_test.go b/datastore/catalog_syncer_integration_test.go index 50c808ce2..c76ee41a1 100644 --- a/datastore/catalog_syncer_integration_test.go +++ b/datastore/catalog_syncer_integration_test.go @@ -3,9 +3,11 @@ package datastore_test import ( "context" "os" + "strings" "testing" "github.com/Masterminds/semver/v3" + pb "github.com/smartcontractkit/chainlink-protos/op-catalog/v1/datastore" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/credentials/insecure" @@ -34,11 +36,19 @@ func TestMergeDataStoreToCatalog_FullSync(t *testing.T) { }) if err != nil { t.Skipf("Catalog service not available at %s: %v. Start with: cd op-catalog && docker-compose up -d", catalogAddr, err) + return } - testStream, streamErr := testClient.DataAccess() + testStream, streamErr := testClient.DataAccess(&pb.DataAccessRequest{}) if streamErr != nil { + // Check if it's an auth error + if strings.Contains(streamErr.Error(), "Unauthenticated") || strings.Contains(streamErr.Error(), "HMAC") { + t.Skipf("Catalog service at %s requires HMAC authentication. Skipping integration test.", catalogAddr) + + return + } t.Skipf("Cannot connect to catalog service at %s: %v. Start with: cd op-catalog && docker-compose up -d", catalogAddr, streamErr) + return } _ = testStream.CloseSend() @@ -154,7 +164,14 @@ func TestMergeDataStoreToCatalog_FullSync(t *testing.T) { // Step 2: Merge datastore to catalog (full sync for initial migration) t.Log("Step 2: Merging local datastore to catalog...") err = datastore.MergeDataStoreToCatalog(ctx, sealedDS, catalogStore) - require.NoError(t, err, "Failed to merge datastore to catalog") + if err != nil { + // Check if it's an auth error and skip if so + if strings.Contains(err.Error(), "Unauthenticated") || strings.Contains(err.Error(), "HMAC") { + t.Skipf("Catalog service requires HMAC authentication: %v", err) + return + } + require.NoError(t, err, "Failed to merge datastore to catalog") + } t.Log("✅ Merge completed successfully!") // Step 3: Verify data was synced correctly by reading back from catalog @@ -249,11 +266,19 @@ func TestMergeDataStoreToCatalog_Incremental(t *testing.T) { }) if err != nil { t.Skipf("Catalog service not available at %s: %v. Start with: cd op-catalog && docker-compose up -d", catalogAddr, err) + return } - testStream, streamErr := testClient.DataAccess() + testStream, streamErr := testClient.DataAccess(&pb.DataAccessRequest{}) if streamErr != nil { + // Check if it's an auth error + if strings.Contains(streamErr.Error(), "Unauthenticated") || strings.Contains(streamErr.Error(), "HMAC") { + t.Skipf("Catalog service at %s requires HMAC authentication. Skipping integration test.", catalogAddr) + + return + } t.Skipf("Cannot connect to catalog service at %s: %v. Start with: cd op-catalog && docker-compose up -d", catalogAddr, streamErr) + return } _ = testStream.CloseSend() @@ -299,7 +324,14 @@ func TestMergeDataStoreToCatalog_Incremental(t *testing.T) { // Merge initial state to catalog err = datastore.MergeDataStoreToCatalog(ctx, initialDS.Seal(), catalogStore) - require.NoError(t, err) + if err != nil { + // Check if it's an auth error and skip if so + if strings.Contains(err.Error(), "Unauthenticated") || strings.Contains(err.Error(), "HMAC") { + t.Skipf("Catalog service requires HMAC authentication: %v", err) + return + } + require.NoError(t, err) + } t.Log("✅ Initial state merged") // Step 2: Create a migration datastore with new contracts @@ -396,11 +428,19 @@ func TestMergeDataStoreToCatalog_TransactionRollback(t *testing.T) { }) if err != nil { t.Skipf("Catalog service not available at %s: %v. Start with: cd op-catalog && docker-compose up -d", catalogAddr, err) + return } - testStream, streamErr := testClient.DataAccess() + testStream, streamErr := testClient.DataAccess(&pb.DataAccessRequest{}) if streamErr != nil { + // Check if it's an auth error + if strings.Contains(streamErr.Error(), "Unauthenticated") || strings.Contains(streamErr.Error(), "HMAC") { + t.Skipf("Catalog service at %s requires HMAC authentication. Skipping integration test.", catalogAddr) + + return + } t.Skipf("Cannot connect to catalog service at %s: %v. Start with: cd op-catalog && docker-compose up -d", catalogAddr, streamErr) + return } _ = testStream.CloseSend() @@ -445,7 +485,14 @@ func TestMergeDataStoreToCatalog_TransactionRollback(t *testing.T) { // Merge should succeed err = datastore.MergeDataStoreToCatalog(ctx, localDS.Seal(), catalogStore) - require.NoError(t, err) + if err != nil { + // Check if it's an auth error and skip if so + if strings.Contains(err.Error(), "Unauthenticated") || strings.Contains(err.Error(), "HMAC") { + t.Skipf("Catalog service requires HMAC authentication: %v", err) + return + } + require.NoError(t, err) + } // Verify data was written addressRefs, err := catalogStore.Addresses().Fetch(ctx) diff --git a/engine/cld/catalog/catalog.go b/engine/cld/catalog/catalog.go index d771bdc7d..ea1fb8bd0 100644 --- a/engine/cld/catalog/catalog.go +++ b/engine/cld/catalog/catalog.go @@ -2,10 +2,13 @@ package catalog import ( "context" + "fmt" + "strings" fdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore" catalogremote "github.com/smartcontractkit/chainlink-deployments-framework/datastore/catalog/remote" "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/config" + cfgenv "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/config/env" "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/domain" credentials "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/internal/credentials" ) @@ -13,7 +16,7 @@ import ( // LoadCatalog loads a catalog data store for the specified domain and environment. func LoadCatalog(ctx context.Context, env string, config *config.Config, domain domain.Domain) (fdatastore.CatalogStore, error) { - catalogClient, err := loadCatalogClient(ctx, env, config.Env.Catalog.GRPC) + catalogClient, err := loadCatalogClient(ctx, env, &config.Env.Catalog) if err != nil { return nil, err } @@ -29,17 +32,50 @@ func LoadCatalog(ctx context.Context, env string, // loadCatalogClient initializes a Catalogue client using the grpc and gap config. func loadCatalogClient( - ctx context.Context, env string, url string, + ctx context.Context, env string, cfg *cfgenv.CatalogConfig, ) (*catalogremote.CatalogClient, error) { creds := credentials.GetCredsForEnv(env) - client, err := catalogremote.NewCatalogClient(ctx, catalogremote.CatalogConfig{ - GRPC: url, + catalogCfg := catalogremote.CatalogConfig{ + GRPC: cfg.GRPC, Creds: creds, - }) + } + + // Configure HMAC authentication if KMS key is provided + if cfg.Auth != nil && cfg.Auth.KMSKeyID != "" { + // Extract authority from GRPC URL (hostname without port) + authority := extractAuthority(cfg.GRPC) + fmt.Println("Authority:", authority) + + catalogCfg.HMACConfig = &catalogremote.HMACAuthConfig{ + KeyID: cfg.Auth.KMSKeyID, + KeyRegion: cfg.Auth.KMSKeyRegion, + Authority: authority, + } + } + + client, err := catalogremote.NewCatalogClient(ctx, catalogCfg) if err != nil { return nil, err } return client, nil } + +// extractAuthority extracts the authority (hostname without scheme and port) from a gRPC URL. +// Examples: +// - "grpc.example.com:443" -> "grpc.example.com" +// - "https://grpc.example.com:443" -> "grpc.example.com" +// - "grpc.example.com" -> "grpc.example.com" +func extractAuthority(grpcURL string) string { + // Remove scheme if present + authority := strings.TrimPrefix(grpcURL, "https://") + authority = strings.TrimPrefix(authority, "http://") + + // Remove port if present + if idx := strings.LastIndex(authority, ":"); idx != -1 { + authority = authority[:idx] + } + + return authority +} diff --git a/engine/cld/catalog/catalog_test.go b/engine/cld/catalog/catalog_test.go index 1602fe313..7161b710b 100644 --- a/engine/cld/catalog/catalog_test.go +++ b/engine/cld/catalog/catalog_test.go @@ -75,23 +75,40 @@ func TestLoadCatalogClient(t *testing.T) { tests := []struct { name string env string - url string + cfg *cfgenv.CatalogConfig wantErr string }{ { name: "successful client creation with local env", env: "local", - url: "localhost:50051", + cfg: &cfgenv.CatalogConfig{ + GRPC: "localhost:50051", + }, }, { name: "successful client creation with non-local env", env: "testnet", - url: "grpc.example.com:443", + cfg: &cfgenv.CatalogConfig{ + GRPC: "grpc.example.com:443", + }, }, { name: "empty url - should still create client", env: "testnet", - url: "", + cfg: &cfgenv.CatalogConfig{ + GRPC: "", + }, + }, + { + name: "client with HMAC authentication", + env: "testnet", + cfg: &cfgenv.CatalogConfig{ + GRPC: "grpc.example.com:443", + Auth: &cfgenv.CatalogAuthConfig{ + KMSKeyID: "test-key-id", + KMSKeyRegion: "us-west-2", + }, + }, }, } @@ -101,7 +118,7 @@ func TestLoadCatalogClient(t *testing.T) { ctx := t.Context() - result, err := loadCatalogClient(ctx, tt.env, tt.url) + result, err := loadCatalogClient(ctx, tt.env, tt.cfg) if tt.wantErr != "" { require.Error(t, err) @@ -117,3 +134,53 @@ func TestLoadCatalogClient(t *testing.T) { }) } } + +func TestExtractAuthority(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + grpcURL string + expected string + }{ + { + name: "hostname with port", + grpcURL: "grpc.example.com:443", + expected: "grpc.example.com", + }, + { + name: "hostname without port", + grpcURL: "grpc.example.com", + expected: "grpc.example.com", + }, + { + name: "https scheme with port", + grpcURL: "https://grpc.example.com:443", + expected: "grpc.example.com", + }, + { + name: "http scheme with port", + grpcURL: "http://grpc.example.com:8080", + expected: "grpc.example.com", + }, + { + name: "localhost with port", + grpcURL: "localhost:50051", + expected: "localhost", + }, + { + name: "localhost without port", + grpcURL: "localhost", + expected: "localhost", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := extractAuthority(tt.grpcURL) + require.Equal(t, tt.expected, result) + }) + } +} diff --git a/engine/cld/config/env/config.go b/engine/cld/config/env/config.go index 3aad0e368..efb83797c 100644 --- a/engine/cld/config/env/config.go +++ b/engine/cld/config/env/config.go @@ -111,9 +111,16 @@ type OCRConfig struct { XProposers string `mapstructure:"x_proposers" yaml:"x_proposers"` // Secret: BIP39 mnemonic phrase for the OCR proposer. } +// CatalogAuthConfig is the configuration for the Catalog authentication. +type CatalogAuthConfig struct { + KMSKeyID string `mapstructure:"kms_key_id" yaml:"kms_key_id"` // AWS KMS Key ID (arn or alias) + KMSKeyRegion string `mapstructure:"kms_key_region" yaml:"kms_key_region"` // AWS KMS Key Region (e.g. us-west-1) +} + // CatalogConfig is the configuration to connect to the Catalog. type CatalogConfig struct { - GRPC string `mapstructure:"grpc" yaml:"grpc"` // The gRPC URL for the Catalog. Used to interact with the Catalog API. + GRPC string `mapstructure:"grpc" yaml:"grpc"` // The gRPC URL for the Catalog. Used to interact with the Catalog API. + Auth *CatalogAuthConfig `mapstructure:"auth" yaml:"auth,omitempty"` // The authentication configuration for the Catalog. } // OnchainConfig wraps the configuration for the onchain components. diff --git a/go.mod b/go.mod index 4127c5550..14339ebe4 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,8 @@ require ( github.com/aptos-labs/aptos-go-sdk v1.9.1-0.20250613185448-581cb03acb8f github.com/avast/retry-go/v4 v4.6.1 github.com/aws/aws-sdk-go v1.55.7 - github.com/aws/aws-sdk-go-v2 v1.38.1 - github.com/aws/aws-sdk-go-v2/config v1.28.0 + github.com/aws/aws-sdk-go-v2 v1.39.6 + github.com/aws/aws-sdk-go-v2/config v1.31.17 github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.57.1 github.com/block-vision/sui-go-sdk v1.1.2 github.com/cosmos/go-bip39 v1.0.0 @@ -57,6 +57,7 @@ require ( ) require ( + github.com/aws/aws-sdk-go-v2/service/kms v1.47.1 // indirect github.com/btcsuite/btcutil v1.0.2 // indirect github.com/lib/pq v1.10.9 // indirect github.com/smartcontractkit/chainlink-common v0.9.1-0.20250815142532-64e0a7965958 // indirect @@ -76,17 +77,17 @@ require ( github.com/XSAM/otelsql v0.37.0 // indirect github.com/avast/retry-go v3.0.0+incompatible // indirect github.com/awalterschulze/gographviz v2.0.3+incompatible // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.41 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 // indirect - github.com/aws/smithy-go v1.22.5 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect + github.com/aws/smithy-go v1.23.2 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect diff --git a/go.sum b/go.sum index 9de7c4d42..1ce11e19b 100644 --- a/go.sum +++ b/go.sum @@ -41,32 +41,60 @@ github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.38.1 h1:j7sc33amE74Rz0M/PoCpsZQ6OunLqys/m5antM0J+Z8= github.com/aws/aws-sdk-go-v2 v1.38.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= +github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk= +github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= github.com/aws/aws-sdk-go-v2/config v1.28.0 h1:FosVYWcqEtWNxHn8gB/Vs6jOlNwSoyOCA/g/sxyySOQ= github.com/aws/aws-sdk-go-v2/config v1.28.0/go.mod h1:pYhbtvg1siOOg8h5an77rXle9tVG8T+BWLWAo7cOukc= +github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y= +github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c= github.com/aws/aws-sdk-go-v2/credentials v1.17.41 h1:7gXo+Axmp+R4Z+AK8YFQO0ZV3L0gizGINCOWxSLY9W8= github.com/aws/aws-sdk-go-v2/credentials v1.17.41/go.mod h1:u4Eb8d3394YLubphT4jLEwN1rLNq2wFOlT6OuxFwPzU= +github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA= +github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 h1:TMH3f/SCAWdNtXXVPPu5D6wrr4G5hI1rAxbcocKfC7Q= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17/go.mod h1:1ZRXLdTpzdJb9fwTMXiLipENRxkGMTn1sfKexGllQCw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 h1:IdCLsiiIj5YJ3AFevsewURCPV+YWUlOW8JiPhoAy8vg= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4/go.mod h1:l4bdfCD7XyyZA9BolKBo1eLqgaJxl0/x91PL4Yqe0ao= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 h1:j7vjtr1YIssWQOMeOWRbh3z8g2oY/xPjnZH2gLY4sGw= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4/go.mod h1:yDmJgqOiH4EA8Hndnv4KwAo8jCGTSnM5ASG1nBI+toA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.57.1 h1:gKFnV8HEJomx4XFOVBXRUA5hphkhpnUjqJsYPCc9K8Q= github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.57.1/go.mod h1:+UxryRSMGMtqsvxdnws+VpNyFYWRkw4ZlM+5AC160XA= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 h1:s7NA1SOw8q/5c0wr8477yOPp0z+uBaXBnLE0XYb0POA= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2/go.mod h1:fnjjWyAW/Pj5HYOxl9LJqWtEwS7W2qgcRLWP+uWbss0= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= +github.com/aws/aws-sdk-go-v2/service/kms v1.47.1 h1:6+C0RoGF4HJQALrsecOXN7cm/l5rgNHCw2xbcvFgpH4= +github.com/aws/aws-sdk-go-v2/service/kms v1.47.1/go.mod h1:VJcNH6BLr+3VJwinRKdotLOMglHO8mIKlD3ea5c7hbw= github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 h1:bSYXVyUzoTHoKalBmwaZxs97HU9DWWI3ehHSAMa7xOk= github.com/aws/aws-sdk-go-v2/service/sso v1.24.2/go.mod h1:skMqY7JElusiOUjMJMOv1jJsP7YUg7DrhgqZZWuzu1U= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 h1:AhmO1fHINP9vFYUE0LHzCWg/LfUWUF+zFPEcY9QXb7o= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2/go.mod h1:o8aQygT2+MVP0NaV6kbdE1YnnIM8RRVQzoeUH45GOdI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 h1:CiS7i0+FUe+/YY1GvIBLLrR/XNGZ4CtM1Ll0XavNuVo= github.com/aws/aws-sdk-go-v2/service/sts v1.32.2/go.mod h1:HtaiBI8CjYoNVde8arShXb94UbQQi9L4EMr6D+xGBwo= +github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs= +github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= +github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0= From 12f78cd2e36074e4c36e8fc938074ef26072234f Mon Sep 17 00:00:00 2001 From: Giorgio Gambino <151543+giogam@users.noreply.github.com> Date: Fri, 7 Nov 2025 22:02:54 +0000 Subject: [PATCH 02/13] Create flat-moose-check.md --- .changeset/flat-moose-check.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/flat-moose-check.md diff --git a/.changeset/flat-moose-check.md b/.changeset/flat-moose-check.md new file mode 100644 index 000000000..51f3c178b --- /dev/null +++ b/.changeset/flat-moose-check.md @@ -0,0 +1,5 @@ +--- +"chainlink-deployments-framework": minor +--- + +feat: adds HMAC authentication support for catalog remote From 9911d95c7b29f8f5e1dcaecbb6ad6bb26e08294a Mon Sep 17 00:00:00 2001 From: giogam <151543+giogam@users.noreply.github.com> Date: Sun, 9 Nov 2025 12:27:00 +0100 Subject: [PATCH 03/13] chore: refactor hmac auth logic into its own file --- datastore/catalog/remote/grpc.go | 104 --------- datastore/catalog/remote/hmac_auth.go | 123 ++++++++++ datastore/catalog/remote/hmac_auth_test.go | 251 +++++++++++++++++++++ 3 files changed, 374 insertions(+), 104 deletions(-) create mode 100644 datastore/catalog/remote/hmac_auth.go create mode 100644 datastore/catalog/remote/hmac_auth_test.go diff --git a/datastore/catalog/remote/grpc.go b/datastore/catalog/remote/grpc.go index e395d8d4c..01c657f5a 100644 --- a/datastore/catalog/remote/grpc.go +++ b/datastore/catalog/remote/grpc.go @@ -2,20 +2,11 @@ package remote import ( "context" - "crypto/sha256" - "encoding/hex" "fmt" - "strconv" - "time" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/aws/aws-sdk-go-v2/service/kms/types" pb "github.com/smartcontractkit/chainlink-protos/op-catalog/v1/datastore" "google.golang.org/grpc" "google.golang.org/grpc/credentials" - "google.golang.org/grpc/metadata" "google.golang.org/protobuf/proto" ) @@ -135,98 +126,3 @@ func newCatalogConnection(cfg CatalogConfig) (*grpc.ClientConn, error) { return conn, nil } - -const ( - // dataAccessMethod is the full gRPC method name for DataAccess - dataAccessMethod = "/op_catalog.v1.datastore.Datastore/DataAccess" -) - -// HMACAuthConfig holds HMAC authentication configuration. -type HMACAuthConfig struct { - KeyID string - KeyRegion string - Authority string // The gRPC authority (hostname without port) used for HMAC signing -} - -// prepareHMACContext prepares the context with HMAC authentication metadata. -// It loads AWS KMS configuration, creates a KMS client, generates an HMAC signature, -// and attaches it to the outgoing gRPC metadata. -func (c *CatalogClient) prepareHMACContext(ctx context.Context, req proto.Message) (context.Context, error) { - // Load AWS configuration - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(c.hmacConfig.KeyRegion)) - if err != nil { - return nil, fmt.Errorf("failed to load AWS config: %w", err) - } - - // Create KMS client - kmsClient := kms.NewFromConfig(cfg) - - // Create HMAC helper - hmacHelper := &kmsHMACClientHelper{ - kmsClient: kmsClient, - keyID: c.hmacConfig.KeyID, - } - - // Marshal the message to bytes for HMAC - payload, err := proto.Marshal(req) - if err != nil { - return nil, fmt.Errorf("failed to marshal message for HMAC: %w", err) - } - - // Generate HMAC signature and timestamp - signature, timestamp, err := hmacHelper.generateHMACSignature(ctx, dataAccessMethod, c.hmacConfig.Authority, payload) - if err != nil { - return nil, fmt.Errorf("failed to generate HMAC signature: %w", err) - } - - // Add HMAC authentication to gRPC metadata - md := metadata.Pairs( - "x-hmac-signature", signature, - "x-hmac-timestamp", timestamp, - ) - - // Merge with existing metadata if present - if existingMd, ok := metadata.FromOutgoingContext(ctx); ok { - md = metadata.Join(existingMd, md) - } - - return metadata.NewOutgoingContext(ctx, md), nil -} - -// kmsHMACClientHelper helps clients generate HMAC signatures using AWS KMS. -type kmsHMACClientHelper struct { - kmsClient *kms.Client - keyID string -} - -// generateHMACSignature generates an HMAC signature and timestamp for the given request. -// Returns the hex-encoded signature and Unix timestamp as strings. -// The caller is responsible for adding these to transport-specific metadata/headers. -func (h *kmsHMACClientHelper) generateHMACSignature(ctx context.Context, method string, authority string, payload []byte) (signature string, timestamp string, err error) { - timestamp = strconv.FormatInt(time.Now().Unix(), 10) - - // Hash the payload with SHA-256 to stay within KMS message size limits (4096 bytes) - // and to have a predictable signature length - payloadHash := sha256.Sum256(payload) - - // Construct HMAC message using method path, authority, timestamp, and payload hash - // Format: method\nauthority\ntimestamp\nsha256(payload) - messagePrefix := fmt.Sprintf("%s\n%s\n%s\n", method, authority, timestamp) - fullMessage := append([]byte(messagePrefix), payloadHash[:]...) - - // Generate MAC using KMS with HMAC_SHA_256 - generateInput := &kms.GenerateMacInput{ - KeyId: aws.String(h.keyID), - Message: fullMessage, - MacAlgorithm: types.MacAlgorithmSpecHmacSha256, - } - - generateOutput, err := h.kmsClient.GenerateMac(ctx, generateInput) - if err != nil { - return "", "", fmt.Errorf("failed to generate MAC: %w", err) - } - - signature = hex.EncodeToString(generateOutput.Mac) - - return signature, timestamp, nil -} diff --git a/datastore/catalog/remote/hmac_auth.go b/datastore/catalog/remote/hmac_auth.go new file mode 100644 index 000000000..b430eacd3 --- /dev/null +++ b/datastore/catalog/remote/hmac_auth.go @@ -0,0 +1,123 @@ +package remote + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "strconv" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/kms" + "github.com/aws/aws-sdk-go-v2/service/kms/types" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/proto" +) + +const ( + // dataAccessMethod is the full gRPC method name for DataAccess + dataAccessMethod = "/op_catalog.v1.datastore.Datastore/DataAccess" +) + +// HMACAuthConfig holds HMAC authentication configuration. +type HMACAuthConfig struct { + KeyID string + KeyRegion string + Authority string // The gRPC authority (hostname without port) used for HMAC signing +} + +// prepareHMACContext prepares the context with HMAC authentication metadata. +// It loads AWS KMS configuration, creates a KMS client, generates an HMAC signature, +// and attaches it to the outgoing gRPC metadata. +func (c *CatalogClient) prepareHMACContext(ctx context.Context, req proto.Message) (context.Context, error) { + // Load AWS configuration + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(c.hmacConfig.KeyRegion)) + if err != nil { + return nil, fmt.Errorf("failed to load AWS config: %w", err) + } + + // Create KMS client + kmsClient := kms.NewFromConfig(cfg) + + return c.prepareHMACContextWithClient(ctx, req, kmsClient) +} + +// prepareHMACContextWithClient prepares the context with HMAC authentication metadata using the provided KMS client. +// This method is extracted for testability. +func (c *CatalogClient) prepareHMACContextWithClient(ctx context.Context, req proto.Message, client kmsClient) (context.Context, error) { + // Create HMAC helper + hmacHelper := &kmsHMACClientHelper{ + kmsClient: client, + keyID: c.hmacConfig.KeyID, + } + + // Marshal the message to bytes for HMAC + payload, err := proto.Marshal(req) + if err != nil { + return nil, fmt.Errorf("failed to marshal message for HMAC: %w", err) + } + + // Generate HMAC signature and timestamp + signature, timestamp, err := hmacHelper.generateHMACSignature(ctx, dataAccessMethod, c.hmacConfig.Authority, payload) + if err != nil { + return nil, fmt.Errorf("failed to generate HMAC signature: %w", err) + } + + // Add HMAC authentication to gRPC metadata + md := metadata.Pairs( + "x-hmac-signature", signature, + "x-hmac-timestamp", timestamp, + ) + + // Merge with existing metadata if present + if existingMd, ok := metadata.FromOutgoingContext(ctx); ok { + md = metadata.Join(existingMd, md) + } + + return metadata.NewOutgoingContext(ctx, md), nil +} + +// kmsClient defines the interface for KMS operations needed for HMAC +type kmsClient interface { + GenerateMac(ctx context.Context, params *kms.GenerateMacInput, optFns ...func(*kms.Options)) (*kms.GenerateMacOutput, error) +} + +// kmsHMACClientHelper helps clients generate HMAC signatures using AWS KMS. +type kmsHMACClientHelper struct { + kmsClient kmsClient + keyID string +} + +// generateHMACSignature generates an HMAC signature and timestamp for the given request. +// Returns the hex-encoded signature and Unix timestamp as strings. +// The caller is responsible for adding these to transport-specific metadata/headers. +func (h *kmsHMACClientHelper) generateHMACSignature(ctx context.Context, method string, authority string, payload []byte) (signature string, timestamp string, err error) { + timestamp = strconv.FormatInt(time.Now().Unix(), 10) + + // Hash the payload with SHA-256 to stay within KMS message size limits (4096 bytes) + // and to have a predictable signature length + payloadHash := sha256.Sum256(payload) + + // Construct HMAC message using method path, authority, timestamp, and payload hash + // Format: method\nauthority\ntimestamp\nsha256(payload) + messagePrefix := fmt.Sprintf("%s\n%s\n%s\n", method, authority, timestamp) + fullMessage := append([]byte(messagePrefix), payloadHash[:]...) + + // Generate MAC using KMS with HMAC_SHA_256 + generateInput := &kms.GenerateMacInput{ + KeyId: aws.String(h.keyID), + Message: fullMessage, + MacAlgorithm: types.MacAlgorithmSpecHmacSha256, + } + + generateOutput, err := h.kmsClient.GenerateMac(ctx, generateInput) + if err != nil { + return "", "", fmt.Errorf("failed to generate MAC: %w", err) + } + + signature = hex.EncodeToString(generateOutput.Mac) + + return signature, timestamp, nil +} diff --git a/datastore/catalog/remote/hmac_auth_test.go b/datastore/catalog/remote/hmac_auth_test.go new file mode 100644 index 000000000..d3e25fa1c --- /dev/null +++ b/datastore/catalog/remote/hmac_auth_test.go @@ -0,0 +1,251 @@ +package remote + +import ( + "context" + "errors" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/kms" + "github.com/aws/aws-sdk-go-v2/service/kms/types" + pb "github.com/smartcontractkit/chainlink-protos/op-catalog/v1/datastore" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/metadata" +) + +// mockKMSClient is a mock implementation of the kmsClient interface for testing +type mockKMSClient struct { + generateMacFunc func(ctx context.Context, params *kms.GenerateMacInput, optFns ...func(*kms.Options)) (*kms.GenerateMacOutput, error) +} + +// GenerateMac implements the kmsClient interface +func (m *mockKMSClient) GenerateMac(ctx context.Context, params *kms.GenerateMacInput, optFns ...func(*kms.Options)) (*kms.GenerateMacOutput, error) { + if m.generateMacFunc != nil { + return m.generateMacFunc(ctx, params, optFns...) + } + + return nil, errors.New("mock not configured") +} + +func TestKmsHMACClientHelper_generateHMACSignature(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + keyID string + method string + authority string + payload []byte + mockGenerateMac func(ctx context.Context, params *kms.GenerateMacInput, optFns ...func(*kms.Options)) (*kms.GenerateMacOutput, error) + wantSignature string + wantErr bool + wantErrContains string + }{ + { + name: "successful signature generation", + keyID: "test-key-id", + method: "/op_catalog.v1.datastore.Datastore/DataAccess", + authority: "catalog.example.com", + payload: []byte("test payload"), + mockGenerateMac: func(ctx context.Context, params *kms.GenerateMacInput, optFns ...func(*kms.Options)) (*kms.GenerateMacOutput, error) { + // Verify inputs + assert.Equal(t, "test-key-id", *params.KeyId) + assert.Equal(t, types.MacAlgorithmSpecHmacSha256, params.MacAlgorithm) + assert.NotNil(t, params.Message) + + // Return mock MAC + mockMac := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} + + return &kms.GenerateMacOutput{ + Mac: mockMac, + }, nil + }, + wantSignature: "0102030405060708", + wantErr: false, + }, + { + name: "large payload", + keyID: "test-key-id", + method: "/op_catalog.v1.datastore.Datastore/DataAccess", + authority: "catalog.example.com", + payload: make([]byte, 10000), // 10KB payload + mockGenerateMac: func(ctx context.Context, params *kms.GenerateMacInput, optFns ...func(*kms.Options)) (*kms.GenerateMacOutput, error) { + // Verify the message length stays under KMS limits (4096 bytes) (should be hashed) + // Message format: method\nauthority\ntimestamp\nsha256(payload) + assert.Less(t, len(params.Message), 4096, "Message should be hashed to stay under KMS limits") + return &kms.GenerateMacOutput{ + Mac: []byte{0xaa, 0xbb}, + }, nil + }, + wantSignature: "aabb", + wantErr: false, + }, + { + name: "kms error", + keyID: "test-key-id", + method: "/op_catalog.v1.datastore.Datastore/DataAccess", + authority: "catalog.example.com", + payload: []byte("test payload"), + mockGenerateMac: func(ctx context.Context, params *kms.GenerateMacInput, optFns ...func(*kms.Options)) (*kms.GenerateMacOutput, error) { + return nil, errors.New("kms service unavailable") + }, + wantErr: true, + wantErrContains: "failed to generate MAC", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + mockClient := &mockKMSClient{ + generateMacFunc: tt.mockGenerateMac, + } + + helper := &kmsHMACClientHelper{ + kmsClient: mockClient, + keyID: tt.keyID, + } + + signature, timestamp, err := helper.generateHMACSignature(ctx, tt.method, tt.authority, tt.payload) + + if tt.wantErr { + require.Error(t, err) + if tt.wantErrContains != "" { + assert.Contains(t, err.Error(), tt.wantErrContains) + } + assert.Empty(t, signature) + } else { + require.NoError(t, err) + assert.Equal(t, tt.wantSignature, signature) + assert.NotEmpty(t, timestamp, "timestamp should not be empty") + // Verify timestamp is a valid unix timestamp string + assert.Regexp(t, `^\d+$`, timestamp, "timestamp should be numeric") + } + }) + } +} + +func TestCatalogClient_prepareHMACContextWithClient(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + hmacConfig *HMACAuthConfig + request *pb.DataAccessRequest + existingMeta metadata.MD + mockGenerateMac func(ctx context.Context, params *kms.GenerateMacInput, optFns ...func(*kms.Options)) (*kms.GenerateMacOutput, error) + wantErr bool + wantErrContains string + validateResult func(t *testing.T, ctx context.Context) + }{ + { + name: "successful context preparation with metadata", + hmacConfig: &HMACAuthConfig{ + KeyID: "test-key-id", + KeyRegion: "us-west-2", + Authority: "catalog.example.com", + }, + request: &pb.DataAccessRequest{}, + mockGenerateMac: func(ctx context.Context, params *kms.GenerateMacInput, optFns ...func(*kms.Options)) (*kms.GenerateMacOutput, error) { + mockMac := []byte{0x01, 0x02, 0x03, 0x04} + return &kms.GenerateMacOutput{ + Mac: mockMac, + }, nil + }, + wantErr: false, + validateResult: func(t *testing.T, ctx context.Context) { + t.Helper() + + md, ok := metadata.FromOutgoingContext(ctx) + require.True(t, ok, "metadata should be present") + assert.NotEmpty(t, md.Get("x-hmac-signature"), "signature should be present") + assert.NotEmpty(t, md.Get("x-hmac-timestamp"), "timestamp should be present") + assert.Equal(t, "01020304", md.Get("x-hmac-signature")[0]) + }, + }, + { + name: "merge with existing metadata", + hmacConfig: &HMACAuthConfig{ + KeyID: "test-key-id", + KeyRegion: "us-west-2", + Authority: "catalog.example.com", + }, + request: &pb.DataAccessRequest{}, + existingMeta: metadata.Pairs( + "existing-key", "existing-value", + "another-key", "another-value", + ), + mockGenerateMac: func(ctx context.Context, params *kms.GenerateMacInput, optFns ...func(*kms.Options)) (*kms.GenerateMacOutput, error) { + mockMac := []byte{0xaa, 0xbb} + return &kms.GenerateMacOutput{ + Mac: mockMac, + }, nil + }, + wantErr: false, + validateResult: func(t *testing.T, ctx context.Context) { + t.Helper() + + md, ok := metadata.FromOutgoingContext(ctx) + require.True(t, ok, "metadata should be present") + // Check existing metadata is preserved + assert.Equal(t, "existing-value", md.Get("existing-key")[0]) + assert.Equal(t, "another-value", md.Get("another-key")[0]) + // Check new HMAC metadata is added + assert.NotEmpty(t, md.Get("x-hmac-signature")) + assert.NotEmpty(t, md.Get("x-hmac-timestamp")) + }, + }, + { + name: "kms error propagates", + hmacConfig: &HMACAuthConfig{ + KeyID: "test-key-id", + KeyRegion: "us-west-2", + Authority: "catalog.example.com", + }, + request: &pb.DataAccessRequest{}, + mockGenerateMac: func(ctx context.Context, params *kms.GenerateMacInput, optFns ...func(*kms.Options)) (*kms.GenerateMacOutput, error) { + return nil, errors.New("kms access denied") + }, + wantErr: true, + wantErrContains: "failed to generate HMAC signature", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + // Add existing metadata to context if provided + if tt.existingMeta != nil { + ctx = metadata.NewOutgoingContext(ctx, tt.existingMeta) + } + + mockClient := &mockKMSClient{ + generateMacFunc: tt.mockGenerateMac, + } + + catalogClient := &CatalogClient{ + hmacConfig: tt.hmacConfig, + } + + resultCtx, err := catalogClient.prepareHMACContextWithClient(ctx, tt.request, mockClient) + + if tt.wantErr { + require.Error(t, err) + if tt.wantErrContains != "" { + assert.Contains(t, err.Error(), tt.wantErrContains) + } + } else { + require.NoError(t, err) + require.NotNil(t, resultCtx) + if tt.validateResult != nil { + tt.validateResult(t, resultCtx) + } + } + }) + } +} From 85b3e4528a414a1770bbab8c3497deed5a963847 Mon Sep 17 00:00:00 2001 From: giogam <151543+giogam@users.noreply.github.com> Date: Sun, 9 Nov 2025 19:44:50 +0100 Subject: [PATCH 04/13] feat: updates env config loading --- engine/cld/config/env/config.go | 2 ++ engine/cld/config/env/config_test.go | 12 ++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/engine/cld/config/env/config.go b/engine/cld/config/env/config.go index efb83797c..a86651bd4 100644 --- a/engine/cld/config/env/config.go +++ b/engine/cld/config/env/config.go @@ -237,6 +237,8 @@ var ( "offchain.ocr.x_signers": {"OFFCHAIN_OCR_X_SIGNERS", "OCR_X_SIGNERS"}, "offchain.ocr.x_proposers": {"OFFCHAIN_OCR_X_PROPOSERS", "OCR_X_PROPOSERS"}, "catalog.grpc": {"CATALOG_GRPC", "CATALOG_SERVICE_GRPC"}, + "catalog.auth.kms_key_id": {"CATALOG_AUTH_KMS_KEY_ID", "CATALOG_SERVICE_AUTH_KMS_KEY_ID"}, + "catalog.auth.kms_key_region": {"CATALOG_AUTH_KMS_KEY_REGION", "CATALOG_SERVICE_AUTH_KMS_KEY_REGION"}, } ) diff --git a/engine/cld/config/env/config_test.go b/engine/cld/config/env/config_test.go index 090d10f39..258fc09d9 100644 --- a/engine/cld/config/env/config_test.go +++ b/engine/cld/config/env/config_test.go @@ -90,6 +90,8 @@ var ( "OFFCHAIN_OCR_X_SIGNERS": "awkward bat", "OFFCHAIN_OCR_X_PROPOSERS": "caring deer", "CATALOG_GRPC": "http://localhost:8080", + "CATALOG_AUTH_KMS_KEY_ID": "123", + "CATALOG_AUTH_KMS_KEY_REGION": "us-east-1", } legacyEnvVars = map[string]string{ @@ -114,8 +116,10 @@ var ( "OCR_X_PROPOSERS": "caring deer", "CATALOG_SERVICE_GRPC": "http://localhost:8080", // These values do not have a legacy equivalent - "ONCHAIN_TON_DEPLOYER_KEY": "0x123", - "ONCHAIN_TON_WALLET_VERSION": "V5R1", + "ONCHAIN_TON_DEPLOYER_KEY": "0x123", + "ONCHAIN_TON_WALLET_VERSION": "V5R1", + "CATALOG_SERVICE_AUTH_KMS_KEY_ID": "123", + "CATALOG_SERVICE_AUTH_KMS_KEY_REGION": "us-east-1", } // envCfg is the config that is loaded from the environment variables. @@ -171,6 +175,10 @@ var ( }, Catalog: CatalogConfig{ GRPC: "http://localhost:8080", + Auth: &CatalogAuthConfig{ + KMSKeyID: "123", + KMSKeyRegion: "us-east-1", + }, }, } ) From 3ac1af95c5dcc588581e89420b932b800debc7b8 Mon Sep 17 00:00:00 2001 From: giogam <151543+giogam@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:12:07 +0100 Subject: [PATCH 05/13] fix: solve potential race condition --- datastore/catalog/remote/grpc.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/datastore/catalog/remote/grpc.go b/datastore/catalog/remote/grpc.go index 01c657f5a..71e931a4a 100644 --- a/datastore/catalog/remote/grpc.go +++ b/datastore/catalog/remote/grpc.go @@ -3,6 +3,7 @@ package remote import ( "context" "fmt" + "sync" pb "github.com/smartcontractkit/chainlink-protos/op-catalog/v1/datastore" "google.golang.org/grpc" @@ -21,31 +22,34 @@ type CatalogClient struct { // passing context down the call-stack. // //nolint:containedctx - ctx context.Context - cachedStream grpc.BidiStreamingClient[pb.DataAccessRequest, pb.DataAccessResponse] - hmacConfig *HMACAuthConfig + ctx context.Context + cachedStream grpc.BidiStreamingClient[pb.DataAccessRequest, pb.DataAccessResponse] + hmacConfig *HMACAuthConfig + streamInitOnce sync.Once + streamInitErr error } func (c *CatalogClient) DataAccess(req proto.Message) (grpc.BidiStreamingClient[pb.DataAccessRequest, pb.DataAccessResponse], error) { - if c.cachedStream == nil { - // Apply HMAC signature if enabled + c.streamInitOnce.Do(func() { ctx := c.ctx if c.hmacConfig != nil { var err error ctx, err = c.prepareHMACContext(c.ctx, req) if err != nil { - return nil, fmt.Errorf("failed to prepare HMAC context: %w", err) + c.streamInitErr = fmt.Errorf("failed to prepare HMAC context: %w", err) + return } } stream, err := c.protoClient.DataAccess(ctx) if err != nil { - return nil, err + c.streamInitErr = err + return } c.cachedStream = stream - } + }) - return c.cachedStream, nil + return c.cachedStream, c.streamInitErr } func (c *CatalogClient) CloseStream() error { From b36781d02607aad13257e0c99c0b39d3c1a1e7b8 Mon Sep 17 00:00:00 2001 From: giogam <151543+giogam@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:22:25 +0100 Subject: [PATCH 06/13] chore: refactor NewCatalogClient --- datastore/catalog/remote/grpc.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/datastore/catalog/remote/grpc.go b/datastore/catalog/remote/grpc.go index 71e931a4a..8e2674b7f 100644 --- a/datastore/catalog/remote/grpc.go +++ b/datastore/catalog/remote/grpc.go @@ -86,20 +86,19 @@ type CatalogConfig struct { // } // client, err := NewCatalogClient(ctx, cfg) func NewCatalogClient(ctx context.Context, cfg CatalogConfig) (*CatalogClient, error) { - client := &CatalogClient{ - ctx: ctx, - hmacConfig: cfg.HMACConfig, - } - - // Create connection with the configured options + // Create connection with the configured options. conn, err := newCatalogConnection(cfg) if err != nil { - return nil, fmt.Errorf("failed to connect Catalog service. Err: %w", err) + return &CatalogClient{}, fmt.Errorf("failed to connect Catalog service. Err: %w", err) } - client.protoClient = pb.NewDatastoreClient(conn) + client := CatalogClient{ + ctx: ctx, + hmacConfig: cfg.HMACConfig, + protoClient: pb.NewDatastoreClient(conn), + } - return client, nil + return &client, nil } // newCatalogConnection creates a new gRPC connection to the Catalog service. From d392d17c51672b03529d9ebf2a16e1d2e0d8f669 Mon Sep 17 00:00:00 2001 From: giogam <151543+giogam@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:26:03 +0100 Subject: [PATCH 07/13] chore: improve set authority comment --- datastore/catalog/remote/grpc.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/datastore/catalog/remote/grpc.go b/datastore/catalog/remote/grpc.go index 8e2674b7f..0d34032bf 100644 --- a/datastore/catalog/remote/grpc.go +++ b/datastore/catalog/remote/grpc.go @@ -111,8 +111,10 @@ func newCatalogConnection(cfg CatalogConfig) (*grpc.ClientConn, error) { } if cfg.HMACConfig != nil { - // Force authority header to be set to match what's used in the HMAC signature. - // This ensures the server verifies against the same authority we signed with. + // Force authority header to be set to match what's used in the HMAC signature, this ensures the server verifies against the + // same authority we signed with. If not set explicitly, the authority is derived from the grpc URL, which may not match the + // authority used in the HMAC signature since gRPC clients take some liberties with the authority header like removing the port + // E.g. if it is default 443, the authority header will be "grpc.example.com" instead of "grpc.example.com:443" // see: https://github.com/grpc/grpc-go/blob/7472d578b15f718cbe8ca0f5f5a3713093c47b03/internal/transport/http2_client.go#L653 // see: https://github.com/grpc/grpc-go/blob/7472d578b15f718cbe8ca0f5f5a3713093c47b03/internal/transport/http2_client.go#L533 opts = append(opts, grpc.WithAuthority(cfg.HMACConfig.Authority)) From cce024cb326fc79bc03f2353b9cae6fcee469b62 Mon Sep 17 00:00:00 2001 From: giogam <151543+giogam@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:44:21 +0100 Subject: [PATCH 08/13] feat: only init client once --- datastore/catalog/remote/grpc.go | 5 ++++- datastore/catalog/remote/hmac_auth.go | 23 +++++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/datastore/catalog/remote/grpc.go b/datastore/catalog/remote/grpc.go index 0d34032bf..b9ec05ceb 100644 --- a/datastore/catalog/remote/grpc.go +++ b/datastore/catalog/remote/grpc.go @@ -27,6 +27,9 @@ type CatalogClient struct { hmacConfig *HMACAuthConfig streamInitOnce sync.Once streamInitErr error + kmsClient kmsClient + kmsClientOnce sync.Once + kmsClientErr error } func (c *CatalogClient) DataAccess(req proto.Message) (grpc.BidiStreamingClient[pb.DataAccessRequest, pb.DataAccessResponse], error) { @@ -89,7 +92,7 @@ func NewCatalogClient(ctx context.Context, cfg CatalogConfig) (*CatalogClient, e // Create connection with the configured options. conn, err := newCatalogConnection(cfg) if err != nil { - return &CatalogClient{}, fmt.Errorf("failed to connect Catalog service. Err: %w", err) + return nil, fmt.Errorf("failed to connect Catalog service. Err: %w", err) } client := CatalogClient{ diff --git a/datastore/catalog/remote/hmac_auth.go b/datastore/catalog/remote/hmac_auth.go index b430eacd3..deb31de65 100644 --- a/datastore/catalog/remote/hmac_auth.go +++ b/datastore/catalog/remote/hmac_auth.go @@ -28,19 +28,30 @@ type HMACAuthConfig struct { Authority string // The gRPC authority (hostname without port) used for HMAC signing } +// getKMSClient initializes and returns the KMS client, using sync.Once to ensure +// the AWS config is loaded only once per CatalogClient instance. +func (c *CatalogClient) getKMSClient(ctx context.Context) (kmsClient, error) { + c.kmsClientOnce.Do(func() { + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(c.hmacConfig.KeyRegion)) + if err != nil { + c.kmsClientErr = fmt.Errorf("failed to load AWS config: %w", err) + return + } + c.kmsClient = kms.NewFromConfig(cfg) + }) + return c.kmsClient, c.kmsClientErr +} + // prepareHMACContext prepares the context with HMAC authentication metadata. // It loads AWS KMS configuration, creates a KMS client, generates an HMAC signature, // and attaches it to the outgoing gRPC metadata. func (c *CatalogClient) prepareHMACContext(ctx context.Context, req proto.Message) (context.Context, error) { - // Load AWS configuration - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(c.hmacConfig.KeyRegion)) + // Get or initialize KMS client (cached via sync.Once) + kmsClient, err := c.getKMSClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to load AWS config: %w", err) + return nil, err } - // Create KMS client - kmsClient := kms.NewFromConfig(cfg) - return c.prepareHMACContextWithClient(ctx, req, kmsClient) } From 18f273a558ff1e75d0a8f899a244a6b6d964e3f4 Mon Sep 17 00:00:00 2001 From: giogam <151543+giogam@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:44:49 +0100 Subject: [PATCH 09/13] chore: removes leftover print --- engine/cld/catalog/catalog.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/engine/cld/catalog/catalog.go b/engine/cld/catalog/catalog.go index ea1fb8bd0..387c353e8 100644 --- a/engine/cld/catalog/catalog.go +++ b/engine/cld/catalog/catalog.go @@ -2,7 +2,6 @@ package catalog import ( "context" - "fmt" "strings" fdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore" @@ -45,7 +44,6 @@ func loadCatalogClient( if cfg.Auth != nil && cfg.Auth.KMSKeyID != "" { // Extract authority from GRPC URL (hostname without port) authority := extractAuthority(cfg.GRPC) - fmt.Println("Authority:", authority) catalogCfg.HMACConfig = &catalogremote.HMACAuthConfig{ KeyID: cfg.Auth.KMSKeyID, From 4a76a72b4d9a385dc9269881ee4724085a6e9260 Mon Sep 17 00:00:00 2001 From: giogam <151543+giogam@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:14:59 +0100 Subject: [PATCH 10/13] chore: fix linter --- datastore/catalog/remote/hmac_auth.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/datastore/catalog/remote/hmac_auth.go b/datastore/catalog/remote/hmac_auth.go index deb31de65..322830d96 100644 --- a/datastore/catalog/remote/hmac_auth.go +++ b/datastore/catalog/remote/hmac_auth.go @@ -39,6 +39,7 @@ func (c *CatalogClient) getKMSClient(ctx context.Context) (kmsClient, error) { } c.kmsClient = kms.NewFromConfig(cfg) }) + return c.kmsClient, c.kmsClientErr } @@ -47,12 +48,12 @@ func (c *CatalogClient) getKMSClient(ctx context.Context) (kmsClient, error) { // and attaches it to the outgoing gRPC metadata. func (c *CatalogClient) prepareHMACContext(ctx context.Context, req proto.Message) (context.Context, error) { // Get or initialize KMS client (cached via sync.Once) - kmsClient, err := c.getKMSClient(ctx) + client, err := c.getKMSClient(ctx) if err != nil { return nil, err } - return c.prepareHMACContextWithClient(ctx, req, kmsClient) + return c.prepareHMACContextWithClient(ctx, req, client) } // prepareHMACContextWithClient prepares the context with HMAC authentication metadata using the provided KMS client. From 61f87c0b1c4e923dadbfd8a3b26ca2d1995ad422 Mon Sep 17 00:00:00 2001 From: giogam <151543+giogam@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:54:08 +0100 Subject: [PATCH 11/13] chore: add coverage for HMACConfig != nil --- datastore/catalog/remote/grpc.go | 12 ++++++------ datastore/catalog/remote/grpc_test.go | 13 +++++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/datastore/catalog/remote/grpc.go b/datastore/catalog/remote/grpc.go index b9ec05ceb..6c31b397c 100644 --- a/datastore/catalog/remote/grpc.go +++ b/datastore/catalog/remote/grpc.go @@ -113,13 +113,13 @@ func newCatalogConnection(cfg CatalogConfig) (*grpc.ClientConn, error) { opts = append(opts, grpc.WithTransportCredentials(cfg.Creds)) } + // Force authority header to be set to match what's used in the HMAC signature, this ensures the server verifies against the + // same authority we signed with. If not set explicitly, the authority is derived from the grpc URL, which may not match the + // authority used in the HMAC signature since gRPC clients take some liberties with the authority header like removing the port + // E.g. if it is default 443, the authority header will be "grpc.example.com" instead of "grpc.example.com:443" + // see: https://github.com/grpc/grpc-go/blob/7472d578b15f718cbe8ca0f5f5a3713093c47b03/internal/transport/http2_client.go#L653 + // see: https://github.com/grpc/grpc-go/blob/7472d578b15f718cbe8ca0f5f5a3713093c47b03/internal/transport/http2_client.go#L533 if cfg.HMACConfig != nil { - // Force authority header to be set to match what's used in the HMAC signature, this ensures the server verifies against the - // same authority we signed with. If not set explicitly, the authority is derived from the grpc URL, which may not match the - // authority used in the HMAC signature since gRPC clients take some liberties with the authority header like removing the port - // E.g. if it is default 443, the authority header will be "grpc.example.com" instead of "grpc.example.com:443" - // see: https://github.com/grpc/grpc-go/blob/7472d578b15f718cbe8ca0f5f5a3713093c47b03/internal/transport/http2_client.go#L653 - // see: https://github.com/grpc/grpc-go/blob/7472d578b15f718cbe8ca0f5f5a3713093c47b03/internal/transport/http2_client.go#L533 opts = append(opts, grpc.WithAuthority(cfg.HMACConfig.Authority)) } diff --git a/datastore/catalog/remote/grpc_test.go b/datastore/catalog/remote/grpc_test.go index 28c739119..dde5f4dae 100644 --- a/datastore/catalog/remote/grpc_test.go +++ b/datastore/catalog/remote/grpc_test.go @@ -36,6 +36,19 @@ func TestNewCatalogClient_Success(t *testing.T) { expectError: true, errorContains: "no transport security set", }, + { + name: "config_with_hmac_auth", + config: remote.CatalogConfig{ + GRPC: "localhost:9090", + Creds: insecure.NewCredentials(), + HMACConfig: &remote.HMACAuthConfig{ + KeyID: "test-key-id", + KeyRegion: "us-west-2", + Authority: "catalog.example.com", + }, + }, + expectError: false, + }, } for _, tt := range tests { From 75a227e23c8da59323b5765af6ab852b059783ca Mon Sep 17 00:00:00 2001 From: giogam <151543+giogam@users.noreply.github.com> Date: Mon, 10 Nov 2025 17:05:00 +0100 Subject: [PATCH 12/13] chore: removes unused interceptors variable and dead code The interceptors slice was declared but never populated, making the conditional check and append operation dead code. This cleanup removes both the unused variable declaration and the unreachable code block. --- datastore/catalog/remote/grpc.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/datastore/catalog/remote/grpc.go b/datastore/catalog/remote/grpc.go index 6c31b397c..ecb75fc04 100644 --- a/datastore/catalog/remote/grpc.go +++ b/datastore/catalog/remote/grpc.go @@ -107,7 +107,6 @@ func NewCatalogClient(ctx context.Context, cfg CatalogConfig) (*CatalogClient, e // newCatalogConnection creates a new gRPC connection to the Catalog service. func newCatalogConnection(cfg CatalogConfig) (*grpc.ClientConn, error) { var opts []grpc.DialOption - var interceptors []grpc.UnaryClientInterceptor if cfg.Creds != nil { opts = append(opts, grpc.WithTransportCredentials(cfg.Creds)) @@ -123,10 +122,6 @@ func newCatalogConnection(cfg CatalogConfig) (*grpc.ClientConn, error) { opts = append(opts, grpc.WithAuthority(cfg.HMACConfig.Authority)) } - if len(interceptors) > 0 { - opts = append(opts, grpc.WithChainUnaryInterceptor(interceptors...)) - } - conn, err := grpc.NewClient(cfg.GRPC, opts...) if err != nil { return nil, err From af656db8bc8fab5b0f01dc5962a491716cdbf695 Mon Sep 17 00:00:00 2001 From: giogam <151543+giogam@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:53:27 +0100 Subject: [PATCH 13/13] chore: remove legacy env variables --- engine/cld/config/env/config.go | 6 +++--- engine/cld/config/env/config_test.go | 15 ++++++++++----- engine/cld/config/env/testdata/config.yml | 3 +++ engine/cld/config/env_test.go | 6 ++++-- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/engine/cld/config/env/config.go b/engine/cld/config/env/config.go index a86651bd4..b79f516ea 100644 --- a/engine/cld/config/env/config.go +++ b/engine/cld/config/env/config.go @@ -236,9 +236,9 @@ var ( "offchain.job_distributor.endpoints.grpc": {"OFFCHAIN_JD_ENDPOINTS_GRPC", "JD_GRPC"}, "offchain.ocr.x_signers": {"OFFCHAIN_OCR_X_SIGNERS", "OCR_X_SIGNERS"}, "offchain.ocr.x_proposers": {"OFFCHAIN_OCR_X_PROPOSERS", "OCR_X_PROPOSERS"}, - "catalog.grpc": {"CATALOG_GRPC", "CATALOG_SERVICE_GRPC"}, - "catalog.auth.kms_key_id": {"CATALOG_AUTH_KMS_KEY_ID", "CATALOG_SERVICE_AUTH_KMS_KEY_ID"}, - "catalog.auth.kms_key_region": {"CATALOG_AUTH_KMS_KEY_REGION", "CATALOG_SERVICE_AUTH_KMS_KEY_REGION"}, + "catalog.grpc": {"CATALOG_GRPC"}, + "catalog.auth.kms_key_id": {"CATALOG_AUTH_KMS_KEY_ID"}, + "catalog.auth.kms_key_region": {"CATALOG_AUTH_KMS_KEY_REGION"}, } ) diff --git a/engine/cld/config/env/config_test.go b/engine/cld/config/env/config_test.go index 258fc09d9..1b51a0918 100644 --- a/engine/cld/config/env/config_test.go +++ b/engine/cld/config/env/config_test.go @@ -63,6 +63,10 @@ var ( }, Catalog: CatalogConfig{ GRPC: "http://localhost:1000", + Auth: &CatalogAuthConfig{ + KMSKeyID: "123", + KMSKeyRegion: "us-east-1", + }, }, } @@ -114,12 +118,12 @@ var ( "JD_GRPC": "GRPC2", "OCR_X_SIGNERS": "awkward bat", "OCR_X_PROPOSERS": "caring deer", - "CATALOG_SERVICE_GRPC": "http://localhost:8080", // These values do not have a legacy equivalent - "ONCHAIN_TON_DEPLOYER_KEY": "0x123", - "ONCHAIN_TON_WALLET_VERSION": "V5R1", - "CATALOG_SERVICE_AUTH_KMS_KEY_ID": "123", - "CATALOG_SERVICE_AUTH_KMS_KEY_REGION": "us-east-1", + "ONCHAIN_TON_DEPLOYER_KEY": "0x123", + "ONCHAIN_TON_WALLET_VERSION": "V5R1", + "CATALOG_GRPC": "http://localhost:8080", + "CATALOG_AUTH_KMS_KEY_ID": "123", + "CATALOG_AUTH_KMS_KEY_REGION": "us-east-1", } // envCfg is the config that is loaded from the environment variables. @@ -338,6 +342,7 @@ func Test_YAML_Marshal_Unmarshal(t *testing.T) { cfg.Offchain.JobDistributor.Auth = nil cfg.Onchain.EVM.Seth = nil + cfg.Catalog.Auth = nil return &cfg }, diff --git a/engine/cld/config/env/testdata/config.yml b/engine/cld/config/env/testdata/config.yml index b29b56099..3ea26d297 100644 --- a/engine/cld/config/env/testdata/config.yml +++ b/engine/cld/config/env/testdata/config.yml @@ -38,3 +38,6 @@ offchain: x_proposers: "furious fox" catalog: grpc: "http://localhost:1000" + auth: + kms_key_id: "123" + kms_key_region: "us-east-1" diff --git a/engine/cld/config/env_test.go b/engine/cld/config/env_test.go index 85f5e0900..f0e2ca1ba 100644 --- a/engine/cld/config/env_test.go +++ b/engine/cld/config/env_test.go @@ -67,7 +67,6 @@ func Test_LoadEnvConfig(t *testing.T) { //nolint:paralleltest // These tests are "SOLANA_PROGRAM_PATH": "0xcde", "APTOS_DEPLOYER_KEY": "0x345", "TRON_DEPLOYER_KEY": "0x456", - "CATALOG_SERVICE_GRPC": "http://localhost:2000", }, wantFunc: func(t *testing.T, cfg *cfgenv.Config) { t.Helper() @@ -92,7 +91,6 @@ func Test_LoadEnvConfig(t *testing.T) { //nolint:paralleltest // These tests are assert.Equal(t, "0x234", cfg.Onchain.Solana.WalletKey) assert.Equal(t, "0x345", cfg.Onchain.Aptos.DeployerKey) assert.Equal(t, "0x456", cfg.Onchain.Tron.DeployerKey) - assert.Equal(t, "http://localhost:2000", cfg.Catalog.GRPC) }, }, { @@ -114,6 +112,8 @@ func Test_LoadEnvConfig(t *testing.T) { //nolint:paralleltest // These tests are "ONCHAIN_EVM_SETH_CONFIG_FILE_PATH": "/tmp/config", "ONCHAIN_EVM_SETH_GETH_WRAPPER_DIRS": "dir1,dir2", "CATALOG_GRPC": "http://localhost:2000", + "CATALOG_AUTH_KMS_KEY_ID": "c4f1a2b3", + "CATALOG_AUTH_KMS_KEY_REGION": "us-east-1", "ONCHAIN_SOLANA_WALLET_KEY": "0x234", "ONCHAIN_SOLANA_PROGRAM_PATH": "0xcde", "ONCHAIN_APTOS_DEPLOYER_KEY": "0x345", @@ -147,6 +147,8 @@ func Test_LoadEnvConfig(t *testing.T) { //nolint:paralleltest // These tests are assert.Equal(t, "0x456", cfg.Onchain.Tron.DeployerKey) assert.Equal(t, "0x567", cfg.Onchain.Sui.DeployerKey) assert.Equal(t, "http://localhost:2000", cfg.Catalog.GRPC) + assert.Equal(t, "c4f1a2b3", cfg.Catalog.Auth.KMSKeyID) + assert.Equal(t, "us-east-1", cfg.Catalog.Auth.KMSKeyRegion) }, }, }