···5959 state.next
60606161let verify_nonce state nonce =
6262- nonce = state.prev || nonce = state.curr || nonce = state.next
6262+ let valid = nonce = state.prev || nonce = state.curr || nonce = state.next in
6363+ next_nonce state |> ignore ;
6464+ valid
6565+6666+let add_jti jti =
6767+ let expires_at = int_of_float (Unix.gettimeofday ()) + jti_ttl_s in
6868+ if Hashtbl.mem jti_cache jti then false (* replay *)
6969+ else (
7070+ Hashtbl.add jti_cache jti expires_at ;
7171+ (* clean up every once in a while *)
7272+ if Hashtbl.length jti_cache mod 100 = 0 then cleanup_jti_cache () ;
7373+ true )
7474+7575+let normalize_url url =
7676+ let uri = Uri.of_string url in
7777+ Uri.make ~scheme:"https"
7878+ ~host:(Uri.host uri |> Option.get)
7979+ ?port:(Uri.port uri) ~path:(Uri.path uri) ()
8080+ |> Uri.to_string
8181+8282+let b64url_decode s =
8383+ Base64.decode_exn ~alphabet:Base64.uri_safe_alphabet ~pad:false s
63846485let compute_jwk_thumbprint jwk =
6586 let open Yojson.Safe.Util in
···6889 let x = jwk |> member "x" |> to_string in
6990 let y = jwk |> member "y" |> to_string in
7091 let tp =
9292+ (* keys must be in lexicographic order *)
7193 Printf.sprintf {|{"crv":"%s","kty":"%s","x":"%s","y":"%s"}|} crv kty x y
7294 in
7395 Digestif.SHA256.(
7496 digest_string tp |> to_raw_string |> Base64.encode_exn ~pad:false )
75977676-let normalize_url url =
7777- let uri = Uri.of_string url in
7878- Uri.make ~scheme:"https"
7979- ~host:(Uri.host uri |> Option.get)
8080- ?port:(Uri.port uri) ~path:(Uri.path uri) ()
8181- |> Uri.to_string
8282-8383-let verify_signature jwt jwk alg =
9898+let verify_signature jwt jwk =
8499 let open Yojson.Safe.Util in
85100 let parts = String.split_on_char '.' jwt in
86101 match parts with
···90105 Digestif.SHA256.(digest_string signing_input |> to_raw_string)
91106 |> Bytes.of_string
92107 in
9393- let x = jwk |> member "x" |> to_string |> Base64.decode_exn ~pad:false in
9494- let y = jwk |> member "y" |> to_string |> Base64.decode_exn ~pad:false in
9595- let pubkey =
9696- Bytes.cat (Bytes.of_string "\x04") (Bytes.of_string (x ^ y))
108108+ let x =
109109+ jwk |> member "x" |> to_string |> b64url_decode |> Bytes.of_string
97110 in
111111+ let y =
112112+ jwk |> member "y" |> to_string |> b64url_decode |> Bytes.of_string
113113+ in
114114+ let crv = jwk |> member "crv" |> to_string in
115115+ let pubkey = Bytes.cat (Bytes.of_string "\x04") (Bytes.cat x y) in
98116 let pubkey =
99117 ( pubkey
100100- , match alg with
101101- | "ES256K" ->
118118+ , match crv with
119119+ | "secp256k1" ->
102120 (module Kleidos.K256 : Kleidos.CURVE)
103103- | "ES256" ->
121121+ | "P-256" ->
104122 (module Kleidos.P256 : Kleidos.CURVE)
105123 | _ ->
106124 failwith "unsupported algorithm" )
107125 in
108108- let sig_bytes = Base64.decode_exn sig_b64 in
109109- let r = String.sub sig_bytes 0 32 in
110110- let s = String.sub sig_bytes 32 32 in
111111- let signature = Bytes.of_string (r ^ s) in
126126+ let sig_bytes = b64url_decode sig_b64 |> Bytes.of_string in
127127+ let r = Bytes.sub sig_bytes 0 32 in
128128+ let s = Bytes.sub sig_bytes 32 32 in
129129+ let signature = Bytes.cat r s in
112130 Kleidos.verify ~pubkey ~msg ~signature
113131 | _ ->
114132 false
···121139 let open Yojson.Safe.Util in
122140 match String.split_on_char '.' jwt with
123141 | [header_b64; payload_b64; _] -> (
124124- let header = Yojson.Safe.from_string (Base64.decode_exn header_b64) in
125125- let payload =
126126- Yojson.Safe.from_string (Base64.decode_exn payload_b64)
127127- in
142142+ let header = Yojson.Safe.from_string (b64url_decode header_b64) in
143143+ let payload = Yojson.Safe.from_string (b64url_decode payload_b64) in
128144 let typ = header |> member "typ" |> to_string in
129145 if typ <> "dpop+jwt" then Lwt.return_error "invalid typ in dpop proof"
130146 else
···133149 Lwt.return_error "only es256 and es256k supported for dpop"
134150 else
135151 let jwk = header |> member "jwk" in
136136- let jti = payload |> member "jti" |> to_string in
137137- let htm = payload |> member "htm" |> to_string in
138138- let htu = payload |> member "htu" |> to_string in
139139- let iat = payload |> member "iat" |> to_int in
140140- let nonce_claim = payload |> member "nonce" |> to_string_option in
141141- match nonce_claim with
142142- (* error must be this string; see https://datatracker.ietf.org/doc/html/rfc9449#section-8 *)
143143- | None ->
144144- Lwt.return_error "use_dpop_nonce"
145145- | Some n when not (verify_nonce nonce_state n) ->
146146- Lwt.return_error "use_dpop_nonce"
147147- | Some _ ->
148148- if htm <> mthd then Lwt.return_error "htm mismatch"
149149- else if
150150- not (String.equal (normalize_url htu) (normalize_url url))
151151- then Lwt.return_error "htu mismatch"
152152- else
153153- let now = int_of_float (Unix.gettimeofday ()) in
154154- if now - iat > max_age_s then
155155- Lwt.return_error "dpop proof too old"
156156- else if Hashtbl.mem jti_cache jti then
157157- Lwt.return_error "dpop proof replay detected"
158158- else (
159159- Hashtbl.add jti_cache jti (now + jti_ttl_s) ;
160160- if not (verify_signature jwt jwk alg) then
152152+ let crv = jwk |> member "crv" |> to_string in
153153+ if
154154+ not
155155+ ( match (alg, crv) with
156156+ | "ES256", "P-256" ->
157157+ true
158158+ | "ES256K", "secp256k1" ->
159159+ true
160160+ | _ ->
161161+ false )
162162+ then
163163+ Lwt.return_error
164164+ (Printf.sprintf "algorithm %s doesn't match curve %s" alg crv)
165165+ else
166166+ let jti = payload |> member "jti" |> to_string in
167167+ let htm = payload |> member "htm" |> to_string in
168168+ let htu = payload |> member "htu" |> to_string in
169169+ let iat = payload |> member "iat" |> to_int in
170170+ let nonce_claim =
171171+ payload |> member "nonce" |> to_string_option
172172+ in
173173+ match nonce_claim with
174174+ (* error must be this string; see https://datatracker.ietf.org/doc/html/rfc9449#section-8 *)
175175+ | None ->
176176+ Lwt.return_error "use_dpop_nonce"
177177+ | Some n when not (verify_nonce nonce_state n) ->
178178+ Lwt.return_error "use_dpop_nonce"
179179+ | Some _ -> (
180180+ if htm <> mthd then Lwt.return_error "htm mismatch"
181181+ else if
182182+ not (String.equal (normalize_url htu) (normalize_url url))
183183+ then Lwt.return_error "htu mismatch"
184184+ else
185185+ let now = int_of_float (Unix.gettimeofday ()) in
186186+ if now - iat > max_age_s then
187187+ Lwt.return_error "dpop proof too old"
188188+ else if iat - now > 5 then
189189+ Lwt.return_error "dpop proof in future"
190190+ else if not (add_jti jti) then
191191+ Lwt.return_error "dpop proof replay detected"
192192+ else if not (verify_signature jwt jwk) then
161193 Lwt.return_error "invalid dpop signature"
162194 else
163195 let jkt = compute_jwk_thumbprint jwk in