@@ -85,16 +85,48 @@ struct AcquiredToken {
8585}
8686
8787fn install_token_slot ( token : & str ) -> Result < TokenSlot > {
88- let bearer = AsciiMetadataValue :: try_from ( format ! ( "Bearer {token}" ) )
88+ let bearer = validate_gateway_bearer ( token) ?;
89+ Ok ( install_validated_token_slot ( bearer) )
90+ }
91+
92+ fn validate_gateway_bearer ( token : & str ) -> Result < AsciiMetadataValue > {
93+ AsciiMetadataValue :: try_from ( format ! ( "Bearer {token}" ) )
8994 . into_diagnostic ( )
90- . wrap_err ( "sandbox JWT contained characters not valid for a header value" ) ?;
95+ . wrap_err ( "sandbox JWT contained characters not valid for a header value" )
96+ }
97+
98+ fn install_validated_token_slot ( bearer : AsciiMetadataValue ) -> TokenSlot {
9199 if let Some ( existing) = TOKEN_SLOT . get ( ) {
92- * existing. write ( ) . expect ( "token slot poisoned" ) = bearer;
93- return Ok ( existing. clone ( ) ) ;
100+ * existing
101+ . write ( )
102+ . unwrap_or_else ( std:: sync:: PoisonError :: into_inner) = bearer;
103+ return existing. clone ( ) ;
94104 }
95105 let slot: TokenSlot = Arc :: new ( RwLock :: new ( bearer) ) ;
96106 let _ = TOKEN_SLOT . set ( slot. clone ( ) ) ;
97- Ok ( TOKEN_SLOT . get ( ) . cloned ( ) . unwrap_or ( slot) )
107+ TOKEN_SLOT . get ( ) . cloned ( ) . unwrap_or ( slot)
108+ }
109+
110+ #[ cfg( feature = "jwt" ) ]
111+ struct ValidatedSandboxRefresh {
112+ token : crate :: jwt:: SecretJwt ,
113+ expires_at : i64 ,
114+ credential_epoch : crate :: jwt:: CredentialEpoch ,
115+ }
116+
117+ #[ cfg( feature = "jwt" ) ]
118+ fn validate_sandbox_refresh (
119+ response : & crate :: proto:: RefreshSandboxTokenResponse ,
120+ ) -> std:: result:: Result < ValidatedSandboxRefresh , crate :: jwt:: SessionJwtError > {
121+ let token = crate :: jwt:: SecretJwt :: parse ( response. sandbox_token . clone ( ) ) ?;
122+ let credential_epoch = crate :: jwt:: CredentialEpoch :: new ( response. credential_epoch ) ?;
123+ let expires_at = response. sandbox_expires_at_ms / 1000 ;
124+ crate :: jwt:: SessionBearerTokenSlot :: new ( token. clone ( ) , expires_at, credential_epoch) ?;
125+ Ok ( ValidatedSandboxRefresh {
126+ token,
127+ expires_at,
128+ credential_epoch,
129+ } )
98130}
99131
100132/// Install the gateway-session credential supplied in trusted supervisor
@@ -380,28 +412,37 @@ async fn refresh_token_loop(
380412 {
381413 Ok ( resp) => {
382414 let response = resp. into_inner ( ) ;
383- let new_token = response. token ;
384- match AsciiMetadataValue :: try_from ( format ! ( "Bearer {new_token}" ) ) {
385- Ok ( value) => {
386- if let Ok ( mut guard) = slot. write ( ) {
387- * guard = value;
388- info ! ( "renewed gateway sandbox JWT in-place" ) ;
389- }
390- }
391- Err ( e) => warn ! ( error = %e, "refreshed JWT contained invalid header bytes" ) ,
392- }
393415 #[ cfg( feature = "jwt" ) ]
394- if let Some ( sandbox_slot) = SANDBOX_BEARER_SLOT . get ( ) {
395- let update =
396- crate :: jwt:: SecretJwt :: parse ( response. sandbox_token ) . and_then ( |token| {
397- let epoch =
398- crate :: jwt:: CredentialEpoch :: new ( response. credential_epoch ) ?;
399- sandbox_slot. update ( token, response. sandbox_expires_at_ms / 1000 , epoch)
400- } ) ;
401- if let Err ( error) = update {
416+ let sandbox_refresh = match validate_sandbox_refresh ( & response) {
417+ Ok ( refresh) => refresh,
418+ Err ( error) => {
402419 warn ! ( %error, "gateway returned an invalid Sandbox Protocol credential" ) ;
420+ continue ;
421+ }
422+ } ;
423+ let gateway_bearer = match validate_gateway_bearer ( & response. token ) {
424+ Ok ( value) => value,
425+ Err ( error) => {
426+ warn ! ( %error, "refreshed JWT contained invalid header bytes" ) ;
427+ continue ;
403428 }
429+ } ;
430+
431+ * slot
432+ . write ( )
433+ . unwrap_or_else ( std:: sync:: PoisonError :: into_inner) = gateway_bearer;
434+ #[ cfg( feature = "jwt" ) ]
435+ if let Some ( sandbox_slot) = SANDBOX_BEARER_SLOT . get ( )
436+ && let Err ( error) = sandbox_slot. update (
437+ sandbox_refresh. token ,
438+ sandbox_refresh. expires_at ,
439+ sandbox_refresh. credential_epoch ,
440+ )
441+ && error != crate :: jwt:: SessionJwtError :: StaleCredentialEpoch
442+ {
443+ warn ! ( %error, "gateway returned an invalid Sandbox Protocol credential" ) ;
404444 }
445+ info ! ( "renewed gateway and Sandbox Protocol credentials in-place" ) ;
405446 }
406447 Err ( status) => {
407448 if status. code ( ) == tonic:: Code :: Unauthenticated
@@ -494,28 +535,11 @@ async fn refresh_extension_credentials_with_client(
494535 . wrap_err ( "failed to refresh extension service credentials" ) ?
495536 . into_inner ( ) ;
496537
497- // The same refresh response renews the gateway credential. Install it
498- // before returning so all process-wide gateway clients stay current. This
499- // is a superset of what the dedicated renewal loop would do, so letting it
500- // land early is harmless.
501- install_token_slot ( & response. token ) ?;
538+ let gateway_bearer = validate_gateway_bearer ( & response. token ) ?;
502539 #[ cfg( feature = "jwt" ) ]
503- if let Some ( sandbox_slot) = SANDBOX_BEARER_SLOT . get ( ) {
504- let token = crate :: jwt:: SecretJwt :: parse ( response. sandbox_token . clone ( ) )
505- . into_diagnostic ( )
506- . wrap_err ( "gateway returned an invalid Sandbox Protocol token" ) ?;
507- let epoch = crate :: jwt:: CredentialEpoch :: new ( response. credential_epoch )
508- . into_diagnostic ( )
509- . wrap_err ( "gateway returned an invalid Sandbox Protocol credential epoch" ) ?;
510- match sandbox_slot. update ( token, response. sandbox_expires_at_ms / 1000 , epoch) {
511- Ok ( ( ) ) | Err ( crate :: jwt:: SessionJwtError :: StaleCredentialEpoch ) => { }
512- Err ( error) => {
513- return Err ( miette:: miette!(
514- "gateway returned an invalid Sandbox Protocol credential: {error}"
515- ) ) ;
516- }
517- }
518- }
540+ let sandbox_refresh = validate_sandbox_refresh ( & response)
541+ . into_diagnostic ( )
542+ . wrap_err ( "gateway returned an invalid Sandbox Protocol credential" ) ?;
519543
520544 // Validate the whole response before mutating any slot, so a malformed or
521545 // partial reply cannot leave the store half-rotated.
@@ -544,6 +568,32 @@ async fn refresh_extension_credentials_with_client(
544568 }
545569
546570 let now_ms = now_ms ( ) ;
571+ for ( token, expires_at_ms) in validated. values ( ) {
572+ BearerTokenSlot :: new ( token, * expires_at_ms)
573+ . into_diagnostic ( )
574+ . wrap_err ( "gateway returned an invalid extension credential" ) ?;
575+ }
576+
577+ // Commit only after the gateway, Sandbox Protocol, and extension
578+ // credentials have all been parsed and validated. The remaining updates
579+ // repeat those validations but cannot fail for the validated inputs.
580+ install_validated_token_slot ( gateway_bearer) ;
581+ #[ cfg( feature = "jwt" ) ]
582+ if let Some ( sandbox_slot) = SANDBOX_BEARER_SLOT . get ( ) {
583+ match sandbox_slot. update (
584+ sandbox_refresh. token ,
585+ sandbox_refresh. expires_at ,
586+ sandbox_refresh. credential_epoch ,
587+ ) {
588+ Ok ( ( ) ) | Err ( crate :: jwt:: SessionJwtError :: StaleCredentialEpoch ) => { }
589+ Err ( error) => {
590+ return Err ( miette:: miette!(
591+ "validated Sandbox Protocol credential could not be installed: {error}"
592+ ) ) ;
593+ }
594+ }
595+ }
596+
547597 let mut selected = HashMap :: with_capacity ( validated. len ( ) ) ;
548598 for ( name, ( token, expires_at_ms) ) in validated {
549599 let slot = store
@@ -603,6 +653,22 @@ fn parse_jwt_exp_ms(jwt: &str) -> Option<i64> {
603653mod auth_tests {
604654 use super :: * ;
605655
656+ #[ cfg( feature = "jwt" ) ]
657+ #[ test]
658+ fn sandbox_refresh_validation_rejects_invalid_lifetime_before_installation ( ) {
659+ let response = crate :: proto:: RefreshSandboxTokenResponse {
660+ sandbox_token : "sandbox-token" . to_string ( ) ,
661+ sandbox_expires_at_ms : 999 ,
662+ credential_epoch : 2 ,
663+ ..Default :: default ( )
664+ } ;
665+
666+ assert_eq ! (
667+ validate_sandbox_refresh( & response) . err( ) ,
668+ Some ( crate :: jwt:: SessionJwtError :: InvalidLifetime )
669+ ) ;
670+ }
671+
606672 #[ test]
607673 fn parse_jwt_exp_reads_unsigned_payload ( ) {
608674 use base64:: Engine as _;
0 commit comments