Skip to content

Commit 55948ff

Browse files
authored
fmt (#1640)
1 parent 20bf958 commit 55948ff

File tree

2 files changed

+190
-12
lines changed

2 files changed

+190
-12
lines changed

pkg/beholder/auth.go

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -129,24 +129,16 @@ func (r *rotatingAuth) Headers(ctx context.Context) (map[string]string, error) {
129129
return returnHeader, nil
130130
}
131131

132-
// Append the bytes of the public key with bytes of the timestamp to create the message to sign
133-
ts := time.Now()
134-
tsBytes := make([]byte, 8)
135-
binary.BigEndian.PutUint64(tsBytes, uint64(ts.UnixNano()))
136-
msgBytes := append(r.csaPubKey, tsBytes...)
137-
138132
ctxWithTimeout, cancel := context.WithTimeout(ctx, r.signerTimeout)
139133
defer cancel()
140134

141-
// Sign(public key bytes + timestamp bytes)
142-
signature, err := r.signer.Sign(ctxWithTimeout, fmt.Sprintf("%x", r.csaPubKey), msgBytes)
135+
ts := time.Now()
136+
137+
newHeaders, err := NewAuthHeaderV2(ctxWithTimeout, r.csaPubKey, r.signer, ts)
143138
if err != nil {
144-
return nil, fmt.Errorf("beholder: failed to sign auth header: %w", err)
139+
return nil, fmt.Errorf("beholder: failed to create auth header: %w", err)
145140
}
146141

147-
newHeaders := make(map[string]string)
148-
newHeaders[authHeaderKey] = fmt.Sprintf("%s:%x:%d:%x", authHeaderV2, r.csaPubKey, ts.UnixNano(), signature)
149-
150142
r.headers.Store(newHeaders)
151143
r.lastUpdatedNanos.Store(ts.UnixNano())
152144
}
@@ -200,6 +192,32 @@ func NewAuthHeaders(ed25519Signer crypto.Signer) (map[string]string, error) {
200192
return map[string]string{authHeaderKey: headerValue}, nil
201193
}
202194

195+
// NewAuthHeadersV2 creates the V2 format of the auth header value to be included on requests.
196+
// This format includes a timestamp as part of the message to sign.
197+
// The current format for V2 headers is:
198+
//
199+
// <version>:<public_key_hex>:<timestamp_bytes>:<signature_hex>
200+
//
201+
// where the byte value of <public_key_hex> + <timestamp_bytes> is what's being signed
202+
func NewAuthHeaderV2(ctx context.Context, pubKey ed25519.PublicKey, signer Signer, ts time.Time) (map[string]string, error) {
203+
204+
// Append the bytes of the public key with bytes of the timestamp to create the message to sign
205+
tsBytes := make([]byte, 8)
206+
binary.BigEndian.PutUint64(tsBytes, uint64(ts.UnixNano()))
207+
msgBytes := append(pubKey, tsBytes...)
208+
209+
// Sign(public key bytes + timestamp bytes)
210+
signature, err := signer.Sign(ctx, fmt.Sprintf("%x", pubKey), msgBytes)
211+
if err != nil {
212+
return nil, fmt.Errorf("beholder: failed to sign auth header: %w", err)
213+
}
214+
215+
headers := make(map[string]string)
216+
headers[authHeaderKey] = fmt.Sprintf("%s:%x:%d:%x", authHeaderV2, pubKey, ts.UnixNano(), signature)
217+
218+
return headers, nil
219+
}
220+
203221
func authDialOpt(auth PerRPCCredentialsProvider) grpc.DialOption {
204222
return grpc.WithPerRPCCredentials(auth.Credentials())
205223
}

