Skip to content

Commit 2560b72

Browse files
authored
INFOPLAT 2962 rotating beholder headers (#1567)
* INFOPLAT-2962 Updates `pkg/chipingress` version * INFOPLAT-2962 Adds rotating header impl * INFOPLAT-2962 Removes log from `rotatingHeaderProvider` - fixes unhandled error * INFOPLAT-2962 Bumps `chipingress` to `0.0.6` Fixes quotes * INFOPLAT-2962 Adds rotating auth header setup Fixes tests * INFOPLAT-2962 Add deprecation warning for NewStaticAuthHeaderProvider Adjust return type Small refactor Adjust test * INFOPLAT-2962 Clamp lowest possible TTL to 10mins * INFOPLAT-2962 Makes signing headers thread safe Handle thundering heard with double checked locking * INFOPLAT-2962 Use atomic value for lastupdated * Removes `defer client.Close` makes test run long as it waits for 10s ctx timeout * Fixes header value using `UnixNano` * Uses `atomic` for header map * Ensure thread safety * Adds benchmarking
1 parent 32ee550 commit 2560b72

8 files changed

Lines changed: 752 additions & 43 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ require (
3737
github.com/scylladb/go-reflectx v1.0.1
3838
github.com/shopspring/decimal v1.4.0
3939
github.com/smartcontractkit/chain-selectors v1.0.67
40-
github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.4
40+
github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.6
4141
github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976
4242
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250911124514-5874cc6d62b2
4343
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
326326
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
327327
github.com/smartcontractkit/chain-selectors v1.0.67 h1:gxTqP/JC40KDe3DE1SIsIKSTKTZEPyEU1YufO1admnw=
328328
github.com/smartcontractkit/chain-selectors v1.0.67/go.mod h1:xsKM0aN3YGcQKTPRPDDtPx2l4mlTN1Djmg0VVXV40b8=
329-
github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.4 h1:hvqATtrZ0iMRTI80cpBot/3JFbjz2j+2tvpfooVhRHw=
330-
github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.4/go.mod h1:eKGyfTKzr0/PeR7qKN4l2FcW9p+HzyKUwAfGhm/5YZc=
329+
github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.6 h1:INTd6uKc/QO11B0Vx7Ze17xgW3bqYbWuQcBQa9ixicQ=
330+
github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.6/go.mod h1:eKGyfTKzr0/PeR7qKN4l2FcW9p+HzyKUwAfGhm/5YZc=
331331
github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976 h1:mF3FiDUoV0QbJcks9R2y7ydqntNL1Z0VCPBJgx/Ms+0=
332332
github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA=
333333
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250911124514-5874cc6d62b2 h1:1/KdO5AbUr3CmpLjMPuJXPo2wHMbfB8mldKLsg7D4M8=

pkg/beholder/auth.go

Lines changed: 136 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,157 @@
11
package beholder
22

33
import (
4+
"context"
45
"crypto"
56
"crypto/ed25519"
67
"crypto/rand"
8+
"encoding/binary"
79
"fmt"
10+
"maps"
11+
"sync"
12+
"sync/atomic"
13+
"time"
814

915
"github.com/smartcontractkit/chainlink-common/pkg/chipingress"
16+
"google.golang.org/grpc"
17+
"google.golang.org/grpc/credentials"
1018
)
1119

1220
// authHeaderKey is the name of the header that the node authenticator will use to send the auth token
1321
var authHeaderKey = "X-Beholder-Node-Auth-Token"
1422

1523
// authHeaderVersion is the version of the auth header format
1624
var authHeaderVersion = "1"
25+
var authHeaderV2 = "2"
1726

18-
type staticAuthHeaderProvider struct {
19-
headers map[string]string
27+
type HeaderProvider interface {
28+
Headers(ctx context.Context) (map[string]string, error)
2029
}
2130

22-
func (p *staticAuthHeaderProvider) GetHeaders() map[string]string {
23-
return p.headers
31+
type PerRPCCredentialsProvider interface {
32+
Credentials() credentials.PerRPCCredentials
2433
}
2534

35+
type Auth interface {
36+
PerRPCCredentialsProvider
37+
HeaderProvider
38+
}
39+
40+
type Signer interface {
41+
Sign(ctx context.Context, keyID []byte, data []byte) ([]byte, error)
42+
}
43+
44+
type staticAuth struct {
45+
headers map[string]string
46+
requireTransportSecurity bool
47+
}
48+
49+
func (p *staticAuth) Headers(_ context.Context) (map[string]string, error) {
50+
return p.headers, nil
51+
}
52+
53+
func (p *staticAuth) Credentials() credentials.PerRPCCredentials {
54+
return p
55+
}
56+
57+
func (p *staticAuth) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
58+
return p.Headers(ctx)
59+
}
60+
61+
func (p *staticAuth) RequireTransportSecurity() bool {
62+
return p.requireTransportSecurity
63+
}
64+
65+
func NewStaticAuth(headers map[string]string, requireTransportSecurity bool) Auth {
66+
return &staticAuth{headers, requireTransportSecurity}
67+
}
68+
69+
// Deprecated: use NewStaticAuth instead
2670
func NewStaticAuthHeaderProvider(headers map[string]string) chipingress.HeaderProvider {
27-
return &staticAuthHeaderProvider{headers: headers}
71+
return &staticAuth{headers: headers}
72+
}
73+
74+
type rotatingAuth struct {
75+
csaPubKey ed25519.PublicKey
76+
signer Signer
77+
signerTimeout time.Duration
78+
headers atomic.Value // stores map[string]string
79+
ttl time.Duration
80+
lastUpdatedNanos atomic.Int64
81+
requireTransportSecurity bool
82+
mu sync.Mutex
83+
}
84+
85+
func NewRotatingAuth(csaPubKey ed25519.PublicKey, signer Signer, ttl time.Duration, requireTransportSecurity bool) Auth {
86+
r := &rotatingAuth{
87+
csaPubKey: csaPubKey,
88+
signer: signer,
89+
signerTimeout: time.Second * 5,
90+
ttl: ttl,
91+
lastUpdatedNanos: atomic.Int64{},
92+
requireTransportSecurity: requireTransportSecurity,
93+
}
94+
r.headers.Store(make(map[string]string))
95+
return r
96+
}
97+
98+
func (r *rotatingAuth) Headers(ctx context.Context) (map[string]string, error) {
99+
100+
// Return a copy of the headers to avoid concurrent read/write to the map by callers
101+
returnHeader := make(map[string]string)
102+
lastUpdated := time.Unix(0, r.lastUpdatedNanos.Load())
103+
104+
if time.Since(lastUpdated) > r.ttl {
105+
106+
r.mu.Lock()
107+
defer r.mu.Unlock()
108+
109+
// Multiple concurrent calls (after the first) will block waiting for the lock.
110+
// First will get the lock and update headers + lastUpdated, double check since potentially another goroutine has already
111+
// updated the headers and lastUpdated while waiting for the lock.
112+
lastUpdated = time.Unix(0, r.lastUpdatedNanos.Load())
113+
if time.Since(lastUpdated) < r.ttl {
114+
maps.Copy(returnHeader, r.headers.Load().(map[string]string))
115+
return returnHeader, nil
116+
}
117+
118+
// Append the bytes of the public key with bytes of the timestamp to create the message to sign
119+
ts := time.Now()
120+
tsBytes := make([]byte, 8)
121+
binary.BigEndian.PutUint64(tsBytes, uint64(ts.UnixNano()))
122+
msgBytes := append(r.csaPubKey, tsBytes...)
123+
124+
ctxWithTimeout, cancel := context.WithTimeout(ctx, r.signerTimeout)
125+
defer cancel()
126+
127+
// Sign(public key bytes + timestamp bytes)
128+
signature, err := r.signer.Sign(ctxWithTimeout, r.csaPubKey, msgBytes)
129+
if err != nil {
130+
return nil, fmt.Errorf("beholder: failed to sign auth header: %w", err)
131+
}
132+
133+
newHeaders := make(map[string]string)
134+
newHeaders[authHeaderKey] = fmt.Sprintf("%s:%x:%d:%x", authHeaderV2, r.csaPubKey, ts.UnixNano(), signature)
135+
136+
r.headers.Store(newHeaders)
137+
r.lastUpdatedNanos.Store(ts.UnixNano())
138+
}
139+
140+
maps.Copy(returnHeader, r.headers.Load().(map[string]string))
141+
142+
return returnHeader, nil
143+
}
144+
145+
func (a *rotatingAuth) Credentials() credentials.PerRPCCredentials {
146+
return a
147+
}
148+
149+
func (a *rotatingAuth) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
150+
return a.Headers(ctx)
151+
}
152+
153+
func (a *rotatingAuth) RequireTransportSecurity() bool {
154+
return a.requireTransportSecurity
28155
}
29156

30157
// BuildAuthHeaders creates the auth header value to be included on requests.
@@ -58,3 +185,7 @@ func NewAuthHeaders(ed25519Signer crypto.Signer) (map[string]string, error) {
58185

59186
return map[string]string{authHeaderKey: headerValue}, nil
60187
}
188+
189+
func authDialOpt(auth PerRPCCredentialsProvider) grpc.DialOption {
190+
return grpc.WithPerRPCCredentials(auth.Credentials())
191+
}

0 commit comments

Comments
 (0)