Skip to content

Commit 11a3eb9

Browse files
committed
INFOPLAT-2962 Adds rotating auth header setup
Fixes tests
1 parent 6be8d43 commit 11a3eb9

7 files changed

Lines changed: 532 additions & 50 deletions

File tree

pkg/beholder/auth.go

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import (
88
"encoding/binary"
99
"fmt"
1010
"time"
11+
12+
"google.golang.org/grpc"
13+
"google.golang.org/grpc/credentials"
1114
)
1215

1316
// authHeaderKey is the name of the header that the node authenticator will use to send the auth token
@@ -21,41 +24,65 @@ type HeaderProvider interface {
2124
Headers(ctx context.Context) (map[string]string, error)
2225
}
2326

27+
type PerRPCCredentialsProvider interface {
28+
Credentials() credentials.PerRPCCredentials
29+
}
30+
31+
type Auth interface {
32+
PerRPCCredentialsProvider
33+
HeaderProvider
34+
}
35+
2436
type Signer interface {
2537
Sign(ctx context.Context, keyID []byte, data []byte) ([]byte, error)
2638
}
2739

28-
type staticAuthHeaderProvider struct {
29-
headers map[string]string
40+
type staticAuth struct {
41+
headers map[string]string
42+
requireTransportSecurity bool
3043
}
3144

32-
func (p *staticAuthHeaderProvider) Headers(_ context.Context) (map[string]string, error) {
45+
func (p *staticAuth) Headers(_ context.Context) (map[string]string, error) {
3346
return p.headers, nil
3447
}
3548

36-
func NewStaticAuthHeaderProvider(headers map[string]string) HeaderProvider {
37-
return &staticAuthHeaderProvider{headers: headers}
49+
func (p *staticAuth) Credentials() credentials.PerRPCCredentials {
50+
return p
3851
}
3952

40-
type rotatingAuthHeaderProvider struct {
41-
csaPubKey ed25519.PublicKey
42-
signer Signer
43-
headers map[string]string
44-
ttl time.Duration
45-
lastUpdated time.Time
53+
func (p *staticAuth) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
54+
return p.Headers(ctx)
4655
}
4756

48-
func NewRotatingAuthHeaderProvider(csaPubKey ed25519.PublicKey, signer Signer, ttl time.Duration) HeaderProvider {
49-
return &rotatingAuthHeaderProvider{
50-
csaPubKey: csaPubKey,
51-
signer: signer,
52-
ttl: ttl,
53-
headers: make(map[string]string),
54-
lastUpdated: time.Unix(0, 0),
57+
func (p *staticAuth) RequireTransportSecurity() bool {
58+
return p.requireTransportSecurity
59+
}
60+
61+
func NewStaticAuth(headers map[string]string, requireTransportSecurity bool) HeaderProvider {
62+
return &staticAuth{headers, requireTransportSecurity}
63+
}
64+
65+
type rotatingAuth struct {
66+
csaPubKey ed25519.PublicKey
67+
signer Signer
68+
headers map[string]string
69+
ttl time.Duration
70+
lastUpdated time.Time
71+
requireTransportSecurity bool
72+
}
73+
74+
func NewRotatingAuth(csaPubKey ed25519.PublicKey, signer Signer, ttl time.Duration, requireTransportSecurity bool) Auth {
75+
return &rotatingAuth{
76+
csaPubKey: csaPubKey,
77+
signer: signer,
78+
ttl: ttl,
79+
headers: make(map[string]string),
80+
lastUpdated: time.Unix(0, 0),
81+
requireTransportSecurity: requireTransportSecurity,
5582
}
5683
}
5784

58-
func (r *rotatingAuthHeaderProvider) Headers(ctx context.Context) (map[string]string, error) {
85+
func (r *rotatingAuth) Headers(ctx context.Context) (map[string]string, error) {
5986
if time.Since(r.lastUpdated) > r.ttl {
6087
// Append timestamp bytes to the public key bytes
6188
timestamp := time.Now().UnixMilli()
@@ -74,6 +101,18 @@ func (r *rotatingAuthHeaderProvider) Headers(ctx context.Context) (map[string]st
74101
return r.headers, nil
75102
}
76103

104+
func (a *rotatingAuth) Credentials() credentials.PerRPCCredentials {
105+
return a
106+
}
107+
108+
func (a *rotatingAuth) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
109+
return a.Headers(ctx)
110+
}
111+
112+
func (a *rotatingAuth) RequireTransportSecurity() bool {
113+
return a.requireTransportSecurity
114+
}
115+
77116
// BuildAuthHeaders creates the auth header value to be included on requests.
78117
// The current format for the header is:
79118
//
@@ -105,3 +144,7 @@ func NewAuthHeaders(ed25519Signer crypto.Signer) (map[string]string, error) {
105144

106145
return map[string]string{authHeaderKey: headerValue}, nil
107146
}
147+
148+
func authDialOpt(auth PerRPCCredentialsProvider) grpc.DialOption {
149+
return grpc.WithPerRPCCredentials(auth.Credentials())
150+
}

pkg/beholder/auth_test.go

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package beholder_test
22

33
import (
4+
"context"
45
"crypto/ed25519"
56
"encoding/hex"
7+
"strings"
68
"testing"
9+
"time"
710

811
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/mock"
913
"github.com/stretchr/testify/require"
1014

1115
"github.com/smartcontractkit/chainlink-common/pkg/beholder"
@@ -37,10 +41,157 @@ func TestStaticAuthHeaderProvider(t *testing.T) {
3741
}
3842

3943
// Create new header provider
40-
provider := beholder.NewStaticAuthHeaderProvider(testHeaders)
44+
provider := beholder.NewStaticAuth(testHeaders, false)
4145

4246
// Get headers and verify they match
4347
headers, err := provider.Headers(t.Context())
4448
require.NoError(t, err)
4549
assert.Equal(t, testHeaders, headers)
4650
}
51+
52+
// MockSigner implements the beholder.Signer interface for testing rotating auth
53+
type MockSigner struct {
54+
mock.Mock
55+
}
56+
57+
func (m *MockSigner) Sign(ctx context.Context, keyID []byte, data []byte) ([]byte, error) {
58+
args := m.Called(ctx, keyID, data)
59+
return args.Get(0).([]byte), args.Error(1)
60+
}
61+
62+
func TestRotatingAuth(t *testing.T) {
63+
// Generate test key pair
64+
pubKey, privKey, err := ed25519.GenerateKey(nil)
65+
require.NoError(t, err)
66+
67+
t.Run("creates valid rotating auth headers", func(t *testing.T) {
68+
69+
mockSigner := &MockSigner{}
70+
71+
dummySignature := ed25519.Sign(privKey, []byte("test data"))
72+
73+
mockSigner.
74+
On("Sign", mock.Anything, mock.MatchedBy(func(keyID []byte) bool {
75+
return string(keyID) == string(pubKey) // Verify correct public key is passed
76+
}), mock.Anything).
77+
Return(dummySignature, nil)
78+
79+
ttl := 5 * time.Minute
80+
auth := beholder.NewRotatingAuth(pubKey, mockSigner, ttl, false)
81+
82+
headers, err := auth.Headers(t.Context())
83+
require.NoError(t, err)
84+
require.NotEmpty(t, headers)
85+
86+
authHeader := headers["X-Beholder-Node-Auth-Token"]
87+
require.NotEmpty(t, authHeader)
88+
89+
parts := strings.Split(authHeader, ":")
90+
require.Len(t, parts, 4, "Auth header should have format version:pubkey_hex:timestamp:signature_hex")
91+
92+
assert.Equal(t, "2", parts[0], "Version should be 2")
93+
assert.Equal(t, hex.EncodeToString(pubKey), parts[1], "Public key should match")
94+
assert.NotEmpty(t, parts[2], "Timestamp should not be empty")
95+
96+
// Verify signature is hex encoded
97+
_, err = hex.DecodeString(parts[3])
98+
assert.NoError(t, err, "Signature should be valid hex")
99+
100+
mockSigner.AssertExpectations(t)
101+
})
102+
103+
t.Run("reuses headers within TTL", func(t *testing.T) {
104+
105+
mockSigner := &MockSigner{}
106+
107+
dummySignature := ed25519.Sign(privKey, []byte("test data"))
108+
109+
mockSigner.
110+
On("Sign", mock.Anything, mock.Anything, mock.Anything).
111+
Return(dummySignature, nil).
112+
Maybe()
113+
114+
ttl := 5 * time.Minute
115+
auth := beholder.NewRotatingAuth(pubKey, mockSigner, ttl, false)
116+
117+
headers1, err := auth.Headers(t.Context())
118+
require.NoError(t, err)
119+
120+
headers2, err := auth.Headers(t.Context())
121+
require.NoError(t, err)
122+
123+
assert.Equal(t, headers1, headers2, "Headers should be reused within TTL")
124+
125+
mockSigner.AssertExpectations(t)
126+
})
127+
128+
t.Run("handles signer errors", func(t *testing.T) {
129+
130+
mockSigner := &MockSigner{}
131+
expectedErr := assert.AnError
132+
133+
mockSigner.
134+
On("Sign", mock.Anything, mock.Anything, mock.Anything).
135+
Return([]byte{}, expectedErr)
136+
137+
ttl := 5 * time.Minute
138+
auth := beholder.NewRotatingAuth(pubKey, mockSigner, ttl, false)
139+
140+
ctx := context.Background()
141+
headers, err := auth.Headers(ctx)
142+
require.Error(t, err)
143+
assert.Nil(t, headers)
144+
assert.Contains(t, err.Error(), "beholder: failed to sign auth header")
145+
assert.Contains(t, err.Error(), expectedErr.Error())
146+
147+
mockSigner.AssertExpectations(t)
148+
})
149+
150+
t.Run("implements PerRPCCredentialsProvider interface", func(t *testing.T) {
151+
152+
mockSigner := &MockSigner{}
153+
dummySignature := ed25519.Sign(privKey, []byte("test data"))
154+
155+
mockSigner.
156+
On("Sign", mock.Anything, mock.Anything, mock.Anything).
157+
Return(dummySignature, nil).
158+
Maybe()
159+
160+
ttl := 5 * time.Minute
161+
auth := beholder.NewRotatingAuth(pubKey, mockSigner, ttl, false)
162+
163+
creds := auth.Credentials()
164+
require.NotNil(t, creds)
165+
166+
assert.False(t, creds.RequireTransportSecurity())
167+
168+
metadata, err := creds.GetRequestMetadata(t.Context())
169+
require.NoError(t, err)
170+
assert.NotEmpty(t, metadata)
171+
172+
mockSigner.AssertExpectations(t)
173+
})
174+
175+
t.Run("respects transport security requirement", func(t *testing.T) {
176+
177+
mockSigner := &MockSigner{}
178+
dummySignature := ed25519.Sign(privKey, []byte("test data"))
179+
180+
mockSigner.
181+
On("Sign", mock.Anything, mock.Anything, mock.Anything).
182+
Return(dummySignature, nil).
183+
Maybe()
184+
185+
ttl := 5 * time.Minute
186+
// transport security required
187+
authSecure := beholder.NewRotatingAuth(pubKey, mockSigner, ttl, true)
188+
credsSecure := authSecure.Credentials()
189+
assert.True(t, credsSecure.RequireTransportSecurity())
190+
// transport security not required
191+
authInsecure := beholder.NewRotatingAuth(pubKey, mockSigner, ttl, false)
192+
credsInsecure := authInsecure.Credentials()
193+
assert.False(t, credsInsecure.RequireTransportSecurity())
194+
195+
mockSigner.AssertExpectations(t)
196+
})
197+
}

0 commit comments

Comments
 (0)