Skip to content

Commit 9911d95

Browse files
committed
chore: refactor hmac auth logic into its own file
1 parent f14bd58 commit 9911d95

3 files changed

Lines changed: 374 additions & 104 deletions

File tree

datastore/catalog/remote/grpc.go

Lines changed: 0 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,11 @@ package remote
22

33
import (
44
"context"
5-
"crypto/sha256"
6-
"encoding/hex"
75
"fmt"
8-
"strconv"
9-
"time"
106

11-
"github.com/aws/aws-sdk-go-v2/aws"
12-
"github.com/aws/aws-sdk-go-v2/config"
13-
"github.com/aws/aws-sdk-go-v2/service/kms"
14-
"github.com/aws/aws-sdk-go-v2/service/kms/types"
157
pb "github.com/smartcontractkit/chainlink-protos/op-catalog/v1/datastore"
168
"google.golang.org/grpc"
179
"google.golang.org/grpc/credentials"
18-
"google.golang.org/grpc/metadata"
1910
"google.golang.org/protobuf/proto"
2011
)
2112

@@ -135,98 +126,3 @@ func newCatalogConnection(cfg CatalogConfig) (*grpc.ClientConn, error) {
135126

136127
return conn, nil
137128
}
138-
139-
const (
140-
// dataAccessMethod is the full gRPC method name for DataAccess
141-
dataAccessMethod = "/op_catalog.v1.datastore.Datastore/DataAccess"
142-
)
143-
144-
// HMACAuthConfig holds HMAC authentication configuration.
145-
type HMACAuthConfig struct {
146-
KeyID string
147-
KeyRegion string
148-
Authority string // The gRPC authority (hostname without port) used for HMAC signing
149-
}
150-
151-
// prepareHMACContext prepares the context with HMAC authentication metadata.
152-
// It loads AWS KMS configuration, creates a KMS client, generates an HMAC signature,
153-
// and attaches it to the outgoing gRPC metadata.
154-
func (c *CatalogClient) prepareHMACContext(ctx context.Context, req proto.Message) (context.Context, error) {
155-
// Load AWS configuration
156-
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(c.hmacConfig.KeyRegion))
157-
if err != nil {
158-
return nil, fmt.Errorf("failed to load AWS config: %w", err)
159-
}
160-
161-
// Create KMS client
162-
kmsClient := kms.NewFromConfig(cfg)
163-
164-
// Create HMAC helper
165-
hmacHelper := &kmsHMACClientHelper{
166-
kmsClient: kmsClient,
167-
keyID: c.hmacConfig.KeyID,
168-
}
169-
170-
// Marshal the message to bytes for HMAC
171-
payload, err := proto.Marshal(req)
172-
if err != nil {
173-
return nil, fmt.Errorf("failed to marshal message for HMAC: %w", err)
174-
}
175-
176-
// Generate HMAC signature and timestamp
177-
signature, timestamp, err := hmacHelper.generateHMACSignature(ctx, dataAccessMethod, c.hmacConfig.Authority, payload)
178-
if err != nil {
179-
return nil, fmt.Errorf("failed to generate HMAC signature: %w", err)
180-
}
181-
182-
// Add HMAC authentication to gRPC metadata
183-
md := metadata.Pairs(
184-
"x-hmac-signature", signature,
185-
"x-hmac-timestamp", timestamp,
186-
)
187-
188-
// Merge with existing metadata if present
189-
if existingMd, ok := metadata.FromOutgoingContext(ctx); ok {
190-
md = metadata.Join(existingMd, md)
191-
}
192-
193-
return metadata.NewOutgoingContext(ctx, md), nil
194-
}
195-
196-
// kmsHMACClientHelper helps clients generate HMAC signatures using AWS KMS.
197-
type kmsHMACClientHelper struct {
198-
kmsClient *kms.Client
199-
keyID string
200-
}
201-
202-
// generateHMACSignature generates an HMAC signature and timestamp for the given request.
203-
// Returns the hex-encoded signature and Unix timestamp as strings.
204-
// The caller is responsible for adding these to transport-specific metadata/headers.
205-
func (h *kmsHMACClientHelper) generateHMACSignature(ctx context.Context, method string, authority string, payload []byte) (signature string, timestamp string, err error) {
206-
timestamp = strconv.FormatInt(time.Now().Unix(), 10)
207-
208-
// Hash the payload with SHA-256 to stay within KMS message size limits (4096 bytes)
209-
// and to have a predictable signature length
210-
payloadHash := sha256.Sum256(payload)
211-
212-
// Construct HMAC message using method path, authority, timestamp, and payload hash
213-
// Format: method\nauthority\ntimestamp\nsha256(payload)
214-
messagePrefix := fmt.Sprintf("%s\n%s\n%s\n", method, authority, timestamp)
215-
fullMessage := append([]byte(messagePrefix), payloadHash[:]...)
216-
217-
// Generate MAC using KMS with HMAC_SHA_256
218-
generateInput := &kms.GenerateMacInput{
219-
KeyId: aws.String(h.keyID),
220-
Message: fullMessage,
221-
MacAlgorithm: types.MacAlgorithmSpecHmacSha256,
222-
}
223-
224-
generateOutput, err := h.kmsClient.GenerateMac(ctx, generateInput)
225-
if err != nil {
226-
return "", "", fmt.Errorf("failed to generate MAC: %w", err)
227-
}
228-
229-
signature = hex.EncodeToString(generateOutput.Mac)
230-
231-
return signature, timestamp, nil
232-
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package remote
2+
3+
import (
4+
"context"
5+
"crypto/sha256"
6+
"encoding/hex"
7+
"fmt"
8+
"strconv"
9+
"time"
10+
11+
"github.com/aws/aws-sdk-go-v2/aws"
12+
"github.com/aws/aws-sdk-go-v2/config"
13+
"github.com/aws/aws-sdk-go-v2/service/kms"
14+
"github.com/aws/aws-sdk-go-v2/service/kms/types"
15+
"google.golang.org/grpc/metadata"
16+
"google.golang.org/protobuf/proto"
17+
)
18+
19+
const (
20+
// dataAccessMethod is the full gRPC method name for DataAccess
21+
dataAccessMethod = "/op_catalog.v1.datastore.Datastore/DataAccess"
22+
)
23+
24+
// HMACAuthConfig holds HMAC authentication configuration.
25+
type HMACAuthConfig struct {
26+
KeyID string
27+
KeyRegion string
28+
Authority string // The gRPC authority (hostname without port) used for HMAC signing
29+
}
30+
31+
// prepareHMACContext prepares the context with HMAC authentication metadata.
32+
// It loads AWS KMS configuration, creates a KMS client, generates an HMAC signature,
33+
// and attaches it to the outgoing gRPC metadata.
34+
func (c *CatalogClient) prepareHMACContext(ctx context.Context, req proto.Message) (context.Context, error) {
35+
// Load AWS configuration
36+
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(c.hmacConfig.KeyRegion))
37+
if err != nil {
38+
return nil, fmt.Errorf("failed to load AWS config: %w", err)
39+
}
40+
41+
// Create KMS client
42+
kmsClient := kms.NewFromConfig(cfg)
43+
44+
return c.prepareHMACContextWithClient(ctx, req, kmsClient)
45+
}
46+
47+
// prepareHMACContextWithClient prepares the context with HMAC authentication metadata using the provided KMS client.
48+
// This method is extracted for testability.
49+
func (c *CatalogClient) prepareHMACContextWithClient(ctx context.Context, req proto.Message, client kmsClient) (context.Context, error) {
50+
// Create HMAC helper
51+
hmacHelper := &kmsHMACClientHelper{
52+
kmsClient: client,
53+
keyID: c.hmacConfig.KeyID,
54+
}
55+
56+
// Marshal the message to bytes for HMAC
57+
payload, err := proto.Marshal(req)
58+
if err != nil {
59+
return nil, fmt.Errorf("failed to marshal message for HMAC: %w", err)
60+
}
61+
62+
// Generate HMAC signature and timestamp
63+
signature, timestamp, err := hmacHelper.generateHMACSignature(ctx, dataAccessMethod, c.hmacConfig.Authority, payload)
64+
if err != nil {
65+
return nil, fmt.Errorf("failed to generate HMAC signature: %w", err)
66+
}
67+
68+
// Add HMAC authentication to gRPC metadata
69+
md := metadata.Pairs(
70+
"x-hmac-signature", signature,
71+
"x-hmac-timestamp", timestamp,
72+
)
73+
74+
// Merge with existing metadata if present
75+
if existingMd, ok := metadata.FromOutgoingContext(ctx); ok {
76+
md = metadata.Join(existingMd, md)
77+
}
78+
79+
return metadata.NewOutgoingContext(ctx, md), nil
80+
}
81+
82+
// kmsClient defines the interface for KMS operations needed for HMAC
83+
type kmsClient interface {
84+
GenerateMac(ctx context.Context, params *kms.GenerateMacInput, optFns ...func(*kms.Options)) (*kms.GenerateMacOutput, error)
85+
}
86+
87+
// kmsHMACClientHelper helps clients generate HMAC signatures using AWS KMS.
88+
type kmsHMACClientHelper struct {
89+
kmsClient kmsClient
90+
keyID string
91+
}
92+
93+
// generateHMACSignature generates an HMAC signature and timestamp for the given request.
94+
// Returns the hex-encoded signature and Unix timestamp as strings.
95+
// The caller is responsible for adding these to transport-specific metadata/headers.
96+
func (h *kmsHMACClientHelper) generateHMACSignature(ctx context.Context, method string, authority string, payload []byte) (signature string, timestamp string, err error) {
97+
timestamp = strconv.FormatInt(time.Now().Unix(), 10)
98+
99+
// Hash the payload with SHA-256 to stay within KMS message size limits (4096 bytes)
100+
// and to have a predictable signature length
101+
payloadHash := sha256.Sum256(payload)
102+
103+
// Construct HMAC message using method path, authority, timestamp, and payload hash
104+
// Format: method\nauthority\ntimestamp\nsha256(payload)
105+
messagePrefix := fmt.Sprintf("%s\n%s\n%s\n", method, authority, timestamp)
106+
fullMessage := append([]byte(messagePrefix), payloadHash[:]...)
107+
108+
// Generate MAC using KMS with HMAC_SHA_256
109+
generateInput := &kms.GenerateMacInput{
110+
KeyId: aws.String(h.keyID),
111+
Message: fullMessage,
112+
MacAlgorithm: types.MacAlgorithmSpecHmacSha256,
113+
}
114+
115+
generateOutput, err := h.kmsClient.GenerateMac(ctx, generateInput)
116+
if err != nil {
117+
return "", "", fmt.Errorf("failed to generate MAC: %w", err)
118+
}
119+
120+
signature = hex.EncodeToString(generateOutput.Mac)
121+
122+
return signature, timestamp, nil
123+
}

0 commit comments

Comments
 (0)