@@ -3,6 +3,7 @@ package beholder_test
33import (
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+
37197func TestStaticAuthHeaderProvider (t * testing.T ) {
38198 // Create test headers
39199 testHeaders := map [string ]string {
0 commit comments