@@ -29,14 +29,27 @@ fn epoch_ms() -> u64 {
2929 . unwrap_or ( 0 )
3030}
3131
32- fn mint_token ( secret : & [ u8 ; 32 ] , for_node : u64 , ttl_secs : u64 ) -> String {
32+ fn mint_token (
33+ secret : & [ u8 ; 32 ] ,
34+ for_node : u64 ,
35+ ttl_secs : u64 ,
36+ bootstrap_issuer_spki : [ u8 ; 32 ] ,
37+ bootstrap_ca_der : & [ u8 ] ,
38+ ) -> String {
3339 use std:: time:: { SystemTime , UNIX_EPOCH } ;
3440 let expiry = SystemTime :: now ( )
3541 . duration_since ( UNIX_EPOCH )
3642 . unwrap ( )
3743 . as_secs ( )
3844 + ttl_secs;
39- tok:: issue_token ( secret, for_node, expiry) . unwrap ( )
45+ tok:: issue_token (
46+ secret,
47+ for_node,
48+ expiry,
49+ bootstrap_issuer_spki,
50+ bootstrap_ca_der,
51+ )
52+ . unwrap ( )
4053}
4154
4255/// A `BootstrapHandler` that wires token state machine + audit writer
@@ -72,12 +85,12 @@ impl<B: TokenStateBackend, A: AuditWriter> BootstrapHandler for StatefulBootstra
7285 fn handle < ' a > (
7386 & ' a self ,
7487 req : BootstrapCredsRequest ,
88+ remote_addr : SocketAddr ,
7589 ) -> std:: pin:: Pin < Box < dyn std:: future:: Future < Output = BootstrapCredsResponse > + Send + ' a > >
7690 {
7791 Box :: pin ( async move {
7892 // Step 1: constant-time HMAC verification.
79- let ( token_node, expiry_secs) = match verify_token ( & req. token_hex , & self . cluster_secret )
80- {
93+ let verified = match verify_token ( & req. token_hex , & self . cluster_secret ) {
8194 Ok ( v) => v,
8295 Err ( e) => {
8396 // PERSIST audit before responding (acquire-log-then-respond).
@@ -95,7 +108,7 @@ impl<B: TokenStateBackend, A: AuditWriter> BootstrapHandler for StatefulBootstra
95108 }
96109 } ;
97110
98- if token_node != req. node_id {
111+ if verified . for_node != req. node_id {
99112 let th = tok:: token_hash ( & req. token_hex ) . unwrap_or ( [ 0u8 ; 32 ] ) ;
100113 self . audit
101114 . append ( nodedb_cluster:: auth:: audit:: AuditEvent :: new (
@@ -107,8 +120,8 @@ impl<B: TokenStateBackend, A: AuditWriter> BootstrapHandler for StatefulBootstra
107120 } ,
108121 ) ) ;
109122 return BootstrapCredsResponse :: error ( format ! (
110- "node id mismatch: token bound to {token_node }, request claims {}" ,
111- req. node_id
123+ "node id mismatch: token bound to {}, request claims {}" ,
124+ verified . for_node , req. node_id
112125 ) ) ;
113126 }
114127
@@ -119,15 +132,18 @@ impl<B: TokenStateBackend, A: AuditWriter> BootstrapHandler for StatefulBootstra
119132 }
120133 } ;
121134
122- // Step 2: check token state machine.
123- // Use a dummy addr since we don't have the remote addr here.
124- let dummy_addr: SocketAddr = "0.0.0.0:0" . parse ( ) . unwrap ( ) ;
125- match self
135+ if verified. bootstrap_ca_der . as_slice ( ) != self . ca . cert_der ( ) . as_ref ( ) {
136+ return BootstrapCredsResponse :: error ( "token CA mismatch" ) ;
137+ }
138+ let expiry_secs = verified. expiry_unix_secs ;
139+
140+ // Step 2: check token state machine, bound to the actual peer.
141+ let lease = match self
126142 . token_store
127- . begin_inflight ( & token_hash, dummy_addr )
143+ . begin_inflight ( & token_hash, remote_addr )
128144 . await
129145 {
130- Ok ( ( ) ) => { }
146+ Ok ( lease ) => lease ,
131147 Err ( TokenStateError :: AlreadyConsumed ) => {
132148 self . audit
133149 . append ( nodedb_cluster:: auth:: audit:: AuditEvent :: new (
@@ -160,40 +176,43 @@ impl<B: TokenStateBackend, A: AuditWriter> BootstrapHandler for StatefulBootstra
160176 attempt : 0 ,
161177 } )
162178 . await ;
163- if self
179+ match self
164180 . token_store
165- . begin_inflight ( & token_hash, dummy_addr )
181+ . begin_inflight ( & token_hash, remote_addr )
166182 . await
167- . is_err ( )
168183 {
169- return BootstrapCredsResponse :: error ( "token state conflict" ) ;
184+ Ok ( lease) => lease,
185+ Err ( _) => {
186+ return BootstrapCredsResponse :: error ( "token state conflict" ) ;
187+ }
170188 }
171189 }
172190 Err ( e) => {
173191 return BootstrapCredsResponse :: error ( format ! ( "token state: {e}" ) ) ;
174192 }
175- }
193+ } ;
176194
177195 // Spawn the dead-man timer.
178196 nodedb_cluster:: auth:: token_state:: spawn_inflight_timeout (
179197 Arc :: clone ( & self . token_store ) ,
180198 token_hash,
199+ lease,
181200 self . inflight_timeout ,
182201 ) ;
183202
184203 // Step 3: issue creds.
185204 let resp = match self . issue ( req. node_id ) {
186205 Ok ( r) => r,
187206 Err ( e) => {
188- let _ = self . token_store . revert_inflight ( & token_hash) . await ;
207+ let _ = self . token_store . revert_inflight ( & token_hash, lease ) . await ;
189208 return BootstrapCredsResponse :: error ( e) ;
190209 }
191210 } ;
192211
193212 // Step 4: transition to Consumed — persist audit BEFORE returning.
194213 let _ = self
195214 . token_store
196- . mark_consumed ( & token_hash, dummy_addr , epoch_ms ( ) )
215+ . mark_consumed ( & token_hash, remote_addr , lease , epoch_ms ( ) , Vec :: new ( ) )
197216 . await ;
198217 self . audit
199218 . append ( nodedb_cluster:: auth:: audit:: AuditEvent :: new (
@@ -227,18 +246,38 @@ fn make_handler(
227246 ( h, audit)
228247}
229248
249+ fn spawn_test_listener < H : BootstrapHandler > (
250+ ca : & nexar:: transport:: tls:: ClusterCa ,
251+ handler : Arc < H > ,
252+ shutdown : tokio:: sync:: watch:: Receiver < bool > ,
253+ ) -> ( SocketAddr , tokio:: task:: JoinHandle < ( ) > , [ u8 ; 32 ] ) {
254+ let credentials =
255+ issue_leaf_for_sans ( ca, & [ nodedb_cluster:: transport:: config:: SNI_HOSTNAME ] ) . unwrap ( ) ;
256+ let issuer_spki = credentials. spki_pin ;
257+ let ( addr, task) = spawn_listener (
258+ "127.0.0.1:0" . parse ( ) . unwrap ( ) ,
259+ credentials. cert ,
260+ credentials. key ,
261+ handler,
262+ shutdown,
263+ )
264+ . unwrap ( ) ;
265+ ( addr, task, issuer_spki)
266+ }
267+
230268// ── Tests ────────────────────────────────────────────────────────────────────
231269
232270#[ tokio:: test]
233271async fn valid_token_accepted_and_audit_logged ( ) {
234272 let secret = [ 0x11u8 ; 32 ] ;
235273 let ( handler, audit) = make_handler ( secret) ;
274+ let ca = Arc :: clone ( & handler. ca ) ;
236275
237276 let ( tx, rx) = tokio:: sync:: watch:: channel ( false ) ;
238- let ( local, _join) = spawn_listener ( "127.0.0.1:0" . parse ( ) . unwrap ( ) , handler, rx) . unwrap ( ) ;
277+ let ( local, _join, issuer_spki ) = spawn_test_listener ( & ca , handler, rx) ;
239278 tokio:: time:: sleep ( Duration :: from_millis ( 30 ) ) . await ;
240279
241- let token = mint_token ( & secret, 5 , 60 ) ;
280+ let token = mint_token ( & secret, 5 , 60 , issuer_spki , ca . cert_der ( ) . as_ref ( ) ) ;
242281 let resp =
243282 nodedb_cluster:: bootstrap_listener:: fetch_creds ( local, & token, 5 , Duration :: from_secs ( 3 ) )
244283 . await
@@ -262,17 +301,72 @@ async fn valid_token_accepted_and_audit_logged() {
262301 tx. send ( true ) . unwrap ( ) ;
263302}
264303
304+ #[ tokio:: test]
305+ async fn token_bound_to_wrong_ca_is_rejected_before_bootstrap_request ( ) {
306+ let secret = [ 0x12u8 ; 32 ] ;
307+ let ( handler, audit) = make_handler ( secret) ;
308+ let server_ca = Arc :: clone ( & handler. ca ) ;
309+ let ( other_ca, other_creds) = generate_node_credentials_multi_san ( & [ "other-ca" ] ) . unwrap ( ) ;
310+ let token = mint_token (
311+ & secret,
312+ 6 ,
313+ 60 ,
314+ other_creds. spki_pin ,
315+ other_ca. cert_der ( ) . as_ref ( ) ,
316+ ) ;
317+
318+ let ( tx, rx) = tokio:: sync:: watch:: channel ( false ) ;
319+ let ( local, _join, _) = spawn_test_listener ( & server_ca, handler, rx) ;
320+ tokio:: time:: sleep ( Duration :: from_millis ( 30 ) ) . await ;
321+
322+ let error =
323+ nodedb_cluster:: bootstrap_listener:: fetch_creds ( local, & token, 6 , Duration :: from_secs ( 3 ) )
324+ . await
325+ . unwrap_err ( ) ;
326+ assert ! ( error. to_string( ) . contains( "bootstrap connect" ) ) ;
327+ assert ! (
328+ audit. snapshot( ) . is_empty( ) ,
329+ "server handler must not see the token"
330+ ) ;
331+ tx. send ( true ) . unwrap ( ) ;
332+ }
333+
334+ #[ tokio:: test]
335+ async fn token_bound_to_same_ca_nonissuer_is_rejected_before_request ( ) {
336+ let secret = [ 0x13u8 ; 32 ] ;
337+ let ( handler, audit) = make_handler ( secret) ;
338+ let ca = Arc :: clone ( & handler. ca ) ;
339+ let nonissuer = issue_leaf_for_sans ( & ca, & [ "nonissuer" ] ) . unwrap ( ) ;
340+ let token = mint_token ( & secret, 6 , 60 , nonissuer. spki_pin , ca. cert_der ( ) . as_ref ( ) ) ;
341+
342+ let ( tx, rx) = tokio:: sync:: watch:: channel ( false ) ;
343+ let ( local, _join, _) = spawn_test_listener ( & ca, handler, rx) ;
344+ tokio:: time:: sleep ( Duration :: from_millis ( 30 ) ) . await ;
345+
346+ let error =
347+ nodedb_cluster:: bootstrap_listener:: fetch_creds ( local, & token, 6 , Duration :: from_secs ( 3 ) )
348+ . await
349+ . unwrap_err ( ) ;
350+ assert ! ( error. to_string( ) . contains( "issuer identity mismatch" ) ) ;
351+ assert ! (
352+ audit. snapshot( ) . is_empty( ) ,
353+ "handler must not see bearer token"
354+ ) ;
355+ tx. send ( true ) . unwrap ( ) ;
356+ }
357+
265358#[ tokio:: test]
266359async fn invalid_hmac_rejected_audit_logged ( ) {
267360 let secret = [ 0x22u8 ; 32 ] ;
268361 let ( handler, audit) = make_handler ( secret) ;
362+ let ca = Arc :: clone ( & handler. ca ) ;
269363
270364 let ( tx, rx) = tokio:: sync:: watch:: channel ( false ) ;
271- let ( local, _join) = spawn_listener ( "127.0.0.1:0" . parse ( ) . unwrap ( ) , handler, rx) . unwrap ( ) ;
365+ let ( local, _join, issuer_spki ) = spawn_test_listener ( & ca , handler, rx) ;
272366 tokio:: time:: sleep ( Duration :: from_millis ( 30 ) ) . await ;
273367
274368 // Token issued under a different secret.
275- let bad_token = mint_token ( & [ 0xFFu8 ; 32 ] , 2 , 60 ) ;
369+ let bad_token = mint_token ( & [ 0xFFu8 ; 32 ] , 2 , 60 , issuer_spki , ca . cert_der ( ) . as_ref ( ) ) ;
276370 let err = nodedb_cluster:: bootstrap_listener:: fetch_creds (
277371 local,
278372 & bad_token,
@@ -301,12 +395,13 @@ async fn invalid_hmac_rejected_audit_logged() {
301395async fn replayed_token_rejected ( ) {
302396 let secret = [ 0x33u8 ; 32 ] ;
303397 let ( handler, audit) = make_handler ( secret) ;
398+ let ca = Arc :: clone ( & handler. ca ) ;
304399
305400 let ( tx, rx) = tokio:: sync:: watch:: channel ( false ) ;
306- let ( local, _join) = spawn_listener ( "127.0.0.1:0" . parse ( ) . unwrap ( ) , handler, rx) . unwrap ( ) ;
401+ let ( local, _join, issuer_spki ) = spawn_test_listener ( & ca , handler, rx) ;
307402 tokio:: time:: sleep ( Duration :: from_millis ( 30 ) ) . await ;
308403
309- let token = mint_token ( & secret, 7 , 60 ) ;
404+ let token = mint_token ( & secret, 7 , 60 , issuer_spki , ca . cert_der ( ) . as_ref ( ) ) ;
310405
311406 // First use: should succeed.
312407 let resp1 =
@@ -343,13 +438,15 @@ async fn replayed_token_rejected() {
343438async fn expired_token_rejected_audit_logged ( ) {
344439 let secret = [ 0x44u8 ; 32 ] ;
345440 let ( handler, audit) = make_handler ( secret) ;
441+ let ca = Arc :: clone ( & handler. ca ) ;
346442
347443 let ( tx, rx) = tokio:: sync:: watch:: channel ( false ) ;
348- let ( local, _join) = spawn_listener ( "127.0.0.1:0" . parse ( ) . unwrap ( ) , handler, rx) . unwrap ( ) ;
444+ let ( local, _join, issuer_spki ) = spawn_test_listener ( & ca , handler, rx) ;
349445 tokio:: time:: sleep ( Duration :: from_millis ( 30 ) ) . await ;
350446
351447 // Issue a token that is already expired (expiry = 1 second past unix epoch).
352- let expired_token = tok:: issue_token ( & secret, 3 , 1 ) . unwrap ( ) ;
448+ let expired_token =
449+ tok:: issue_token ( & secret, 3 , 1 , issuer_spki, ca. cert_der ( ) . as_ref ( ) ) . unwrap ( ) ;
353450 let err = nodedb_cluster:: bootstrap_listener:: fetch_creds (
354451 local,
355452 & expired_token,
@@ -432,8 +529,11 @@ async fn token_state_issued_to_consumed() {
432529 attempt : 0 ,
433530 } )
434531 . await ;
435- store. begin_inflight ( & hash, addr) . await . unwrap ( ) ;
436- store. mark_consumed ( & hash, addr, epoch_ms ( ) ) . await . unwrap ( ) ;
532+ let lease = store. begin_inflight ( & hash, addr) . await . unwrap ( ) ;
533+ store
534+ . mark_consumed ( & hash, addr, lease, epoch_ms ( ) , Vec :: new ( ) )
535+ . await
536+ . unwrap ( ) ;
437537 let s = store. get ( & hash) . unwrap ( ) ;
438538 assert ! ( matches!( s. lifecycle, JoinTokenLifecycle :: Consumed { .. } ) ) ;
439539}
@@ -452,8 +552,11 @@ async fn token_state_replay_on_consumed_returns_error() {
452552 attempt : 0 ,
453553 } )
454554 . await ;
455- store. begin_inflight ( & hash, addr) . await . unwrap ( ) ;
456- store. mark_consumed ( & hash, addr, epoch_ms ( ) ) . await . unwrap ( ) ;
555+ let lease = store. begin_inflight ( & hash, addr) . await . unwrap ( ) ;
556+ store
557+ . mark_consumed ( & hash, addr, lease, epoch_ms ( ) , Vec :: new ( ) )
558+ . await
559+ . unwrap ( ) ;
457560 assert_eq ! (
458561 store. begin_inflight( & hash, addr) . await . unwrap_err( ) ,
459562 TokenStateError :: AlreadyConsumed
@@ -494,8 +597,8 @@ async fn token_state_inflight_reverts_on_timeout() {
494597 attempt : 0 ,
495598 } )
496599 . await ;
497- store. begin_inflight ( & hash, addr) . await . unwrap ( ) ;
498- store. revert_inflight ( & hash) . await . unwrap ( ) ;
600+ let lease = store. begin_inflight ( & hash, addr) . await . unwrap ( ) ;
601+ store. revert_inflight ( & hash, lease ) . await . unwrap ( ) ;
499602 let s = store. get ( & hash) . unwrap ( ) ;
500603 assert_eq ! ( s. lifecycle, JoinTokenLifecycle :: Issued ) ;
501604 assert_eq ! ( s. attempt, 1 ) ;
0 commit comments