objective categorical abstract machine language personal data server

Fix dpop nonce rotation

futur.blue 7a7e753c 4c76d5e1

verified
+44 -48
-1
pegasus/lib/api/oauth_/token.ml
··· 128 128 Dream.json 129 129 ~headers: 130 130 [ ("DPoP-Nonce", nonce) 131 - ; ("Access-Control-Expose-Headers", "DPoP-Nonce") 132 131 ; ("Cache-Control", "no-store") ] 133 132 @@ Yojson.Safe.to_string 134 133 @@ `Assoc
+3 -3
pegasus/lib/oauth/constants.ml
··· 1 - let max_dpop_age_s = 60 1 + let max_dpop_age_s = 180 2 2 3 - let dpop_rotation_interval_ms = 60_000L 3 + let dpop_rotation_interval_ms = 60_000 4 4 5 - let jti_ttl_s = 3600 5 + let jti_ttl_s = 24 * 60 * 60 6 6 7 7 let jti_cache_size = 10_000 8 8
+23 -30
pegasus/lib/oauth/dpop.ml
··· 1 1 type nonce_state = 2 2 { secret: bytes 3 - ; mutable counter: int64 3 + ; mutable counter: int 4 4 ; mutable prev: string 5 5 ; mutable curr: string 6 - ; mutable next: string 7 - ; rotation_interval_ms: int64 } 6 + ; mutable next: string } 8 7 9 8 type ec_jwk = {crv: string; kty: string; x: string; y: string} 10 9 [@@deriving yojson {strict= false}] ··· 16 15 Hashtbl.create Constants.jti_cache_size 17 16 18 17 let cleanup_jti_cache () = 19 - let now = int_of_float (Unix.gettimeofday ()) in 18 + let now = Util.now_ms () in 20 19 Hashtbl.filter_map_inplace 21 20 (fun _ expires_at -> if expires_at > now then Some expires_at else None) 22 21 jti_cache 23 22 24 23 let compute_nonce secret counter = 25 24 let data = Bytes.create 8 in 26 - Bytes.set_int64_be data 0 counter ; 25 + Bytes.set_int64_be data 0 (Int64.of_int counter) ; 27 26 Digestif.SHA256.( 28 27 hmac_bytes ~key:(Bytes.to_string secret) data 29 28 |> to_raw_string |> Jwt.b64_encode ) 30 29 31 30 let create_nonce_state secret = 32 - let counter = 33 - Int64.div 34 - (Int64.of_float (Unix.gettimeofday () *. 1000.)) 35 - Constants.dpop_rotation_interval_ms 36 - in 31 + let counter = Util.now_ms () / Constants.dpop_rotation_interval_ms in 37 32 { secret 38 33 ; counter 39 - ; prev= compute_nonce secret (Int64.pred counter) 34 + ; prev= compute_nonce secret (pred counter) 40 35 ; curr= compute_nonce secret counter 41 - ; next= compute_nonce secret (Int64.succ counter) 42 - ; rotation_interval_ms= Constants.dpop_rotation_interval_ms } 36 + ; next= compute_nonce secret (succ counter) } 43 37 44 38 let nonce_state = ref (create_nonce_state Env.dpop_nonce_secret) 45 39 46 40 let next_nonce () = 47 - let now_counter = 48 - Int64.div 49 - (Int64.of_float (Unix.gettimeofday () *. 1000.)) 50 - !nonce_state.rotation_interval_ms 51 - in 52 - let diff = Int64.sub now_counter !nonce_state.counter in 41 + let now_counter = Util.now_ms () / Constants.dpop_rotation_interval_ms in 42 + let diff = now_counter - !nonce_state.counter in 53 43 ( match diff with 54 - | 0L -> 44 + | 0 -> 55 45 () 56 - | 1L -> 46 + | 1 -> 57 47 !nonce_state.prev <- !nonce_state.curr ; 58 48 !nonce_state.curr <- !nonce_state.next ; 59 - !nonce_state.next <- 60 - compute_nonce !nonce_state.secret (Int64.succ now_counter) 61 - | 2L -> 49 + !nonce_state.next <- compute_nonce !nonce_state.secret (succ now_counter) 50 + | 2 -> 62 51 !nonce_state.prev <- !nonce_state.next ; 63 52 !nonce_state.curr <- compute_nonce !nonce_state.secret now_counter ; 64 - !nonce_state.next <- 65 - compute_nonce !nonce_state.secret (Int64.succ now_counter) 53 + !nonce_state.next <- compute_nonce !nonce_state.secret (succ now_counter) 66 54 | _ -> 67 - !nonce_state.prev <- 68 - compute_nonce !nonce_state.secret (Int64.pred now_counter) ; 55 + !nonce_state.prev <- compute_nonce !nonce_state.secret (pred now_counter) ; 69 56 !nonce_state.curr <- compute_nonce !nonce_state.secret now_counter ; 70 - !nonce_state.next <- 71 - compute_nonce !nonce_state.secret (Int64.succ now_counter) ) ; 57 + !nonce_state.next <- compute_nonce !nonce_state.secret (succ now_counter) 58 + ) ; 72 59 !nonce_state.counter <- now_counter ; 73 60 !nonce_state.next 74 61 ··· 189 176 | None -> 190 177 Error "use_dpop_nonce" 191 178 | Some n when not (verify_nonce n) -> 179 + Log.debug (fun log -> 180 + let state = !nonce_state in 181 + log 182 + "given nonce %s, failed to match any of: %s, %s, \ 183 + %s" 184 + n state.prev state.curr state.next ) ; 192 185 Error "use_dpop_nonce" 193 186 | Some _ -> ( 194 187 if htm <> mthd then Error "htm mismatch"
+18 -14
pegasus/lib/xrpc.ml
··· 99 99 let extract_nsid req = (Dream.path [@warning "-3"]) req |> List.rev |> List.hd 100 100 101 101 let add_dpop_nonce_if_needed res = 102 - let nonce = Oauth.Dpop.next_nonce () in 103 - Dream.set_header res "DPoP-Nonce" nonce ; 104 - let expose_header = Dream.header res "Access-Control-Expose-Headers" in 105 - Dream.add_header res "Access-Control-Expose-Headers" 106 - ( match expose_header with 107 - | Some headers when not @@ Util.str_contains ~affix:"DPoP-Nonce" headers -> 108 - headers ^ ", DPoP-Nonce" 102 + let () = 103 + match Dream.header res "DPoP-Nonce" with 104 + | Some _ -> 105 + () 106 + | None -> 107 + Dream.set_header res "DPoP-Nonce" (Oauth.Dpop.next_nonce ()) 108 + in 109 + let () = 110 + match Dream.header res "Access-Control-Expose-Headers" with 111 + | Some header when Util.str_contains ~affix:"DPoP-Nonce" header -> 112 + () 113 + | Some header -> 114 + Dream.set_header res "Access-Control-Expose-Headers" 115 + (header ^ ", DPoP-Nonce") 109 116 | _ -> 110 - "DPoP-Nonce" ) ; 117 + Dream.set_header res "Access-Control-Expose-Headers" "DPoP-Nonce" 118 + in 111 119 res 112 120 113 121 let handler ?(auth : Auth.Verifiers.t = Any) ··· 337 345 Option.is_some dpop 338 346 || Option.is_some www_auth 339 347 && Option.get www_auth |> Util.str_contains ~affix:"DPoP" 340 - then begin 341 - Dream.set_header res "DPoP-Nonce" (Oauth.Dpop.next_nonce ()) ; 342 - Dream.add_header res "Access-Control-Expose-Headers" 343 - "DPoP-Nonce, WWW-Authenticate" 344 - end ; 345 - Lwt.return res 348 + then Lwt.return @@ add_dpop_nonce_if_needed res 349 + else Lwt.return res 346 350 347 351 let cors_middleware inner_handler req = 348 352 let%lwt res = inner_handler req in