objective categorical abstract machine language personal data server
65
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix jwt signature verification

futurGH 987aad0a 61b5e937

+81 -49
+81 -49
pegasus/lib/oauth/dpop.ml
··· 59 59 state.next 60 60 61 61 let verify_nonce state nonce = 62 - nonce = state.prev || nonce = state.curr || nonce = state.next 62 + let valid = nonce = state.prev || nonce = state.curr || nonce = state.next in 63 + next_nonce state |> ignore ; 64 + valid 65 + 66 + let add_jti jti = 67 + let expires_at = int_of_float (Unix.gettimeofday ()) + jti_ttl_s in 68 + if Hashtbl.mem jti_cache jti then false (* replay *) 69 + else ( 70 + Hashtbl.add jti_cache jti expires_at ; 71 + (* clean up every once in a while *) 72 + if Hashtbl.length jti_cache mod 100 = 0 then cleanup_jti_cache () ; 73 + true ) 74 + 75 + let normalize_url url = 76 + let uri = Uri.of_string url in 77 + Uri.make ~scheme:"https" 78 + ~host:(Uri.host uri |> Option.get) 79 + ?port:(Uri.port uri) ~path:(Uri.path uri) () 80 + |> Uri.to_string 81 + 82 + let b64url_decode s = 83 + Base64.decode_exn ~alphabet:Base64.uri_safe_alphabet ~pad:false s 63 84 64 85 let compute_jwk_thumbprint jwk = 65 86 let open Yojson.Safe.Util in ··· 68 89 let x = jwk |> member "x" |> to_string in 69 90 let y = jwk |> member "y" |> to_string in 70 91 let tp = 92 + (* keys must be in lexicographic order *) 71 93 Printf.sprintf {|{"crv":"%s","kty":"%s","x":"%s","y":"%s"}|} crv kty x y 72 94 in 73 95 Digestif.SHA256.( 74 96 digest_string tp |> to_raw_string |> Base64.encode_exn ~pad:false ) 75 97 76 - let normalize_url url = 77 - let uri = Uri.of_string url in 78 - Uri.make ~scheme:"https" 79 - ~host:(Uri.host uri |> Option.get) 80 - ?port:(Uri.port uri) ~path:(Uri.path uri) () 81 - |> Uri.to_string 82 - 83 - let verify_signature jwt jwk alg = 98 + let verify_signature jwt jwk = 84 99 let open Yojson.Safe.Util in 85 100 let parts = String.split_on_char '.' jwt in 86 101 match parts with ··· 90 105 Digestif.SHA256.(digest_string signing_input |> to_raw_string) 91 106 |> Bytes.of_string 92 107 in 93 - let x = jwk |> member "x" |> to_string |> Base64.decode_exn ~pad:false in 94 - let y = jwk |> member "y" |> to_string |> Base64.decode_exn ~pad:false in 95 - let pubkey = 96 - Bytes.cat (Bytes.of_string "\x04") (Bytes.of_string (x ^ y)) 108 + let x = 109 + jwk |> member "x" |> to_string |> b64url_decode |> Bytes.of_string 97 110 in 111 + let y = 112 + jwk |> member "y" |> to_string |> b64url_decode |> Bytes.of_string 113 + in 114 + let crv = jwk |> member "crv" |> to_string in 115 + let pubkey = Bytes.cat (Bytes.of_string "\x04") (Bytes.cat x y) in 98 116 let pubkey = 99 117 ( pubkey 100 - , match alg with 101 - | "ES256K" -> 118 + , match crv with 119 + | "secp256k1" -> 102 120 (module Kleidos.K256 : Kleidos.CURVE) 103 - | "ES256" -> 121 + | "P-256" -> 104 122 (module Kleidos.P256 : Kleidos.CURVE) 105 123 | _ -> 106 124 failwith "unsupported algorithm" ) 107 125 in 108 - let sig_bytes = Base64.decode_exn sig_b64 in 109 - let r = String.sub sig_bytes 0 32 in 110 - let s = String.sub sig_bytes 32 32 in 111 - let signature = Bytes.of_string (r ^ s) in 126 + let sig_bytes = b64url_decode sig_b64 |> Bytes.of_string in 127 + let r = Bytes.sub sig_bytes 0 32 in 128 + let s = Bytes.sub sig_bytes 32 32 in 129 + let signature = Bytes.cat r s in 112 130 Kleidos.verify ~pubkey ~msg ~signature 113 131 | _ -> 114 132 false ··· 121 139 let open Yojson.Safe.Util in 122 140 match String.split_on_char '.' jwt with 123 141 | [header_b64; payload_b64; _] -> ( 124 - let header = Yojson.Safe.from_string (Base64.decode_exn header_b64) in 125 - let payload = 126 - Yojson.Safe.from_string (Base64.decode_exn payload_b64) 127 - in 142 + let header = Yojson.Safe.from_string (b64url_decode header_b64) in 143 + let payload = Yojson.Safe.from_string (b64url_decode payload_b64) in 128 144 let typ = header |> member "typ" |> to_string in 129 145 if typ <> "dpop+jwt" then Lwt.return_error "invalid typ in dpop proof" 130 146 else ··· 133 149 Lwt.return_error "only es256 and es256k supported for dpop" 134 150 else 135 151 let jwk = header |> member "jwk" in 136 - let jti = payload |> member "jti" |> to_string in 137 - let htm = payload |> member "htm" |> to_string in 138 - let htu = payload |> member "htu" |> to_string in 139 - let iat = payload |> member "iat" |> to_int in 140 - let nonce_claim = payload |> member "nonce" |> to_string_option in 141 - match nonce_claim with 142 - (* error must be this string; see https://datatracker.ietf.org/doc/html/rfc9449#section-8 *) 143 - | None -> 144 - Lwt.return_error "use_dpop_nonce" 145 - | Some n when not (verify_nonce nonce_state n) -> 146 - Lwt.return_error "use_dpop_nonce" 147 - | Some _ -> 148 - if htm <> mthd then Lwt.return_error "htm mismatch" 149 - else if 150 - not (String.equal (normalize_url htu) (normalize_url url)) 151 - then Lwt.return_error "htu mismatch" 152 - else 153 - let now = int_of_float (Unix.gettimeofday ()) in 154 - if now - iat > max_age_s then 155 - Lwt.return_error "dpop proof too old" 156 - else if Hashtbl.mem jti_cache jti then 157 - Lwt.return_error "dpop proof replay detected" 158 - else ( 159 - Hashtbl.add jti_cache jti (now + jti_ttl_s) ; 160 - if not (verify_signature jwt jwk alg) then 152 + let crv = jwk |> member "crv" |> to_string in 153 + if 154 + not 155 + ( match (alg, crv) with 156 + | "ES256", "P-256" -> 157 + true 158 + | "ES256K", "secp256k1" -> 159 + true 160 + | _ -> 161 + false ) 162 + then 163 + Lwt.return_error 164 + (Printf.sprintf "algorithm %s doesn't match curve %s" alg crv) 165 + else 166 + let jti = payload |> member "jti" |> to_string in 167 + let htm = payload |> member "htm" |> to_string in 168 + let htu = payload |> member "htu" |> to_string in 169 + let iat = payload |> member "iat" |> to_int in 170 + let nonce_claim = 171 + payload |> member "nonce" |> to_string_option 172 + in 173 + match nonce_claim with 174 + (* error must be this string; see https://datatracker.ietf.org/doc/html/rfc9449#section-8 *) 175 + | None -> 176 + Lwt.return_error "use_dpop_nonce" 177 + | Some n when not (verify_nonce nonce_state n) -> 178 + Lwt.return_error "use_dpop_nonce" 179 + | Some _ -> ( 180 + if htm <> mthd then Lwt.return_error "htm mismatch" 181 + else if 182 + not (String.equal (normalize_url htu) (normalize_url url)) 183 + then Lwt.return_error "htu mismatch" 184 + else 185 + let now = int_of_float (Unix.gettimeofday ()) in 186 + if now - iat > max_age_s then 187 + Lwt.return_error "dpop proof too old" 188 + else if iat - now > 5 then 189 + Lwt.return_error "dpop proof in future" 190 + else if not (add_jti jti) then 191 + Lwt.return_error "dpop proof replay detected" 192 + else if not (verify_signature jwt jwk) then 161 193 Lwt.return_error "invalid dpop signature" 162 194 else 163 195 let jkt = compute_jwk_thumbprint jwk in