@@ -197,59 +197,74 @@ fn canonicalize_json_value(value: &serde_json::Value) -> String {
197197 }
198198}
199199
200- /// Parse and verify a Bearer session token.
200+ /// Verify a session token from the Authorization header .
201201///
202- /// Token format: `{hotkey_hex}:{expiry_ms}:{nonce}:{signature_hex}`
203- /// Signed message: `session:{hotkey_ss58}:{expiry_ms}:{nonce}`
202+ /// Token format: `{session_pubkey_hex}:{timestamp}:{nonce}:{signature_hex}`
204203///
205- /// Returns the hotkey hex string if the token is valid.
206- /// Does NOT check revocation (that requires a WASM call to the auth challenge).
207- pub fn verify_bearer_token ( token : & str ) -> Result < BearerTokenInfo , AuthError > {
204+ /// The signature is over:
205+ /// `{method}:{path}:{body_hash}:{timestamp}:{nonce}`
206+ ///
207+ /// The session_pubkey is an ephemeral sr25519 public key registered via
208+ /// the auth challenge `/login`. The RPC verifies the signature, then calls
209+ /// the auth WASM challenge `/verify` to resolve the hotkey and check revocation.
210+ ///
211+ /// Validators cannot forge requests because they don't have the ephemeral
212+ /// private key. Each request has a unique signature (timestamp + nonce).
213+ pub fn verify_session_token (
214+ token : & str ,
215+ method : & str ,
216+ path : & str ,
217+ body : & [ u8 ] ,
218+ ) -> Result < SessionTokenInfo , AuthError > {
208219 let parts: Vec < & str > = token. splitn ( 4 , ':' ) . collect ( ) ;
209220 if parts. len ( ) != 4 {
210221 return Err ( AuthError :: InvalidSignature ) ;
211222 }
212223
213- let hotkey_hex = parts[ 0 ] ;
214- let expiry_str = parts[ 1 ] ;
224+ let session_pubkey_hex = parts[ 0 ] ;
225+ let timestamp_str = parts[ 1 ] ;
215226 let nonce = parts[ 2 ] ;
216227 let signature_hex = parts[ 3 ] ;
217228
218- // Parse hotkey
219- let hotkey = Hotkey :: from_hex ( hotkey_hex) . ok_or ( AuthError :: InvalidHotkey ) ?;
220-
221- // Parse expiry
222- let expiry: i64 = expiry_str. parse ( ) . map_err ( |_| AuthError :: InvalidNonce ) ?;
229+ // Validate session pubkey format (64 hex chars = 32 bytes)
230+ if session_pubkey_hex. len ( ) != 64 {
231+ return Err ( AuthError :: InvalidHotkey ) ;
232+ }
223233
224- // Check expiry
225- let now_ms = chrono :: Utc :: now ( ) . timestamp_millis ( ) ;
226- if now_ms > expiry {
234+ // Parse timestamp and check freshness (5 min window)
235+ let timestamp : i64 = timestamp_str . parse ( ) . map_err ( |_| AuthError :: InvalidNonce ) ? ;
236+ if ! verify_timestamp ( timestamp ) {
227237 return Err ( AuthError :: MessageExpired ) ;
228238 }
229239
230- // Verify signature
231- let hotkey_ss58 = hotkey. to_ss58 ( ) ;
232- let message = format ! ( "session:{}:{}:{}" , hotkey_ss58, expiry, nonce) ;
240+ // Build the signed message and verify
241+ let body_hash = {
242+ let canonical = if let Ok ( val) = serde_json:: from_slice :: < serde_json:: Value > ( body) {
243+ canonicalize_json_value ( & val) . into_bytes ( )
244+ } else {
245+ body. to_vec ( )
246+ } ;
247+ hex:: encode ( Sha256 :: digest ( & canonical) )
248+ } ;
249+
250+ let message = format ! ( "{}:{}:{}:{}:{}" , method, path, body_hash, timestamp, nonce) ;
233251
234- match verify_validator_signature ( hotkey_hex, & message, signature_hex) ? {
235- true => Ok ( BearerTokenInfo {
236- hotkey_hex : hotkey_hex. to_string ( ) ,
237- expiry,
238- nonce : nonce. to_string ( ) ,
252+ match verify_validator_signature ( session_pubkey_hex, & message, signature_hex) ? {
253+ true => Ok ( SessionTokenInfo {
254+ session_pubkey_hex : session_pubkey_hex. to_string ( ) ,
239255 } ) ,
240256 false => Err ( AuthError :: VerificationFailed ) ,
241257 }
242258}
243259
244- /// Parsed and verified bearer token info.
245- pub struct BearerTokenInfo {
246- pub hotkey_hex : String ,
247- pub expiry : i64 ,
248- pub nonce : String ,
260+ /// Parsed and verified session token info.
261+ pub struct SessionTokenInfo {
262+ pub session_pubkey_hex : String ,
249263}
250264
251- /// Extract Bearer token from Authorization header.
252- pub fn extract_bearer_token ( headers : & HashMap < String , String > ) -> Option < String > {
265+ /// Extract session token from Authorization header.
266+ /// Supports: `Authorization: Session {token}`
267+ pub fn extract_session_token ( headers : & HashMap < String , String > ) -> Option < String > {
253268 let headers_lower: HashMap < String , String > = headers
254269 . iter ( )
255270 . map ( |( k, v) | ( k. to_lowercase ( ) , v. clone ( ) ) )
@@ -258,8 +273,8 @@ pub fn extract_bearer_token(headers: &HashMap<String, String>) -> Option<String>
258273 headers_lower
259274 . get ( "authorization" )
260275 . and_then ( |v| {
261- v. strip_prefix ( "Bearer " )
262- . or_else ( || v. strip_prefix ( "bearer " ) )
276+ v. strip_prefix ( "Session " )
277+ . or_else ( || v. strip_prefix ( "session " ) )
263278 } )
264279 . map ( |t| t. trim ( ) . to_string ( ) )
265280}
0 commit comments