tangled
alpha
login
or
join now
futur.blue
/
pegasus
57
fork
atom
objective categorical abstract machine language personal data server
57
fork
atom
overview
issues
2
pulls
pipelines
Fix dpop nonce rotation
futur.blue
2 months ago
7a7e753c
4c76d5e1
verified
This commit was signed with the committer's
known signature
.
futur.blue
SSH Key Fingerprint:
SHA256:QHGqHWNpqYyw9bt8KmPuJIyeZX9SZewBZ0PR1COtKQ0=
+44
-48
4 changed files
expand all
collapse all
unified
split
pegasus
lib
api
oauth_
token.ml
oauth
constants.ml
dpop.ml
xrpc.ml
-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)
0
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) ->
0
0
0
0
0
0
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 }
0
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
0
0
0
0
32
{ secret
33
; counter
34
+
; prev= compute_nonce secret (pred counter)
35
; curr= compute_nonce secret counter
36
+
; next= compute_nonce secret (succ counter) }
0
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
0
0
0
0
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 ->
0
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)
0
54
| _ ->
55
+
!nonce_state.prev <- compute_nonce !nonce_state.secret (pred now_counter) ;
0
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"
0
0
0
0
0
0
0
109
| _ ->
110
-
"DPoP-Nonce" ) ;
0
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
0
0
0
0
350
351
let cors_middleware inner_handler req =
352
let%lwt res = inner_handler req in