pkg/beholder/auth_test.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package beholder_test
33
import (
44
"context"
55
"crypto/ed25519"
6+
"encoding/binary"
67
"encoding/hex"
78
"fmt"
89
"strings"
@@ -34,6 +35,165 @@ func TestBuildAuthHeaders(t *testing.T) {
3435
assert.Equal(t, expectedHeaders, headers)
3536
}
3637

38+
func TestNewAuthHeaderV2(t *testing.T) {
39+
// Generate test key pair
40+
pubKey, privKey, err := ed25519.GenerateKey(nil)
41+
require.NoError(t, err)
42+
43+
t.Run("creates valid V2 auth headers", func(t *testing.T) {
44+
mockSigner := &MockSigner{}
45+
46+
ts := time.Now()
47+
48+
// Create the expected message bytes (pubkey + timestamp)
49+
expectedSignature := []byte("test-signature")
50+
mockSigner.
51+
On("Sign", t.Context(), hex.EncodeToString(pubKey), mock.Anything).
52+
Return(expectedSignature, nil).
53+
Once()
54+
55+
headers, err := beholder.NewAuthHeaderV2(t.Context(), pubKey, mockSigner, ts)
56+
require.NoError(t, err)
57+
require.NotNil(t, headers)
58+
require.Contains(t, headers, "X-Beholder-Node-Auth-Token")
59+
60+
authHeader := headers["X-Beholder-Node-Auth-Token"]
61+
parts := strings.Split(authHeader, ":")
62+
require.Len(t, parts, 4, "Auth header should have format version:pubkey_hex:timestamp:signature_hex")
63+
64+
assert.Equal(t, "2", parts[0], "Version should be 2")
65+
assert.Equal(t, hex.EncodeToString(pubKey), parts[1], "Public key should match")
66+
assert.Equal(t, fmt.Sprintf("%d", ts.UnixNano()), parts[2], "Timestamp should match")
67+
assert.Equal(t, hex.EncodeToString(expectedSignature), parts[3], "Signature should match")
68+
69+
mockSigner.AssertExpectations(t)
70+
})
71+
t.Run("returns error when signer fails", func(t *testing.T) {
72+
mockSigner := &MockSigner{}
73+
ts := time.Now()
74+
75+
expectedErr := fmt.Errorf("signing failed")
76+
mockSigner.
77+
On("Sign", t.Context(), hex.EncodeToString(pubKey), mock.Anything).
78+
Return([]byte{}, expectedErr).
79+
Once()
80+
81+
headers, err := beholder.NewAuthHeaderV2(t.Context(), pubKey, mockSigner, ts)
82+
require.Error(t, err)
83+
assert.Nil(t, headers)
84+
assert.Contains(t, err.Error(), "beholder: failed to sign auth header")
85+
assert.Contains(t, err.Error(), expectedErr.Error())
86+
87+
mockSigner.AssertExpectations(t)
88+
})
89+
90+
t.Run("verifies signature with ed25519", func(t *testing.T) {
91+
// Use a real signature for verification
92+
mockSigner := &MockSigner{}
93+
ts := time.Now()
94+
95+
// Calculate the message that should be signed
96+
tsBytes := make([]byte, 8)
97+
binary.BigEndian.PutUint64(tsBytes, uint64(ts.UnixNano()))
98+
msgBytes := append(pubKey, tsBytes...)
99+
100+
// Sign with the actual private key
101+
realSignature := ed25519.Sign(privKey, msgBytes)
102+
103+
mockSigner.
104+
On("Sign", t.Context(), hex.EncodeToString(pubKey), mock.MatchedBy(func(data []byte) bool {
105+
// Match if the data contains pubkey + timestamp
106+
return len(data) == len(pubKey)+8 && string(data[:len(pubKey)]) == string(pubKey)
107+
})).
108+
Return(realSignature, nil).
109+
Once()
110+
111+
headers, err := beholder.NewAuthHeaderV2(t.Context(), pubKey, mockSigner, ts)
112+
require.NoError(t, err)
113+
require.NotNil(t, headers)
114+
115+
authHeader := headers["X-Beholder-Node-Auth-Token"]
116+
parts := strings.Split(authHeader, ":")
117+
require.Len(t, parts, 4)
118+
119+
signatureBytes, err := hex.DecodeString(parts[3])
120+
require.NoError(t, err)
121+
122+
// Verify the signature
123+
valid := ed25519.Verify(pubKey, msgBytes, signatureBytes)
124+
assert.True(t, valid, "Signature should be valid")
125+
126+
mockSigner.AssertExpectations(t)
127+
})
128+
129+
t.Run("handles context cancellation", func(t *testing.T) {
130+
mockSigner := &MockSigner{}
131+
132+
ts := time.Now()
133+
134+
mockSigner.
135+
On("Sign", t.Context(), hex.EncodeToString(pubKey), mock.Anything).
136+
Return([]byte{}, context.Canceled).
137+
Maybe()
138+
139+
headers, err := beholder.NewAuthHeaderV2(t.Context(), pubKey, mockSigner, ts)
140+
141+
// The function should propagate the context error
142+
if err != nil {
143+
assert.Contains(t, err.Error(), "beholder: failed to sign auth header")
144+
}
145+
146+
// If mockSigner.Sign was called and returned error, headers should be nil
147+
if err != nil {
148+
assert.Nil(t, headers)
149+
}
150+
})
151+
152+
t.Run("uses correct keyID format", func(t *testing.T) {
153+
mockSigner := &MockSigner{}
154+
ts := time.Now()
155+
156+
var capturedKeyID string
157+
mockSigner.
158+
On("Sign", t.Context(), mock.Anything, mock.Anything).
159+
Run(func(args mock.Arguments) {
160+
capturedKeyID = args.Get(1).(string)
161+
}).
162+
Return([]byte("signature"), nil).
163+
Once()
164+
165+
_, err := beholder.NewAuthHeaderV2(t.Context(), pubKey, mockSigner, ts)
166+
require.NoError(t, err)
167+
168+
// Verify keyID is hex-encoded public key
169+
assert.Equal(t, hex.EncodeToString(pubKey), capturedKeyID)
170+
171+
mockSigner.AssertExpectations(t)
172+
})
173+
174+
t.Run("different timestamps produce different headers", func(t *testing.T) {
175+
mockSigner := &MockSigner{}
176+
177+
ts1 := time.Unix(1000, 0)
178+
ts2 := time.Unix(2000, 0)
179+
180+
mockSigner.
181+
On("Sign", t.Context(), hex.EncodeToString(pubKey), mock.Anything).
182+
Return([]byte("signature"), nil)
183+
184+
headers1, err := beholder.NewAuthHeaderV2(t.Context(), pubKey, mockSigner, ts1)
185+
require.NoError(t, err)
186+
187+
headers2, err := beholder.NewAuthHeaderV2(t.Context(), pubKey, mockSigner, ts2)
188+
require.NoError(t, err)
189+
190+
// Headers should be different due to different timestamps
191+
assert.NotEqual(t, headers1["X-Beholder-Node-Auth-Token"], headers2["X-Beholder-Node-Auth-Token"])
192+
193+
mockSigner.AssertExpectations(t)
194+
})
195+
}
196+
37197
func TestStaticAuthHeaderProvider(t *testing.T) {
38198
// Create test headers
39199
testHeaders := map[string]string{

0 commit comments

Comments
 (0)