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