···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(* Crowbar-based fuzz testing for JWT parsing *)
77+88+open Crowbar
99+1010+(* Test that JWT parsing never crashes on arbitrary input *)
1111+let test_parse_no_crash input =
1212+ let _ = Jsonwt.parse input in
1313+ ()
1414+1515+(* Test that JWT parsing in unsafe mode never crashes *)
1616+let test_parse_unsafe_no_crash input =
1717+ let _ = Jsonwt.parse_unsafe input in
1818+ ()
1919+2020+(* Test that nested JWT parsing never crashes *)
2121+let test_parse_nested_no_crash input =
2222+ let _ = Jsonwt.parse_nested input in
2323+ ()
2424+2525+(* Test header parsing never crashes *)
2626+let test_header_parse_no_crash input =
2727+ let _ = Jsonwt.Header.of_json input in
2828+ ()
2929+3030+(* Test claims parsing never crashes *)
3131+let test_claims_parse_no_crash input =
3232+ let _ = Jsonwt.Claims.of_json input in
3333+ ()
3434+3535+(* Test JWK parsing never crashes *)
3636+let test_jwk_parse_no_crash input =
3737+ let _ = Jsonwt.Jwk.of_json input in
3838+ ()
3939+4040+(* Test algorithm parsing never crashes *)
4141+let test_algorithm_parse_no_crash input =
4242+ let _ = Jsonwt.Algorithm.of_string input in
4343+ ()
4444+4545+(* Test base64url-like inputs (dots are JWT separators) *)
4646+let test_jwt_structure input1 input2 input3 =
4747+ let token = input1 ^ "." ^ input2 ^ "." ^ input3 in
4848+ let _ = Jsonwt.parse token in
4949+ ()
5050+5151+(* Test error printing never crashes *)
5252+let () =
5353+ let errors =
5454+ [
5555+ Jsonwt.Invalid_json "test";
5656+ Jsonwt.Invalid_base64url "test";
5757+ Jsonwt.Invalid_structure "test";
5858+ Jsonwt.Invalid_header "test";
5959+ Jsonwt.Invalid_claims "test";
6060+ Jsonwt.Invalid_uri "test";
6161+ Jsonwt.Duplicate_claim "test";
6262+ Jsonwt.Unsupported_algorithm "test";
6363+ Jsonwt.Algorithm_not_allowed "test";
6464+ Jsonwt.Signature_mismatch;
6565+ Jsonwt.Token_expired;
6666+ Jsonwt.Token_not_yet_valid;
6767+ Jsonwt.Invalid_issuer;
6868+ Jsonwt.Invalid_audience;
6969+ Jsonwt.Key_type_mismatch "test";
7070+ Jsonwt.Unsecured_not_allowed;
7171+ Jsonwt.Nesting_too_deep;
7272+ ]
7373+ in
7474+ List.iter
7575+ (fun e ->
7676+ let _ = Format.asprintf "%a" Jsonwt.pp_error e in
7777+ let _ = Jsonwt.error_to_string e in
7878+ ())
7979+ errors
8080+8181+let () =
8282+ add_test ~name:"jwt: parse no crash" [ bytes ] test_parse_no_crash;
8383+ add_test ~name:"jwt: parse_unsafe no crash" [ bytes ]
8484+ test_parse_unsafe_no_crash;
8585+ add_test ~name:"jwt: parse_nested no crash" [ bytes ]
8686+ test_parse_nested_no_crash;
8787+ add_test ~name:"jwt: header parse no crash" [ bytes ]
8888+ test_header_parse_no_crash;
8989+ add_test ~name:"jwt: claims parse no crash" [ bytes ]
9090+ test_claims_parse_no_crash;
9191+ add_test ~name:"jwt: jwk parse no crash" [ bytes ] test_jwk_parse_no_crash;
9292+ add_test ~name:"jwt: algorithm parse no crash" [ bytes ]
9393+ test_algorithm_parse_no_crash;
9494+ add_test ~name:"jwt: structured input" [ bytes; bytes; bytes ]
9595+ test_jwt_structure
+383-308
ocaml-jsonwt/lib/cwt.ml
···3333 | Invalid_audience -> Format.fprintf ppf "Invalid audience"
3434 | Key_type_mismatch s -> Format.fprintf ppf "Key type mismatch: %s" s
35353636-let error_to_string e =
3737- Format.asprintf "%a" pp_error e
3636+let error_to_string e = Format.asprintf "%a" pp_error e
38373938(* Cbort codec helpers *)
40394141-let cbort_error_to_error e =
4242- Invalid_cbor (Cbort.Error.to_string e)
4040+let cbort_error_to_error e = Invalid_cbor (Cbort.Error.to_string e)
43414442(* COSE Algorithms - RFC 9053 *)
4543···8583 | HMAC_384 -> "HMAC 384/384"
8684 | HMAC_512 -> "HMAC 512/512"
87858888- let all = [ES256; ES384; ES512; EdDSA; HMAC_256_64; HMAC_256; HMAC_384; HMAC_512]
8686+ let all =
8787+ [ ES256; ES384; ES512; EdDSA; HMAC_256_64; HMAC_256; HMAC_384; HMAC_512 ]
8988end
90899190(* COSE Key - RFC 9052 Section 7 *)
92919392module Cose_key = struct
9494- type kty =
9595- | Okp
9696- | Ec2
9797- | Symmetric
9393+ type kty = Okp | Ec2 | Symmetric
98949995 (* COSE key labels *)
10096 let label_kty = 1
···104100 let label_x = -2
105101 let label_y = -3
106102 let label_d = -4
107107- let label_k = -1 (* for symmetric *)
103103+ let label_k = -1 (* for symmetric *)
108104109105 (* COSE key type values *)
110106 let kty_okp = 1
···134130 alg : Algorithm.t option;
135131 }
136132137137- let symmetric k =
138138- { key_data = Symmetric_key { k }; kid = None; alg = None }
133133+ let symmetric k = { key_data = Symmetric_key { k }; kid = None; alg = None }
139134140135 let ed25519_pub x =
141136 { key_data = Ed25519_pub { x }; kid = None; alg = Some Algorithm.EdDSA }
142137143138 let ed25519_priv ~pub ~priv =
144144- { key_data = Ed25519_priv { x = pub; d = priv }; kid = None; alg = Some Algorithm.EdDSA }
139139+ {
140140+ key_data = Ed25519_priv { x = pub; d = priv };
141141+ kid = None;
142142+ alg = Some Algorithm.EdDSA;
143143+ }
145144146145 let p256_pub ~x ~y =
147146 { key_data = P256_pub { x; y }; kid = None; alg = Some Algorithm.ES256 }
···165164 match t.key_data with
166165 | Symmetric_key _ -> Symmetric
167166 | Ed25519_pub _ | Ed25519_priv _ -> Okp
168168- | P256_pub _ | P256_priv _ | P384_pub _ | P384_priv _ | P521_pub _ | P521_priv _ -> Ec2
167167+ | P256_pub _ | P256_priv _ | P384_pub _ | P384_priv _ | P521_pub _
168168+ | P521_priv _ ->
169169+ Ec2
169170170171 let kid t = t.kid
171172 let alg t = t.alg
172172-173173 let with_kid id t = { t with kid = Some id }
174174 let with_alg a t = { t with alg = Some a }
175175···201201 let kty_val = get_int_value (find_int label_kty) in
202202 let crv_val = get_int_value (find_int label_crv) in
203203 let kid = find_kid label_kid in
204204- let alg = match get_int_value (find_int label_alg) with
204204+ let alg =
205205+ match get_int_value (find_int label_alg) with
205206 | None -> None
206206- | Some n -> (match Algorithm.of_cose_int n with Ok a -> Some a | Error _ -> None)
207207+ | Some n -> (
208208+ match Algorithm.of_cose_int n with
209209+ | Ok a -> Some a
210210+ | Error _ -> None)
207211 in
208212 let x = find_bytes label_x in
209213 let y = find_bytes label_y in
210214 let d = find_bytes label_d in
211215 let k = find_bytes label_k in
212212- let key_data = match kty_val, crv_val, x, y, d, k with
216216+ let key_data =
217217+ match (kty_val, crv_val, x, y, d, k) with
213218 | Some 4, _, _, _, _, Some k -> Ok (Symmetric_key { k })
214219 | Some 1, Some 6, Some x, _, None, _ -> Ok (Ed25519_pub { x })
215220 | Some 1, Some 6, Some x, _, Some d, _ -> Ok (Ed25519_priv { x; d })
216221 | Some 2, Some 1, Some x, Some y, None, _ -> Ok (P256_pub { x; y })
217217- | Some 2, Some 1, Some x, Some y, Some d, _ -> Ok (P256_priv { x; y; d })
222222+ | Some 2, Some 1, Some x, Some y, Some d, _ ->
223223+ Ok (P256_priv { x; y; d })
218224 | Some 2, Some 2, Some x, Some y, None, _ -> Ok (P384_pub { x; y })
219219- | Some 2, Some 2, Some x, Some y, Some d, _ -> Ok (P384_priv { x; y; d })
225225+ | Some 2, Some 2, Some x, Some y, Some d, _ ->
226226+ Ok (P384_priv { x; y; d })
220227 | Some 2, Some 3, Some x, Some y, None, _ -> Ok (P521_pub { x; y })
221221- | Some 2, Some 3, Some x, Some y, Some d, _ -> Ok (P521_priv { x; y; d })
222222- | _ -> Error (Invalid_cose "unsupported or invalid COSE key structure")
228228+ | Some 2, Some 3, Some x, Some y, Some d, _ ->
229229+ Ok (P521_priv { x; y; d })
230230+ | _ ->
231231+ Error (Invalid_cose "unsupported or invalid COSE key structure")
223232 in
224233 Result.map (fun key_data -> { key_data; kid; alg }) key_data
225234···231240232241 (* kty - always present *)
233242 (match t.key_data with
234234- | Symmetric_key _ -> add_int label_kty kty_symmetric
235235- | Ed25519_pub _ | Ed25519_priv _ -> add_int label_kty kty_okp
236236- | _ -> add_int label_kty kty_ec2);
243243+ | Symmetric_key _ -> add_int label_kty kty_symmetric
244244+ | Ed25519_pub _ | Ed25519_priv _ -> add_int label_kty kty_okp
245245+ | _ -> add_int label_kty kty_ec2);
237246238247 (* kid (optional) *)
239248 Option.iter (fun kid -> add_bytes label_kid kid) t.kid;
···243252244253 (* Key-type specific parameters *)
245254 (match t.key_data with
246246- | Symmetric_key { k } ->
247247- add_bytes label_k k
248248-249249- | Ed25519_pub { x } ->
250250- add_int label_crv crv_ed25519;
251251- add_bytes label_x x
252252-253253- | Ed25519_priv { x; d } ->
254254- add_int label_crv crv_ed25519;
255255- add_bytes label_x x;
256256- add_bytes label_d d
257257-258258- | P256_pub { x; y } ->
259259- add_int label_crv crv_p256;
260260- add_bytes label_x x;
261261- add_bytes label_y y
262262-263263- | P256_priv { x; y; d } ->
264264- add_int label_crv crv_p256;
265265- add_bytes label_x x;
266266- add_bytes label_y y;
267267- add_bytes label_d d
268268-269269- | P384_pub { x; y } ->
270270- add_int label_crv crv_p384;
271271- add_bytes label_x x;
272272- add_bytes label_y y
273273-274274- | P384_priv { x; y; d } ->
275275- add_int label_crv crv_p384;
276276- add_bytes label_x x;
277277- add_bytes label_y y;
278278- add_bytes label_d d
279279-280280- | P521_pub { x; y } ->
281281- add_int label_crv crv_p521;
282282- add_bytes label_x x;
283283- add_bytes label_y y
284284-285285- | P521_priv { x; y; d } ->
286286- add_int label_crv crv_p521;
287287- add_bytes label_x x;
288288- add_bytes label_y y;
289289- add_bytes label_d d);
255255+ | Symmetric_key { k } -> add_bytes label_k k
256256+ | Ed25519_pub { x } ->
257257+ add_int label_crv crv_ed25519;
258258+ add_bytes label_x x
259259+ | Ed25519_priv { x; d } ->
260260+ add_int label_crv crv_ed25519;
261261+ add_bytes label_x x;
262262+ add_bytes label_d d
263263+ | P256_pub { x; y } ->
264264+ add_int label_crv crv_p256;
265265+ add_bytes label_x x;
266266+ add_bytes label_y y
267267+ | P256_priv { x; y; d } ->
268268+ add_int label_crv crv_p256;
269269+ add_bytes label_x x;
270270+ add_bytes label_y y;
271271+ add_bytes label_d d
272272+ | P384_pub { x; y } ->
273273+ add_int label_crv crv_p384;
274274+ add_bytes label_x x;
275275+ add_bytes label_y y
276276+ | P384_priv { x; y; d } ->
277277+ add_int label_crv crv_p384;
278278+ add_bytes label_x x;
279279+ add_bytes label_y y;
280280+ add_bytes label_d d
281281+ | P521_pub { x; y } ->
282282+ add_int label_crv crv_p521;
283283+ add_bytes label_x x;
284284+ add_bytes label_y y
285285+ | P521_priv { x; y; d } ->
286286+ add_int label_crv crv_p521;
287287+ add_bytes label_x x;
288288+ add_bytes label_y y;
289289+ add_bytes label_d d);
290290291291 Cbort.encode_string Cbort.any (Cbort.Cbor.Map (List.rev !pairs))
292292end
···303303 let key_iat = 6
304304 let key_cti = 7
305305306306- type claim_key =
307307- | Int_key of int
308308- | String_key of string
306306+ type claim_key = Int_key of int | String_key of string
309307310308 type t = {
311309 iss : string option;
···327325 let cti t = t.cti
328326329327 let get_int_key key t =
330330- List.find_map (function
331331- | (Int_key k, v) when k = key -> Some v
332332- | _ -> None
333333- ) t.custom
328328+ List.find_map
329329+ (function Int_key k, v when k = key -> Some v | _ -> None)
330330+ t.custom
334331335332 let get_string_key key t =
336336- List.find_map (function
337337- | (String_key k, v) when k = key -> Some v
338338- | _ -> None
339339- ) t.custom
333333+ List.find_map
334334+ (function String_key k, v when k = key -> Some v | _ -> None)
335335+ t.custom
340336341337 type builder = t
342338343343- let empty = {
344344- iss = None; sub = None; aud = []; exp = None;
345345- nbf = None; iat = None; cti = None; custom = []
346346- }
339339+ let empty =
340340+ {
341341+ iss = None;
342342+ sub = None;
343343+ aud = [];
344344+ exp = None;
345345+ nbf = None;
346346+ iat = None;
347347+ cti = None;
348348+ custom = [];
349349+ }
347350348351 let set_iss v t = { t with iss = Some v }
349352 let set_sub v t = { t with sub = Some v }
···352355 let set_nbf v t = { t with nbf = Some v }
353356 let set_iat v t = { t with iat = Some v }
354357 let set_cti v t = { t with cti = Some v }
355355- let set_int_key key value t = { t with custom = (Int_key key, value) :: t.custom }
356356- let set_string_key key value t = { t with custom = (String_key key, value) :: t.custom }
358358+359359+ let set_int_key key value t =
360360+ { t with custom = (Int_key key, value) :: t.custom }
361361+362362+ let set_string_key key value t =
363363+ { t with custom = (String_key key, value) :: t.custom }
364364+357365 let build t = t
358366359367 (* Standard claim keys *)
360360- let standard_keys = [key_iss; key_sub; key_aud; key_exp; key_nbf; key_iat; key_cti]
368368+ let standard_keys =
369369+ [ key_iss; key_sub; key_aud; key_exp; key_nbf; key_iat; key_cti ]
361370362371 (* Helper to convert claim_key to CBOR *)
363372 let claim_key_to_cbor = function
···367376 (* Helper to find value by integer key in CBOR map *)
368377 let find_int_key key pairs =
369378 let target = Cbort.Cbor.Int (Z.of_int key) in
370370- List.find_map (fun (k, v) ->
371371- if Cbort.Cbor.equal k target then Some v else None
372372- ) pairs
379379+ List.find_map
380380+ (fun (k, v) -> if Cbort.Cbor.equal k target then Some v else None)
381381+ pairs
373382374383 (* Helper to extract string from CBOR *)
375375- let cbor_to_string = function
376376- | Cbort.Cbor.Text s -> Some s
377377- | _ -> None
384384+ let cbor_to_string = function Cbort.Cbor.Text s -> Some s | _ -> None
378385379386 (* Helper to extract bytes from CBOR *)
380380- let cbor_to_bytes = function
381381- | Cbort.Cbor.Bytes s -> Some s
382382- | _ -> None
387387+ let cbor_to_bytes = function Cbort.Cbor.Bytes s -> Some s | _ -> None
383388384389 (* Helper to extract ptime from CBOR integer *)
385390 let cbor_to_ptime = function
386386- | Cbort.Cbor.Int z ->
387387- Ptime.of_float_s (Z.to_float z)
391391+ | Cbort.Cbor.Int z -> Ptime.of_float_s (Z.to_float z)
388392 | _ -> None
389393390394 (* Helper to extract audience (string or array of strings) *)
391395 let cbor_to_aud = function
392392- | Cbort.Cbor.Text s -> Some [s]
396396+ | Cbort.Cbor.Text s -> Some [ s ]
393397 | Cbort.Cbor.Array items ->
394398 let strings = List.filter_map cbor_to_string items in
395395- if List.length strings = List.length items then Some strings
396396- else None
399399+ if List.length strings = List.length items then Some strings else None
397400 | _ -> None
398401399402 (* Decode claims from CBOR map pairs *)
400403 let decode_from_pairs pairs =
401404 let iss = Option.bind (find_int_key key_iss pairs) cbor_to_string in
402405 let sub = Option.bind (find_int_key key_sub pairs) cbor_to_string in
403403- let aud = Option.value ~default:[] (Option.bind (find_int_key key_aud pairs) cbor_to_aud) in
406406+ let aud =
407407+ Option.value ~default:[]
408408+ (Option.bind (find_int_key key_aud pairs) cbor_to_aud)
409409+ in
404410 let exp = Option.bind (find_int_key key_exp pairs) cbor_to_ptime in
405411 let nbf = Option.bind (find_int_key key_nbf pairs) cbor_to_ptime in
406412 let iat = Option.bind (find_int_key key_iat pairs) cbor_to_ptime in
407413 let cti = Option.bind (find_int_key key_cti pairs) cbor_to_bytes in
408414 (* Collect custom claims (non-standard keys) *)
409409- let custom = List.filter_map (fun (k, v) ->
410410- match k with
411411- | Cbort.Cbor.Int z ->
412412- let i = Z.to_int z in
413413- if List.mem i standard_keys then None
414414- else Some (Int_key i, v)
415415- | Cbort.Cbor.Text s -> Some (String_key s, v)
416416- | _ -> None
417417- ) pairs in
415415+ let custom =
416416+ List.filter_map
417417+ (fun (k, v) ->
418418+ match k with
419419+ | Cbort.Cbor.Int z ->
420420+ let i = Z.to_int z in
421421+ if List.mem i standard_keys then None else Some (Int_key i, v)
422422+ | Cbort.Cbor.Text s -> Some (String_key s, v)
423423+ | _ -> None)
424424+ pairs
425425+ in
418426 { iss; sub; aud; exp; nbf; iat; cti; custom }
419427420428 (* Encode claims to CBOR map pairs *)
···426434 Option.iter (fun v -> add_int key_iss (Text v)) t.iss;
427435 Option.iter (fun v -> add_int key_sub (Text v)) t.sub;
428436 (match t.aud with
429429- | [] -> ()
430430- | [s] -> add_int key_aud (Text s)
431431- | lst -> add_int key_aud (Array (List.map (fun s -> Text s) lst)));
432432- Option.iter (fun v ->
433433- add_int key_exp (Int (Z.of_float (Ptime.to_float_s v)))
434434- ) t.exp;
435435- Option.iter (fun v ->
436436- add_int key_nbf (Int (Z.of_float (Ptime.to_float_s v)))
437437- ) t.nbf;
438438- Option.iter (fun v ->
439439- add_int key_iat (Int (Z.of_float (Ptime.to_float_s v)))
440440- ) t.iat;
437437+ | [] -> ()
438438+ | [ s ] -> add_int key_aud (Text s)
439439+ | lst -> add_int key_aud (Array (List.map (fun s -> Text s) lst)));
440440+ Option.iter
441441+ (fun v -> add_int key_exp (Int (Z.of_float (Ptime.to_float_s v))))
442442+ t.exp;
443443+ Option.iter
444444+ (fun v -> add_int key_nbf (Int (Z.of_float (Ptime.to_float_s v))))
445445+ t.nbf;
446446+ Option.iter
447447+ (fun v -> add_int key_iat (Int (Z.of_float (Ptime.to_float_s v))))
448448+ t.iat;
441449 Option.iter (fun v -> add_int key_cti (Bytes v)) t.cti;
442450 (* Custom claims *)
443443- List.iter (fun (k, v) ->
444444- pairs := (claim_key_to_cbor k, v) :: !pairs
445445- ) t.custom;
451451+ List.iter
452452+ (fun (k, v) -> pairs := (claim_key_to_cbor k, v) :: !pairs)
453453+ t.custom;
446454 List.rev !pairs
447455448456 let claims_not_map_error = "claims must be a CBOR map"
···451459 let codec : t Cbort.t =
452460 Cbort.conv
453461 (fun cbor ->
454454- match cbor with
455455- | Cbort.Cbor.Map pairs -> Ok (decode_from_pairs pairs)
456456- | _ -> Error claims_not_map_error)
462462+ match cbor with
463463+ | Cbort.Cbor.Map pairs -> Ok (decode_from_pairs pairs)
464464+ | _ -> Error claims_not_map_error)
457465 (fun t -> Cbort.Cbor.Map (encode_to_pairs t))
458466 Cbort.any
459467···463471 | Error e ->
464472 (* Distinguish CBOR parse errors from claims structure errors *)
465473 let msg = Cbort.Error.to_string e in
466466- if msg = claims_not_map_error then
467467- Error (Invalid_claims msg)
468468- else
469469- Error (Invalid_cbor msg)
474474+ if msg = claims_not_map_error then Error (Invalid_claims msg)
475475+ else Error (Invalid_cbor msg)
470476471477 let to_cbor t = Cbort.encode_string codec t
472478end
···485491 claims : Claims.t;
486492 algorithm : Algorithm.t option;
487493 kid : string option;
488488- protected_header : string; (* CBOR-encoded protected header *)
489489- signature : string; (* Signature or MAC tag *)
490490- raw : string; (* Original CBOR bytes *)
494494+ protected_header : string; (* CBOR-encoded protected header *)
495495+ signature : string; (* Signature or MAC tag *)
496496+ raw : string; (* Original CBOR bytes *)
491497}
492498493499let claims t = t.claims
···498504(** Extract kid from header - can be Text or Bytes per RFC 9052 *)
499505let extract_kid_from_header pairs =
500506 let kid_key = Cbort.Cbor.Int (Z.of_int header_kid) in
501501- List.find_map (fun (k, v) ->
502502- if Cbort.Cbor.equal k kid_key then
503503- match v with
504504- | Cbort.Cbor.Bytes s -> Some s
505505- | Cbort.Cbor.Text s -> Some s
506506- | _ -> None
507507- else None
508508- ) pairs
507507+ List.find_map
508508+ (fun (k, v) ->
509509+ if Cbort.Cbor.equal k kid_key then
510510+ match v with
511511+ | Cbort.Cbor.Bytes s -> Some s
512512+ | Cbort.Cbor.Text s -> Some s
513513+ | _ -> None
514514+ else None)
515515+ pairs
509516510517(** Decode protected header to extract algorithm and kid *)
511518let decode_protected_header bytes =
···513520 | Error _ -> (None, None)
514521 | Ok (Cbort.Cbor.Map pairs) ->
515522 let alg_key = Cbort.Cbor.Int (Z.of_int header_alg) in
516516- let alg_int = List.find_map (fun (k, v) ->
517517- if Cbort.Cbor.equal k alg_key then
518518- match v with
519519- | Cbort.Cbor.Int z -> Some (Z.to_int z)
520520- | _ -> None
521521- else None
522522- ) pairs in
523523- let algorithm = Option.bind alg_int (fun n ->
524524- match Algorithm.of_cose_int n with
525525- | Ok alg -> Some alg
526526- | Error _ -> None)
523523+ let alg_int =
524524+ List.find_map
525525+ (fun (k, v) ->
526526+ if Cbort.Cbor.equal k alg_key then
527527+ match v with Cbort.Cbor.Int z -> Some (Z.to_int z) | _ -> None
528528+ else None)
529529+ pairs
530530+ in
531531+ let algorithm =
532532+ Option.bind alg_int (fun n ->
533533+ match Algorithm.of_cose_int n with
534534+ | Ok alg -> Some alg
535535+ | Error _ -> None)
527536 in
528537 let kid = extract_kid_from_header pairs in
529538 (algorithm, kid)
···538547let parse bytes =
539548 match Cbort.decode_string Cbort.any bytes with
540549 | Error e -> Error (cbort_error_to_error e)
541541- | Ok cbor ->
550550+ | Ok cbor -> (
542551 (* Handle optional COSE tag and extract the array *)
543543- let cose_array = match cbor with
544544- | Cbort.Cbor.Tag (18, arr) -> Some arr (* COSE_Sign1 *)
545545- | Cbort.Cbor.Tag (17, arr) -> Some arr (* COSE_Mac0 *)
552552+ let cose_array =
553553+ match cbor with
554554+ | Cbort.Cbor.Tag (18, arr) -> Some arr (* COSE_Sign1 *)
555555+ | Cbort.Cbor.Tag (17, arr) -> Some arr (* COSE_Mac0 *)
546556 | Cbort.Cbor.Array _ as arr -> Some arr (* Untagged *)
547557 | _ -> None
548558 in
549559 match cose_array with
550550- | None -> Error (Invalid_cose "expected COSE_Sign1 or COSE_Mac0 structure")
551551- | Some (Cbort.Cbor.Array [protected_bstr; unprotected; payload_bstr; sig_bstr]) ->
560560+ | None ->
561561+ Error (Invalid_cose "expected COSE_Sign1 or COSE_Mac0 structure")
562562+ | Some
563563+ (Cbort.Cbor.Array
564564+ [ protected_bstr; unprotected; payload_bstr; sig_bstr ]) -> (
552565 (* Extract byte strings *)
553553- let protected_header = match protected_bstr with
554554- | Cbort.Cbor.Bytes s -> Some s
555555- | _ -> None
566566+ let protected_header =
567567+ match protected_bstr with Cbort.Cbor.Bytes s -> Some s | _ -> None
556568 in
557557- let signature = match sig_bstr with
558558- | Cbort.Cbor.Bytes s -> Some s
559559- | _ -> None
569569+ let signature =
570570+ match sig_bstr with Cbort.Cbor.Bytes s -> Some s | _ -> None
560571 in
561561- (match protected_header, signature with
562562- | Some protected_header, Some signature ->
563563- (* Decode protected header for algorithm and kid *)
564564- let (algorithm, protected_kid) = decode_protected_header protected_header in
565565- (* Decode unprotected header for kid - prefer unprotected over protected *)
566566- let unprotected_kid = decode_unprotected_header unprotected in
567567- let kid = match unprotected_kid with
568568- | Some _ -> unprotected_kid
569569- | None -> protected_kid
570570- in
571571- (* Decode claims from payload - handle detached payloads *)
572572- (match payload_bstr with
573573- | Cbort.Cbor.Null ->
574574- (* Detached payload: not currently supported *)
575575- Error (Invalid_cose "detached payloads are not supported")
576576- | Cbort.Cbor.Bytes payload ->
577577- (match Claims.of_cbor payload with
578578- | Error e -> Error e
579579- | Ok claims ->
580580- Ok { claims; algorithm; kid; protected_header; signature; raw = bytes })
581581- | _ -> Error (Invalid_cose "payload must be a byte string or null"))
582582- | _ -> Error (Invalid_cose "invalid COSE structure fields"))
572572+ match (protected_header, signature) with
573573+ | Some protected_header, Some signature -> (
574574+ (* Decode protected header for algorithm and kid *)
575575+ let algorithm, protected_kid =
576576+ decode_protected_header protected_header
577577+ in
578578+ (* Decode unprotected header for kid - prefer unprotected over protected *)
579579+ let unprotected_kid = decode_unprotected_header unprotected in
580580+ let kid =
581581+ match unprotected_kid with
582582+ | Some _ -> unprotected_kid
583583+ | None -> protected_kid
584584+ in
585585+ (* Decode claims from payload - handle detached payloads *)
586586+ match payload_bstr with
587587+ | Cbort.Cbor.Null ->
588588+ (* Detached payload: not currently supported *)
589589+ Error (Invalid_cose "detached payloads are not supported")
590590+ | Cbort.Cbor.Bytes payload -> (
591591+ match Claims.of_cbor payload with
592592+ | Error e -> Error e
593593+ | Ok claims ->
594594+ Ok
595595+ {
596596+ claims;
597597+ algorithm;
598598+ kid;
599599+ protected_header;
600600+ signature;
601601+ raw = bytes;
602602+ })
603603+ | _ ->
604604+ Error (Invalid_cose "payload must be a byte string or null"))
605605+ | _ -> Error (Invalid_cose "invalid COSE structure fields"))
583606 | Some (Cbort.Cbor.Array _) ->
584607 Error (Invalid_cose "COSE structure must have exactly 4 elements")
585585- | Some _ ->
586586- Error (Invalid_cose "expected COSE array structure")
608608+ | Some _ -> Error (Invalid_cose "expected COSE array structure"))
587609588610(* Cryptographic operations *)
589611···614636 | Error _ -> Error (Key_type_mismatch "Invalid P-256 private key")
615637 | Ok priv ->
616638 let hash = Digestif.SHA256.(digest_string payload |> to_raw_string) in
617617- let (r, s) = Mirage_crypto_ec.P256.Dsa.sign ~key:priv hash in
639639+ let r, s = Mirage_crypto_ec.P256.Dsa.sign ~key:priv hash in
618640 let pad32 s =
619641 let len = String.length s in
620642 if len >= 32 then String.sub s (len - 32) 32
···627649 | Error _ -> Error (Key_type_mismatch "Invalid P-384 private key")
628650 | Ok priv ->
629651 let hash = Digestif.SHA384.(digest_string payload |> to_raw_string) in
630630- let (r, s) = Mirage_crypto_ec.P384.Dsa.sign ~key:priv hash in
652652+ let r, s = Mirage_crypto_ec.P384.Dsa.sign ~key:priv hash in
631653 let pad48 s =
632654 let len = String.length s in
633655 if len >= 48 then String.sub s (len - 48) 48
···640662 | Error _ -> Error (Key_type_mismatch "Invalid P-521 private key")
641663 | Ok priv ->
642664 let hash = Digestif.SHA512.(digest_string payload |> to_raw_string) in
643643- let (r, s) = Mirage_crypto_ec.P521.Dsa.sign ~key:priv hash in
665665+ let r, s = Mirage_crypto_ec.P521.Dsa.sign ~key:priv hash in
644666 let pad66 s =
645667 let len = String.length s in
646668 if len >= 66 then String.sub s (len - 66) 66
···656678(** Build Sig_structure or MAC_structure for COSE operations *)
657679let build_sig_structure ~context_string ~protected_header ~payload =
658680 let open Cbort.Cbor in
659659- Array [
660660- Text context_string;
661661- Bytes protected_header;
662662- Bytes ""; (* external_aad = empty *)
663663- Bytes payload;
664664- ]
681681+ Array
682682+ [
683683+ Text context_string;
684684+ Bytes protected_header;
685685+ Bytes "";
686686+ (* external_aad = empty *)
687687+ Bytes payload;
688688+ ]
665689 |> Cbort.encode_string Cbort.any
666690667691(** Expected signature/MAC length for each algorithm *)
668692let expected_sig_length = function
669669- | Algorithm.ES256 -> 64 (* 32 + 32 *)
670670- | Algorithm.ES384 -> 96 (* 48 + 48 *)
671671- | Algorithm.ES512 -> 132 (* 66 + 66 *)
693693+ | Algorithm.ES256 -> 64 (* 32 + 32 *)
694694+ | Algorithm.ES384 -> 96 (* 48 + 48 *)
695695+ | Algorithm.ES512 -> 132 (* 66 + 66 *)
672696 | Algorithm.EdDSA -> 64
673697 | Algorithm.HMAC_256_64 -> 8
674698 | Algorithm.HMAC_256 -> 32
···677701678702let verify ~key ?allowed_algs t =
679703 (* Check algorithm is allowed *)
680680- let alg = match t.algorithm with
704704+ let alg =
705705+ match t.algorithm with
681706 | None -> Error (Invalid_cose "No algorithm in protected header")
682707 | Some a -> Ok a
683708 in
684709 match alg with
685710 | Error e -> Error e
686711 | Ok alg ->
687687- let allowed = match allowed_algs with
688688- | None -> Algorithm.all
689689- | Some l -> l
712712+ let allowed =
713713+ match allowed_algs with None -> Algorithm.all | Some l -> l
690714 in
691715 if not (List.mem alg allowed) then
692716 Error (Algorithm_not_allowed (Algorithm.to_string alg))
···695719 let expected_len = expected_sig_length alg in
696720 let actual_len = String.length t.signature in
697721 if actual_len <> expected_len then
698698- Error (Invalid_cose (Printf.sprintf
699699- "signature length mismatch: expected %d, got %d" expected_len actual_len))
722722+ Error
723723+ (Invalid_cose
724724+ (Printf.sprintf "signature length mismatch: expected %d, got %d"
725725+ expected_len actual_len))
700726 else
701727 (* Build Sig_structure or MAC_structure for verification *)
702702- let context_string = match alg with
703703- | Algorithm.HMAC_256_64 | Algorithm.HMAC_256
704704- | Algorithm.HMAC_384 | Algorithm.HMAC_512 -> "MAC0"
728728+ let context_string =
729729+ match alg with
730730+ | Algorithm.HMAC_256_64 | Algorithm.HMAC_256 | Algorithm.HMAC_384
731731+ | Algorithm.HMAC_512 ->
732732+ "MAC0"
705733 | _ -> "Signature1"
706734 in
707735 let payload = Claims.to_cbor t.claims in
708708- let sig_structure = build_sig_structure
709709- ~context_string ~protected_header:t.protected_header ~payload
736736+ let sig_structure =
737737+ build_sig_structure ~context_string
738738+ ~protected_header:t.protected_header ~payload
710739 in
711740 (* Verify based on algorithm - returns Result to distinguish key mismatch from sig failure *)
712712- let verify_result = match alg, key.Cose_key.key_data with
713713- | (Algorithm.HMAC_256_64 | Algorithm.HMAC_256
714714- | Algorithm.HMAC_384 | Algorithm.HMAC_512), Cose_key.Symmetric_key { k } ->
741741+ let verify_result =
742742+ match (alg, key.Cose_key.key_data) with
743743+ | ( ( Algorithm.HMAC_256_64 | Algorithm.HMAC_256
744744+ | Algorithm.HMAC_384 | Algorithm.HMAC_512 ),
745745+ Cose_key.Symmetric_key { k } ) ->
715746 if hmac_verify alg k sig_structure t.signature then Ok ()
716747 else Error Signature_mismatch
717717- | Algorithm.EdDSA, (Cose_key.Ed25519_pub { x } | Cose_key.Ed25519_priv { x; _ }) ->
718718- (match Mirage_crypto_ec.Ed25519.pub_of_octets x with
719719- | Ok pub ->
720720- if Mirage_crypto_ec.Ed25519.verify ~key:pub t.signature ~msg:sig_structure
721721- then Ok ()
722722- else Error Signature_mismatch
723723- | Error _ -> Error (Key_type_mismatch "Invalid Ed25519 public key"))
724724- | Algorithm.ES256, (Cose_key.P256_pub { x; y } | Cose_key.P256_priv { x; y; _ }) ->
725725- (match Mirage_crypto_ec.P256.Dsa.pub_of_octets ("\x04" ^ x ^ y) with
726726- | Ok pub ->
727727- let hash = Digestif.SHA256.(digest_string sig_structure |> to_raw_string) in
728728- let r = String.sub t.signature 0 32 in
729729- let s = String.sub t.signature 32 32 in
730730- if Mirage_crypto_ec.P256.Dsa.verify ~key:pub (r, s) hash
731731- then Ok ()
732732- else Error Signature_mismatch
733733- | Error _ -> Error (Key_type_mismatch "Invalid P-256 public key"))
734734- | Algorithm.ES384, (Cose_key.P384_pub { x; y } | Cose_key.P384_priv { x; y; _ }) ->
735735- (match Mirage_crypto_ec.P384.Dsa.pub_of_octets ("\x04" ^ x ^ y) with
736736- | Ok pub ->
737737- let hash = Digestif.SHA384.(digest_string sig_structure |> to_raw_string) in
738738- let r = String.sub t.signature 0 48 in
739739- let s = String.sub t.signature 48 48 in
740740- if Mirage_crypto_ec.P384.Dsa.verify ~key:pub (r, s) hash
741741- then Ok ()
742742- else Error Signature_mismatch
743743- | Error _ -> Error (Key_type_mismatch "Invalid P-384 public key"))
744744- | Algorithm.ES512, (Cose_key.P521_pub { x; y } | Cose_key.P521_priv { x; y; _ }) ->
745745- (match Mirage_crypto_ec.P521.Dsa.pub_of_octets ("\x04" ^ x ^ y) with
746746- | Ok pub ->
747747- let hash = Digestif.SHA512.(digest_string sig_structure |> to_raw_string) in
748748- let r = String.sub t.signature 0 66 in
749749- let s = String.sub t.signature 66 66 in
750750- if Mirage_crypto_ec.P521.Dsa.verify ~key:pub (r, s) hash
751751- then Ok ()
752752- else Error Signature_mismatch
753753- | Error _ -> Error (Key_type_mismatch "Invalid P-521 public key"))
748748+ | ( Algorithm.EdDSA,
749749+ (Cose_key.Ed25519_pub { x } | Cose_key.Ed25519_priv { x; _ }) )
750750+ -> (
751751+ match Mirage_crypto_ec.Ed25519.pub_of_octets x with
752752+ | Ok pub ->
753753+ if
754754+ Mirage_crypto_ec.Ed25519.verify ~key:pub t.signature
755755+ ~msg:sig_structure
756756+ then Ok ()
757757+ else Error Signature_mismatch
758758+ | Error _ ->
759759+ Error (Key_type_mismatch "Invalid Ed25519 public key"))
760760+ | ( Algorithm.ES256,
761761+ (Cose_key.P256_pub { x; y } | Cose_key.P256_priv { x; y; _ }) )
762762+ -> (
763763+ match
764764+ Mirage_crypto_ec.P256.Dsa.pub_of_octets ("\x04" ^ x ^ y)
765765+ with
766766+ | Ok pub ->
767767+ let hash =
768768+ Digestif.SHA256.(
769769+ digest_string sig_structure |> to_raw_string)
770770+ in
771771+ let r = String.sub t.signature 0 32 in
772772+ let s = String.sub t.signature 32 32 in
773773+ if Mirage_crypto_ec.P256.Dsa.verify ~key:pub (r, s) hash
774774+ then Ok ()
775775+ else Error Signature_mismatch
776776+ | Error _ ->
777777+ Error (Key_type_mismatch "Invalid P-256 public key"))
778778+ | ( Algorithm.ES384,
779779+ (Cose_key.P384_pub { x; y } | Cose_key.P384_priv { x; y; _ }) )
780780+ -> (
781781+ match
782782+ Mirage_crypto_ec.P384.Dsa.pub_of_octets ("\x04" ^ x ^ y)
783783+ with
784784+ | Ok pub ->
785785+ let hash =
786786+ Digestif.SHA384.(
787787+ digest_string sig_structure |> to_raw_string)
788788+ in
789789+ let r = String.sub t.signature 0 48 in
790790+ let s = String.sub t.signature 48 48 in
791791+ if Mirage_crypto_ec.P384.Dsa.verify ~key:pub (r, s) hash
792792+ then Ok ()
793793+ else Error Signature_mismatch
794794+ | Error _ ->
795795+ Error (Key_type_mismatch "Invalid P-384 public key"))
796796+ | ( Algorithm.ES512,
797797+ (Cose_key.P521_pub { x; y } | Cose_key.P521_priv { x; y; _ }) )
798798+ -> (
799799+ match
800800+ Mirage_crypto_ec.P521.Dsa.pub_of_octets ("\x04" ^ x ^ y)
801801+ with
802802+ | Ok pub ->
803803+ let hash =
804804+ Digestif.SHA512.(
805805+ digest_string sig_structure |> to_raw_string)
806806+ in
807807+ let r = String.sub t.signature 0 66 in
808808+ let s = String.sub t.signature 66 66 in
809809+ if Mirage_crypto_ec.P521.Dsa.verify ~key:pub (r, s) hash
810810+ then Ok ()
811811+ else Error Signature_mismatch
812812+ | Error _ ->
813813+ Error (Key_type_mismatch "Invalid P-521 public key"))
754814 | _ ->
755755- Error (Key_type_mismatch
756756- (Printf.sprintf "Key type doesn't match algorithm %s"
757757- (Algorithm.to_string alg)))
815815+ Error
816816+ (Key_type_mismatch
817817+ (Printf.sprintf "Key type doesn't match algorithm %s"
818818+ (Algorithm.to_string alg)))
758819 in
759820 verify_result
760821···763824 (* Check exp *)
764825 let check_exp () =
765826 match t.claims.exp with
766766- | Some exp ->
767767- (match Ptime.add_span exp leeway with
768768- | Some exp' when Ptime.is_later now ~than:exp' -> Error Token_expired
769769- | _ -> Ok ())
827827+ | Some exp -> (
828828+ match Ptime.add_span exp leeway with
829829+ | Some exp' when Ptime.is_later now ~than:exp' -> Error Token_expired
830830+ | _ -> Ok ())
770831 | None -> Ok ()
771832 in
772833 (* Check nbf *)
773834 let check_nbf () =
774835 match t.claims.nbf with
775775- | Some nbf ->
776776- (match Ptime.sub_span nbf leeway with
777777- | Some nbf' when Ptime.is_earlier now ~than:nbf' -> Error Token_not_yet_valid
778778- | _ -> Ok ())
836836+ | Some nbf -> (
837837+ match Ptime.sub_span nbf leeway with
838838+ | Some nbf' when Ptime.is_earlier now ~than:nbf' ->
839839+ Error Token_not_yet_valid
840840+ | _ -> Ok ())
779841 | None -> Ok ()
780842 in
781843 (* Check iss *)
782844 let check_iss () =
783845 match iss with
784784- | Some expected_iss ->
785785- (match t.claims.iss with
786786- | Some actual_iss when actual_iss = expected_iss -> Ok ()
787787- | _ -> Error Invalid_issuer)
846846+ | Some expected_iss -> (
847847+ match t.claims.iss with
848848+ | Some actual_iss when actual_iss = expected_iss -> Ok ()
849849+ | _ -> Error Invalid_issuer)
788850 | None -> Ok ()
789851 in
790852 (* Check aud *)
···797859 in
798860 match check_exp () with
799861 | Error _ as e -> e
800800- | Ok () ->
862862+ | Ok () -> (
801863 match check_nbf () with
802864 | Error _ as e -> e
803803- | Ok () ->
804804- match check_iss () with
805805- | Error _ as e -> e
806806- | Ok () -> check_aud ()
865865+ | Ok () -> (
866866+ match check_iss () with Error _ as e -> e | Ok () -> check_aud ()))
807867808868let verify_and_validate ~key ~now ?allowed_algs ?iss ?aud ?leeway t =
809869 match verify ~key ?allowed_algs t with
···813873(** Encode protected header as CBOR map *)
814874let encode_protected_header algorithm =
815875 let open Cbort.Cbor in
816816- Map [
817817- (Int (Z.of_int header_alg), Int (Z.of_int (Algorithm.to_cose_int algorithm)));
818818- ]
876876+ Map
877877+ [
878878+ ( Int (Z.of_int header_alg),
879879+ Int (Z.of_int (Algorithm.to_cose_int algorithm)) );
880880+ ]
819881 |> Cbort.encode_string Cbort.any
820882821883(** Encode COSE_Sign1 or COSE_Mac0 structure *)
822884let encode_cose_message ~cose_tag ~protected_header ~payload ~signature =
823823- Cbort.Cbor.Tag (cose_tag, Cbort.Cbor.Array [
824824- Cbort.Cbor.Bytes protected_header;
825825- Cbort.Cbor.Map []; (* unprotected header - empty *)
826826- Cbort.Cbor.Bytes payload;
827827- Cbort.Cbor.Bytes signature;
828828- ])
885885+ Cbort.Cbor.Tag
886886+ ( cose_tag,
887887+ Cbort.Cbor.Array
888888+ [
889889+ Cbort.Cbor.Bytes protected_header;
890890+ Cbort.Cbor.Map [];
891891+ (* unprotected header - empty *)
892892+ Cbort.Cbor.Bytes payload;
893893+ Cbort.Cbor.Bytes signature;
894894+ ] )
829895 |> Cbort.encode_string Cbort.any
830896831897let create ~algorithm ~claims ~key =
···833899 let protected_header = encode_protected_header algorithm in
834900835901 (* Build Sig_structure or MAC_structure *)
836836- let context_string = match algorithm with
837837- | Algorithm.HMAC_256_64 | Algorithm.HMAC_256
838838- | Algorithm.HMAC_384 | Algorithm.HMAC_512 -> "MAC0"
902902+ let context_string =
903903+ match algorithm with
904904+ | Algorithm.HMAC_256_64 | Algorithm.HMAC_256 | Algorithm.HMAC_384
905905+ | Algorithm.HMAC_512 ->
906906+ "MAC0"
839907 | _ -> "Signature1"
840908 in
841909 let payload = Claims.to_cbor claims in
842842- let to_be_signed = build_sig_structure ~context_string ~protected_header ~payload in
910910+ let to_be_signed =
911911+ build_sig_structure ~context_string ~protected_header ~payload
912912+ in
843913844914 (* Sign or MAC *)
845845- let signature_result = match algorithm, key.Cose_key.key_data with
846846- | (Algorithm.HMAC_256_64 | Algorithm.HMAC_256
847847- | Algorithm.HMAC_384 | Algorithm.HMAC_512), Cose_key.Symmetric_key { k } ->
915915+ let signature_result =
916916+ match (algorithm, key.Cose_key.key_data) with
917917+ | ( ( Algorithm.HMAC_256_64 | Algorithm.HMAC_256 | Algorithm.HMAC_384
918918+ | Algorithm.HMAC_512 ),
919919+ Cose_key.Symmetric_key { k } ) ->
848920 hmac_sign algorithm k to_be_signed
849921 | Algorithm.EdDSA, Cose_key.Ed25519_priv { d; _ } ->
850922 ed25519_sign ~priv:d to_be_signed
···854926 p384_sign ~priv:d to_be_signed
855927 | Algorithm.ES512, Cose_key.P521_priv { d; _ } ->
856928 p521_sign ~priv:d to_be_signed
857857- | _ ->
858858- Error (Key_type_mismatch "Key type doesn't match algorithm")
929929+ | _ -> Error (Key_type_mismatch "Key type doesn't match algorithm")
859930 in
860931861932 match signature_result with
862933 | Error e -> Error e
863934 | Ok signature ->
864935 (* Encode COSE_Sign1 or COSE_Mac0 structure *)
865865- let cose_tag = match algorithm with
866866- | Algorithm.HMAC_256_64 | Algorithm.HMAC_256
867867- | Algorithm.HMAC_384 | Algorithm.HMAC_512 -> cose_mac0_tag
936936+ let cose_tag =
937937+ match algorithm with
938938+ | Algorithm.HMAC_256_64 | Algorithm.HMAC_256 | Algorithm.HMAC_384
939939+ | Algorithm.HMAC_512 ->
940940+ cose_mac0_tag
868941 | _ -> cose_sign1_tag
869942 in
870870- let raw = encode_cose_message ~cose_tag ~protected_header ~payload ~signature in
871871- Ok {
872872- claims;
873873- algorithm = Some algorithm;
874874- kid = key.Cose_key.kid;
875875- protected_header;
876876- signature;
877877- raw;
878878- }
943943+ let raw =
944944+ encode_cose_message ~cose_tag ~protected_header ~payload ~signature
945945+ in
946946+ Ok
947947+ {
948948+ claims;
949949+ algorithm = Some algorithm;
950950+ kid = key.Cose_key.kid;
951951+ protected_header;
952952+ signature;
953953+ raw;
954954+ }
879955880956let encode t = t.raw
881957882958let is_expired ~now ?leeway t =
883959 match t.claims.exp with
884960 | None -> false
885885- | Some exp ->
961961+ | Some exp -> (
886962 let leeway = Option.value leeway ~default:Ptime.Span.zero in
887963 match Ptime.add_span exp leeway with
888964 | Some exp' -> Ptime.is_later now ~than:exp'
889889- | None -> true
965965+ | None -> true)
890966891967let time_to_expiry ~now t =
892968 match t.claims.exp with
893969 | None -> None
894970 | Some exp ->
895971 let diff = Ptime.diff exp now in
896896- if Ptime.Span.compare diff Ptime.Span.zero <= 0 then None
897897- else Some diff
972972+ if Ptime.Span.compare diff Ptime.Span.zero <= 0 then None else Some diff
+124-106
ocaml-jsonwt/lib/cwt.mli
···99 {{:https://datatracker.ietf.org/doc/html/rfc8392}RFC 8392}.
10101111 CWTs are the CBOR-based equivalent of JWTs, designed for constrained
1212- environments where compact binary representation is important. CWTs use
1313- COSE ({{:https://datatracker.ietf.org/doc/html/rfc9052}RFC 9052}) for
1212+ environments where compact binary representation is important. CWTs use COSE
1313+ ({{:https://datatracker.ietf.org/doc/html/rfc9052}RFC 9052}) for
1414 cryptographic protection.
15151616 {2 Quick Start}
17171818 {[
1919 (* Create claims *)
2020- let claims = Cwt.Claims.(empty
2121- |> set_iss "https://example.com"
2222- |> set_sub "user123"
2323- |> set_exp (Ptime.add_span (Ptime_clock.now ()) (Ptime.Span.of_int_s 3600) |> Option.get)
2424- |> build)
2020+ let claims =
2121+ Cwt.Claims.(
2222+ empty
2323+ |> set_iss "https://example.com"
2424+ |> set_sub "user123"
2525+ |> set_exp
2626+ (Ptime.add_span (Ptime_clock.now ()) (Ptime.Span.of_int_s 3600)
2727+ |> Option.get)
2828+ |> build)
25292630 (* Create a symmetric key *)
2727- let key = Cwt.Cose_key.symmetric (Bytes.of_string "my-secret-key-32-bytes-long!!!!!")
3131+ let key =
3232+ Cwt.Cose_key.symmetric
3333+ (Bytes.of_string "my-secret-key-32-bytes-long!!!!!")
28342935 (* Create and encode the CWT *)
3030- let cwt = Cwt.create ~algorithm:Cwt.Algorithm.HMAC_256 ~claims ~key |> Result.get_ok
3636+ let cwt =
3737+ Cwt.create ~algorithm:Cwt.Algorithm.HMAC_256 ~claims ~key
3838+ |> Result.get_ok
3939+3140 let encoded = Cwt.encode cwt
32413342 (* Parse and verify *)
···3645 ]}
37463847 {2 References}
3939- {ul
4040- {- {{:https://datatracker.ietf.org/doc/html/rfc8392}RFC 8392} - CBOR Web Token (CWT)}
4141- {- {{:https://datatracker.ietf.org/doc/html/rfc9052}RFC 9052} - CBOR Object Signing and Encryption (COSE) Structures}
4242- {- {{:https://datatracker.ietf.org/doc/html/rfc9053}RFC 9053} - CBOR Object Signing and Encryption (COSE) Algorithms}
4343- {- {{:https://datatracker.ietf.org/doc/html/rfc8949}RFC 8949} - Concise Binary Object Representation (CBOR)}} *)
4848+ - {{:https://datatracker.ietf.org/doc/html/rfc8392}RFC 8392} - CBOR Web
4949+ Token (CWT)
5050+ - {{:https://datatracker.ietf.org/doc/html/rfc9052}RFC 9052} - CBOR Object
5151+ Signing and Encryption (COSE) Structures
5252+ - {{:https://datatracker.ietf.org/doc/html/rfc9053}RFC 9053} - CBOR Object
5353+ Signing and Encryption (COSE) Algorithms
5454+ - {{:https://datatracker.ietf.org/doc/html/rfc8949}RFC 8949} - Concise
5555+ Binary Object Representation (CBOR) *)
44564557(** {1 Error Handling} *)
46584759type error =
4848- | Invalid_cbor of string
4949- (** CBOR parsing failed *)
5050- | Invalid_cose of string
5151- (** COSE structure validation failed *)
5252- | Invalid_claims of string
5353- (** Claims validation failed *)
5454- | Unsupported_algorithm of string
5555- (** Unknown COSE algorithm identifier *)
6060+ | Invalid_cbor of string (** CBOR parsing failed *)
6161+ | Invalid_cose of string (** COSE structure validation failed *)
6262+ | Invalid_claims of string (** Claims validation failed *)
6363+ | Unsupported_algorithm of string (** Unknown COSE algorithm identifier *)
5664 | Algorithm_not_allowed of string
5765 (** Algorithm rejected by allowed_algs policy *)
5858- | Signature_mismatch
5959- (** Signature/MAC verification failed *)
6666+ | Signature_mismatch (** Signature/MAC verification failed *)
6067 | Token_expired
6168 (** exp claim validation failed per
6262- {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.4}RFC 8392 Section 3.1.4} *)
6969+ {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.4}RFC
7070+ 8392 Section 3.1.4} *)
6371 | Token_not_yet_valid
6472 (** nbf claim validation failed per
6565- {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.5}RFC 8392 Section 3.1.5} *)
7373+ {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.5}RFC
7474+ 8392 Section 3.1.5} *)
6675 | Invalid_issuer
6776 (** iss claim mismatch per
6868- {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.1}RFC 8392 Section 3.1.1} *)
7777+ {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.1}RFC
7878+ 8392 Section 3.1.1} *)
6979 | Invalid_audience
7080 (** aud claim mismatch per
7171- {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.3}RFC 8392 Section 3.1.3} *)
7272- | Key_type_mismatch of string
7373- (** Key doesn't match algorithm *)
8181+ {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.3}RFC
8282+ 8392 Section 3.1.3} *)
8383+ | Key_type_mismatch of string (** Key doesn't match algorithm *)
74847585val pp_error : Format.formatter -> error -> unit
7686(** Pretty-print an error. *)
···8393 Cryptographic algorithms for COSE as specified in
8494 {{:https://datatracker.ietf.org/doc/html/rfc9053}RFC 9053}.
85958686- Each algorithm has a registered integer identifier in the IANA
8787- COSE Algorithms registry. *)
9696+ Each algorithm has a registered integer identifier in the IANA COSE
9797+ Algorithms registry. *)
88988999module Algorithm : sig
90100 type t =
9191- | ES256 (** ECDSA w/ SHA-256, COSE alg = -7 *)
9292- | ES384 (** ECDSA w/ SHA-384, COSE alg = -35 *)
9393- | ES512 (** ECDSA w/ SHA-512, COSE alg = -36 *)
9494- | EdDSA (** EdDSA (Ed25519), COSE alg = -8 *)
101101+ | ES256 (** ECDSA w/ SHA-256, COSE alg = -7 *)
102102+ | ES384 (** ECDSA w/ SHA-384, COSE alg = -35 *)
103103+ | ES512 (** ECDSA w/ SHA-512, COSE alg = -36 *)
104104+ | EdDSA (** EdDSA (Ed25519), COSE alg = -8 *)
95105 | HMAC_256_64 (** HMAC w/ SHA-256 truncated to 64 bits, COSE alg = 4 *)
9696- | HMAC_256 (** HMAC w/ SHA-256 (256 bits), COSE alg = 5 *)
9797- | HMAC_384 (** HMAC w/ SHA-384, COSE alg = 6 *)
9898- | HMAC_512 (** HMAC w/ SHA-512, COSE alg = 7 *)
106106+ | HMAC_256 (** HMAC w/ SHA-256 (256 bits), COSE alg = 5 *)
107107+ | HMAC_384 (** HMAC w/ SHA-384, COSE alg = 6 *)
108108+ | HMAC_512 (** HMAC w/ SHA-512, COSE alg = 7 *)
99109100110 val to_cose_int : t -> int
101111 (** Convert to COSE algorithm identifier (negative for signatures). *)
···112122113123(** {1 COSE Key}
114124115115- Key representation for COSE operations.
116116- See {{:https://datatracker.ietf.org/doc/html/rfc9052#section-7}RFC 9052 Section 7}
117117- and {{:https://datatracker.ietf.org/doc/html/rfc9053}RFC 9053}. *)
125125+ Key representation for COSE operations. See
126126+ {{:https://datatracker.ietf.org/doc/html/rfc9052#section-7}RFC 9052 Section
127127+ 7} and {{:https://datatracker.ietf.org/doc/html/rfc9053}RFC 9053}. *)
118128119129module Cose_key : sig
120120-121121- (** Key type per COSE Key Type registry.
122122- See {{:https://www.iana.org/assignments/cose/cose.xhtml#key-type}IANA COSE Key Types}. *)
130130+ (** Key type per COSE Key Type registry. See
131131+ {{:https://www.iana.org/assignments/cose/cose.xhtml#key-type}IANA COSE Key
132132+ Types}. *)
123133 type kty =
124124- | Okp (** Octet Key Pair (kty = 1), used for EdDSA *)
125125- | Ec2 (** Elliptic Curve with x,y coordinates (kty = 2) *)
134134+ | Okp (** Octet Key Pair (kty = 1), used for EdDSA *)
135135+ | Ec2 (** Elliptic Curve with x,y coordinates (kty = 2) *)
126136 | Symmetric (** Symmetric key (kty = 4) *)
127137138138+ type t
128139 (** A COSE key.
129140130141 Supported key types and curves:
···133144 - P-384 (NIST, crv = 2) for ES384
134145 - P-521 (NIST, crv = 3) for ES512
135146 - Ed25519 (crv = 6) for EdDSA *)
136136- type t
137147138148 (** {2 Constructors} *)
139149140150 val symmetric : string -> t
141141- (** [symmetric k] creates a symmetric COSE key from raw bytes.
142142- Used for HMAC algorithms. The key should be at least as long
143143- as the hash output (32 bytes for HMAC_256, etc.). *)
151151+ (** [symmetric k] creates a symmetric COSE key from raw bytes. Used for HMAC
152152+ algorithms. The key should be at least as long as the hash output (32
153153+ bytes for HMAC_256, etc.). *)
144154145155 val ed25519_pub : string -> t
146146- (** [ed25519_pub pub] creates an Ed25519 public key from the 32-byte
147147- public key value. *)
156156+ (** [ed25519_pub pub] creates an Ed25519 public key from the 32-byte public
157157+ key value. *)
148158149159 val ed25519_priv : pub:string -> priv:string -> t
150150- (** [ed25519_priv ~pub ~priv] creates an Ed25519 private key.
151151- [pub] is the 32-byte public key, [priv] is the 32-byte seed. *)
160160+ (** [ed25519_priv ~pub ~priv] creates an Ed25519 private key. [pub] is the
161161+ 32-byte public key, [priv] is the 32-byte seed. *)
152162153163 val p256_pub : x:string -> y:string -> t
154154- (** [p256_pub ~x ~y] creates a P-256 public key from the x and y
155155- coordinates (each 32 bytes). *)
164164+ (** [p256_pub ~x ~y] creates a P-256 public key from the x and y coordinates
165165+ (each 32 bytes). *)
156166157167 val p256_priv : x:string -> y:string -> d:string -> t
158158- (** [p256_priv ~x ~y ~d] creates a P-256 private key.
159159- [d] is the 32-byte private key value. *)
168168+ (** [p256_priv ~x ~y ~d] creates a P-256 private key. [d] is the 32-byte
169169+ private key value. *)
160170161171 val p384_pub : x:string -> y:string -> t
162162- (** [p384_pub ~x ~y] creates a P-384 public key (coordinates are 48 bytes each). *)
172172+ (** [p384_pub ~x ~y] creates a P-384 public key (coordinates are 48 bytes
173173+ each). *)
163174164175 val p384_priv : x:string -> y:string -> d:string -> t
165176 (** [p384_priv ~x ~y ~d] creates a P-384 private key. *)
166177167178 val p521_pub : x:string -> y:string -> t
168168- (** [p521_pub ~x ~y] creates a P-521 public key (coordinates are 66 bytes each). *)
179179+ (** [p521_pub ~x ~y] creates a P-521 public key (coordinates are 66 bytes
180180+ each). *)
169181170182 val p521_priv : x:string -> y:string -> d:string -> t
171183 (** [p521_priv ~x ~y ~d] creates a P-521 private key. *)
···198210199211(** {1 CWT Claims}
200212201201- CWT Claims Set using CBOR integer keys for compactness.
202202- See {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3}RFC 8392 Section 3}.
213213+ CWT Claims Set using CBOR integer keys for compactness. See
214214+ {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3}RFC 8392 Section
215215+ 3}.
203216204217 {2 Claim Key Mapping}
205218206206- | Claim | Integer Key | Type |
207207- |-------|-------------|------|
208208- | iss | 1 | text string |
209209- | sub | 2 | text string |
210210- | aud | 3 | text string |
211211- | exp | 4 | integer (NumericDate) |
212212- | nbf | 5 | integer (NumericDate) |
213213- | iat | 6 | integer (NumericDate) |
214214- | cti | 7 | byte string | *)
219219+ | Claim | Integer Key | Type | |-------|-------------|------| | iss | 1 |
220220+ text string | | sub | 2 | text string | | aud | 3 | text string | | exp | 4
221221+ | integer (NumericDate) | | nbf | 5 | integer (NumericDate) | | iat | 6 |
222222+ integer (NumericDate) | | cti | 7 | byte string | *)
215223216224module Claims : sig
217225 type t
218226219227 (** {2 Registered Claim Names}
220228221221- See {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1}RFC 8392 Section 3.1}. *)
229229+ See
230230+ {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1}RFC 8392
231231+ Section 3.1}. *)
222232223233 val iss : t -> string option
224234 (** Issuer claim (key 1) per
225225- {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.1}Section 3.1.1}. *)
235235+ {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.1}Section
236236+ 3.1.1}. *)
226237227238 val sub : t -> string option
228239 (** Subject claim (key 2) per
229229- {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.2}Section 3.1.2}. *)
240240+ {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.2}Section
241241+ 3.1.2}. *)
230242231243 val aud : t -> string list
232244 (** Audience claim (key 3) per
233233- {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.3}Section 3.1.3}.
234234- Returns empty list if not present. May be single string or array in CWT. *)
245245+ {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.3}Section
246246+ 3.1.3}. Returns empty list if not present. May be single string or array
247247+ in CWT. *)
235248236249 val exp : t -> Ptime.t option
237250 (** Expiration time claim (key 4) per
238238- {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.4}Section 3.1.4}. *)
251251+ {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.4}Section
252252+ 3.1.4}. *)
239253240254 val nbf : t -> Ptime.t option
241255 (** Not Before claim (key 5) per
242242- {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.5}Section 3.1.5}. *)
256256+ {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.5}Section
257257+ 3.1.5}. *)
243258244259 val iat : t -> Ptime.t option
245260 (** Issued At claim (key 6) per
246246- {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.6}Section 3.1.6}. *)
261261+ {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.6}Section
262262+ 3.1.6}. *)
247263248264 val cti : t -> string option
249265 (** CWT ID claim (key 7) per
250250- {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.7}Section 3.1.7}.
251251- Note: Unlike JWT's jti which is a string, CWT's cti is a byte string. *)
266266+ {{:https://datatracker.ietf.org/doc/html/rfc8392#section-3.1.7}Section
267267+ 3.1.7}. Note: Unlike JWT's jti which is a string, CWT's cti is a byte
268268+ string. *)
252269253270 (** {2 Custom Claims}
254271255272 CWT supports both integer and text string keys for custom claims. *)
256273257274 val get_int_key : int -> t -> Cbort.Cbor.t option
258258- (** [get_int_key key claims] returns the CBOR value of custom claim
259259- with integer key [key]. *)
275275+ (** [get_int_key key claims] returns the CBOR value of custom claim with
276276+ integer key [key]. *)
260277261278 val get_string_key : string -> t -> Cbort.Cbor.t option
262262- (** [get_string_key key claims] returns the CBOR value of custom claim
263263- with string key [key]. *)
279279+ (** [get_string_key key claims] returns the CBOR value of custom claim with
280280+ string key [key]. *)
264281265282 (** {2 Construction} *)
266283···314331(** {1 CWT Token} *)
315332316333type t
317317-(** A parsed CWT token (COSE_Sign1 or COSE_Mac0 structure).
318318- Note: COSE_Encrypt0 is not currently supported. *)
334334+(** A parsed CWT token (COSE_Sign1 or COSE_Mac0 structure). Note: COSE_Encrypt0
335335+ is not currently supported. *)
319336320337(** {2 Parsing}
321338322322- Parse CWT from CBOR bytes. The CWT may be tagged (with COSE tag)
323323- or untagged per {{:https://datatracker.ietf.org/doc/html/rfc8392#section-2}RFC 8392 Section 2}. *)
339339+ Parse CWT from CBOR bytes. The CWT may be tagged (with COSE tag) or untagged
340340+ per
341341+ {{:https://datatracker.ietf.org/doc/html/rfc8392#section-2}RFC 8392 Section
342342+ 2}. *)
324343325344val parse : string -> (t, error) result
326345(** [parse cwt_bytes] parses a CWT from CBOR bytes.
327346328328- This parses the COSE structure and extracts the claims, but does NOT
329329- verify the signature/MAC. Use {!verify} to validate cryptographic
330330- protection after parsing. *)
347347+ This parses the COSE structure and extracts the claims, but does NOT verify
348348+ the signature/MAC. Use {!verify} to validate cryptographic protection after
349349+ parsing. *)
331350332351(** {2 Accessors} *)
333352···348367 Verify cryptographic protection and validate claims. *)
349368350369val verify :
351351- key:Cose_key.t ->
352352- ?allowed_algs:Algorithm.t list ->
353353- t ->
354354- (unit, error) result
370370+ key:Cose_key.t -> ?allowed_algs:Algorithm.t list -> t -> (unit, error) result
355371(** [verify ~key ?allowed_algs t] verifies the COSE signature or MAC.
356372357373 @param key The key to verify with (must match algorithm)
···380396 ?leeway:Ptime.Span.t ->
381397 t ->
382398 (unit, error) result
383383-(** [verify_and_validate ~key ~now ...] verifies signature and validates claims. *)
399399+(** [verify_and_validate ~key ~now ...] verifies signature and validates claims.
400400+*)
384401385402(** {2 Creation}
386403···393410 (t, error) result
394411(** [create ~algorithm ~claims ~key] creates and signs a new CWT.
395412396396- Creates a COSE_Sign1 structure for signature algorithms (ES256, ES384, ES512, EdDSA)
397397- or COSE_Mac0 for MAC algorithms (HMAC_256, HMAC_384, HMAC_512).
413413+ Creates a COSE_Sign1 structure for signature algorithms (ES256, ES384,
414414+ ES512, EdDSA) or COSE_Mac0 for MAC algorithms (HMAC_256, HMAC_384,
415415+ HMAC_512).
398416399417 The [key] must be appropriate for the algorithm:
400418 - HMAC algorithms: symmetric key
···404422 - EdDSA: Ed25519 private key *)
405423406424val encode : t -> string
407407-(** [encode t] returns the CBOR serialization of the CWT.
408408- The result is a tagged COSE structure (COSE_Sign1 or COSE_Mac0). *)
425425+(** [encode t] returns the CBOR serialization of the CWT. The result is a tagged
426426+ COSE structure (COSE_Sign1 or COSE_Mac0). *)
409427410428(** {1 Utilities} *)
411429412430val is_expired : now:Ptime.t -> ?leeway:Ptime.Span.t -> t -> bool
413413-(** [is_expired ~now ?leeway t] checks if the token has expired.
414414- Returns false if no exp claim present. *)
431431+(** [is_expired ~now ?leeway t] checks if the token has expired. Returns false
432432+ if no exp claim present. *)
415433416434val time_to_expiry : now:Ptime.t -> t -> Ptime.Span.t option
417417-(** [time_to_expiry ~now t] returns time until expiration, or [None] if
418418- no expiration claim or already expired. *)
435435+(** [time_to_expiry ~now t] returns time until expiration, or [None] if no
436436+ expiration claim or already expired. *)
+299-239
ocaml-jsonwt/lib/jsonwt.ml
···3939 | Unsecured_not_allowed -> Format.fprintf fmt "Unsecured JWT not allowed"
4040 | Nesting_too_deep -> Format.fprintf fmt "Nested JWT too deep"
41414242-let error_to_string e =
4343- Format.asprintf "%a" pp_error e
4242+let error_to_string e = Format.asprintf "%a" pp_error e
44434544(* Base64url encoding/decoding per RFC 7515 Appendix C *)
4645let base64url_encode s =
···6463 let scheme = String.sub s 0 i in
6564 (* Check scheme is alphanumeric with +.- allowed after first char *)
6665 let valid_scheme =
6767- String.length scheme > 0 &&
6868- (match scheme.[0] with 'a'..'z' | 'A'..'Z' -> true | _ -> false) &&
6969- String.for_all (fun c ->
7070- match c with
7171- | 'a'..'z' | 'A'..'Z' | '0'..'9' | '+' | '-' | '.' -> true
7272- | _ -> false
7373- ) scheme
6666+ String.length scheme > 0
6767+ && (match scheme.[0] with
6868+ | 'a' .. 'z' | 'A' .. 'Z' -> true
6969+ | _ -> false)
7070+ && String.for_all
7171+ (fun c ->
7272+ match c with
7373+ | 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '+' | '-' | '.' ->
7474+ true
7575+ | _ -> false)
7676+ scheme
7477 in
7578 if valid_scheme then Ok s
7679 else Error (Invalid_uri (Printf.sprintf "Invalid URI scheme in: %s" s))
7780 | _ -> Error (Invalid_uri (Printf.sprintf "Invalid URI: %s" s))
7878- else
7979- Ok s
8181+ else Ok s
80828183(* Algorithm module *)
8284module Algorithm = struct
···120122 | "EdDSA" -> Ok EdDSA
121123 | s -> Error (Unsupported_algorithm s)
122124123123- let all = [ HS256; HS384; HS512; RS256; RS384; RS512; ES256; ES384; ES512; EdDSA ]
125125+ let all =
126126+ [ HS256; HS384; HS512; RS256; RS384; RS512; ES256; ES384; ES512; EdDSA ]
127127+124128 let all_with_none = None :: all
125129end
126130127131(* JWK module *)
128132module Jwk = struct
129133 type kty = Oct | Rsa | Ec | Okp
130130-131134 type crv = P256 | P384 | P521 | Ed25519
132135133136 type key_data =
···164167 { key_data = Ed25519_pub { x }; kid = None; alg = Some Algorithm.EdDSA }
165168166169 let ed25519_priv ~pub ~priv =
167167- { key_data = Ed25519_priv { x = pub; d = priv }; kid = None; alg = Some Algorithm.EdDSA }
170170+ {
171171+ key_data = Ed25519_priv { x = pub; d = priv };
172172+ kid = None;
173173+ alg = Some Algorithm.EdDSA;
174174+ }
168175169176 let p256_pub ~x ~y =
170177 { key_data = P256_pub { x; y }; kid = None; alg = Some Algorithm.ES256 }
···188195 { key_data = Rsa_pub { n; e }; kid = None; alg = Some Algorithm.RS256 }
189196190197 let rsa_priv ~n ~e ~d ~p ~q ~dp ~dq ~qi =
191191- { key_data = Rsa_priv { n; e; d; p; q; dp; dq; qi }; kid = None; alg = Some Algorithm.RS256 }
198198+ {
199199+ key_data = Rsa_priv { n; e; d; p; q; dp; dq; qi };
200200+ kid = None;
201201+ alg = Some Algorithm.RS256;
202202+ }
192203193204 let kty t =
194205 match t.key_data with
195206 | Symmetric _ -> Oct
196207 | Ed25519_pub _ | Ed25519_priv _ -> Okp
197197- | P256_pub _ | P256_priv _ | P384_pub _ | P384_priv _ | P521_pub _ | P521_priv _ -> Ec
208208+ | P256_pub _ | P256_priv _ | P384_pub _ | P384_priv _ | P521_pub _
209209+ | P521_priv _ ->
210210+ Ec
198211 | Rsa_pub _ | Rsa_priv _ -> Rsa
199212200213 let kid t = t.kid
201214 let alg t = t.alg
202202-203215 let with_kid id t = { t with kid = Some id }
204216 let with_alg a t = { t with alg = Some a }
205217206218 (* Helper to extract string from Jsont.json object members *)
207219 let get_json_string members name =
208208- List.find_map (fun ((n, _), v) ->
209209- if n = name then
210210- match v with
211211- | Jsont.String (s, _) -> Some s
212212- | _ -> None
213213- else None
214214- ) members
220220+ List.find_map
221221+ (fun ((n, _), v) ->
222222+ if n = name then
223223+ match v with Jsont.String (s, _) -> Some s | _ -> None
224224+ else None)
225225+ members
215226216227 let get_json_string_req members name =
217228 match get_json_string members name with
218229 | Some s -> Ok s
219219- | None -> Error (Invalid_json (Printf.sprintf "missing required field: %s" name))
230230+ | None ->
231231+ Error (Invalid_json (Printf.sprintf "missing required field: %s" name))
220232221233 let of_json s =
222234 (* Parse the JSON to determine key type first *)
223235 match Jsont_bytesrw.decode_string Jsont.json s with
224236 | Error e -> Error (Invalid_json e)
225237 | Ok (Jsont.Null _) -> Error (Invalid_json "null is not a valid JWK")
226226- | Ok (Jsont.Object (members, _)) ->
238238+ | Ok (Jsont.Object (members, _)) -> (
227239 let ( let* ) = Result.bind in
228240 let* kty_s = get_json_string_req members "kty" in
229241 let kid = get_json_string members "kid" in
230242 let alg_opt =
231243 match get_json_string members "alg" with
232244 | None -> Ok None
233233- | Some s ->
245245+ | Some s -> (
234246 match Algorithm.of_string s with
235247 | Ok a -> Ok (Some a)
236236- | Error _ -> Ok None (* ignore unknown alg in JWK *)
248248+ | Error _ -> Ok None (* ignore unknown alg in JWK *))
237249 in
238250 let* alg = alg_opt in
239239- (match kty_s with
251251+ match kty_s with
240252 | "oct" ->
241253 let* k_b64 = get_json_string_req members "k" in
242254 let* k = base64url_decode k_b64 in
243255 Ok { key_data = Symmetric { k }; kid; alg }
244244- | "OKP" ->
256256+ | "OKP" -> (
245257 let* crv = get_json_string_req members "crv" in
246258 if crv <> "Ed25519" then
247259 Error (Invalid_json (Printf.sprintf "unsupported curve: %s" crv))
248260 else
249261 let* x_b64 = get_json_string_req members "x" in
250262 let* x = base64url_decode x_b64 in
251251- (match get_json_string members "d" with
252252- | None -> Ok { key_data = Ed25519_pub { x }; kid; alg }
253253- | Some d_b64 ->
254254- let* d = base64url_decode d_b64 in
255255- Ok { key_data = Ed25519_priv { x; d }; kid; alg })
256256- | "EC" ->
263263+ match get_json_string members "d" with
264264+ | None -> Ok { key_data = Ed25519_pub { x }; kid; alg }
265265+ | Some d_b64 ->
266266+ let* d = base64url_decode d_b64 in
267267+ Ok { key_data = Ed25519_priv { x; d }; kid; alg })
268268+ | "EC" -> (
257269 let* crv = get_json_string_req members "crv" in
258270 let* x_b64 = get_json_string_req members "x" in
259271 let* y_b64 = get_json_string_req members "y" in
···264276 let* d_b64 = get_json_string_req members "d" in
265277 base64url_decode d_b64
266278 in
267267- (match crv with
268268- | "P-256" ->
269269- if has_d then
270270- let* d = get_d () in
271271- Ok { key_data = P256_priv { x; y; d }; kid; alg }
272272- else
273273- Ok { key_data = P256_pub { x; y }; kid; alg }
274274- | "P-384" ->
275275- if has_d then
276276- let* d = get_d () in
277277- Ok { key_data = P384_priv { x; y; d }; kid; alg }
278278- else
279279- Ok { key_data = P384_pub { x; y }; kid; alg }
280280- | "P-521" ->
281281- if has_d then
282282- let* d = get_d () in
283283- Ok { key_data = P521_priv { x; y; d }; kid; alg }
284284- else
285285- Ok { key_data = P521_pub { x; y }; kid; alg }
286286- | _ -> Error (Invalid_json (Printf.sprintf "unsupported curve: %s" crv)))
287287- | "RSA" ->
279279+ match crv with
280280+ | "P-256" ->
281281+ if has_d then
282282+ let* d = get_d () in
283283+ Ok { key_data = P256_priv { x; y; d }; kid; alg }
284284+ else Ok { key_data = P256_pub { x; y }; kid; alg }
285285+ | "P-384" ->
286286+ if has_d then
287287+ let* d = get_d () in
288288+ Ok { key_data = P384_priv { x; y; d }; kid; alg }
289289+ else Ok { key_data = P384_pub { x; y }; kid; alg }
290290+ | "P-521" ->
291291+ if has_d then
292292+ let* d = get_d () in
293293+ Ok { key_data = P521_priv { x; y; d }; kid; alg }
294294+ else Ok { key_data = P521_pub { x; y }; kid; alg }
295295+ | _ ->
296296+ Error
297297+ (Invalid_json (Printf.sprintf "unsupported curve: %s" crv)))
298298+ | "RSA" -> (
288299 let* n_b64 = get_json_string_req members "n" in
289300 let* e_b64 = get_json_string_req members "e" in
290301 let* n = base64url_decode n_b64 in
291302 let* e = base64url_decode e_b64 in
292292- (match get_json_string members "d" with
293293- | None -> Ok { key_data = Rsa_pub { n; e }; kid; alg }
294294- | Some d_b64 ->
295295- let* d = base64url_decode d_b64 in
296296- let* p_b64 = get_json_string_req members "p" in
297297- let* q_b64 = get_json_string_req members "q" in
298298- let* dp_b64 = get_json_string_req members "dp" in
299299- let* dq_b64 = get_json_string_req members "dq" in
300300- let* qi_b64 = get_json_string_req members "qi" in
301301- let* p = base64url_decode p_b64 in
302302- let* q = base64url_decode q_b64 in
303303- let* dp = base64url_decode dp_b64 in
304304- let* dq = base64url_decode dq_b64 in
305305- let* qi = base64url_decode qi_b64 in
306306- Ok { key_data = Rsa_priv { n; e; d; p; q; dp; dq; qi }; kid; alg })
307307- | _ -> Error (Invalid_json (Printf.sprintf "unsupported kty: %s" kty_s)))
303303+ match get_json_string members "d" with
304304+ | None -> Ok { key_data = Rsa_pub { n; e }; kid; alg }
305305+ | Some d_b64 ->
306306+ let* d = base64url_decode d_b64 in
307307+ let* p_b64 = get_json_string_req members "p" in
308308+ let* q_b64 = get_json_string_req members "q" in
309309+ let* dp_b64 = get_json_string_req members "dp" in
310310+ let* dq_b64 = get_json_string_req members "dq" in
311311+ let* qi_b64 = get_json_string_req members "qi" in
312312+ let* p = base64url_decode p_b64 in
313313+ let* q = base64url_decode q_b64 in
314314+ let* dp = base64url_decode dp_b64 in
315315+ let* dq = base64url_decode dq_b64 in
316316+ let* qi = base64url_decode qi_b64 in
317317+ Ok
318318+ {
319319+ key_data = Rsa_priv { n; e; d; p; q; dp; dq; qi };
320320+ kid;
321321+ alg;
322322+ })
323323+ | _ -> Error (Invalid_json (Printf.sprintf "unsupported kty: %s" kty_s))
324324+ )
308325 | Ok _ -> Error (Invalid_json "JWK must be a JSON object")
309326310327 (* Helper to create JSON members *)
···320337 in
321338 let members = [] in
322339 let members = add_opt "kid" t.kid members in
323323- let members = add_opt "alg" (Option.map Algorithm.to_string t.alg) members in
340340+ let members =
341341+ add_opt "alg" (Option.map Algorithm.to_string t.alg) members
342342+ in
324343 let members =
325344 match t.key_data with
326345 | Symmetric { k } ->
327327- json_mem "kty" (json_string "oct") ::
328328- json_mem "k" (json_string (base64url_encode k)) :: members
346346+ json_mem "kty" (json_string "oct")
347347+ :: json_mem "k" (json_string (base64url_encode k))
348348+ :: members
329349 | Ed25519_pub { x } ->
330330- json_mem "kty" (json_string "OKP") ::
331331- json_mem "crv" (json_string "Ed25519") ::
332332- json_mem "x" (json_string (base64url_encode x)) :: members
350350+ json_mem "kty" (json_string "OKP")
351351+ :: json_mem "crv" (json_string "Ed25519")
352352+ :: json_mem "x" (json_string (base64url_encode x))
353353+ :: members
333354 | Ed25519_priv { x; d } ->
334334- json_mem "kty" (json_string "OKP") ::
335335- json_mem "crv" (json_string "Ed25519") ::
336336- json_mem "x" (json_string (base64url_encode x)) ::
337337- json_mem "d" (json_string (base64url_encode d)) :: members
355355+ json_mem "kty" (json_string "OKP")
356356+ :: json_mem "crv" (json_string "Ed25519")
357357+ :: json_mem "x" (json_string (base64url_encode x))
358358+ :: json_mem "d" (json_string (base64url_encode d))
359359+ :: members
338360 | P256_pub { x; y } ->
339339- json_mem "kty" (json_string "EC") ::
340340- json_mem "crv" (json_string "P-256") ::
341341- json_mem "x" (json_string (base64url_encode x)) ::
342342- json_mem "y" (json_string (base64url_encode y)) :: members
361361+ json_mem "kty" (json_string "EC")
362362+ :: json_mem "crv" (json_string "P-256")
363363+ :: json_mem "x" (json_string (base64url_encode x))
364364+ :: json_mem "y" (json_string (base64url_encode y))
365365+ :: members
343366 | P256_priv { x; y; d } ->
344344- json_mem "kty" (json_string "EC") ::
345345- json_mem "crv" (json_string "P-256") ::
346346- json_mem "x" (json_string (base64url_encode x)) ::
347347- json_mem "y" (json_string (base64url_encode y)) ::
348348- json_mem "d" (json_string (base64url_encode d)) :: members
367367+ json_mem "kty" (json_string "EC")
368368+ :: json_mem "crv" (json_string "P-256")
369369+ :: json_mem "x" (json_string (base64url_encode x))
370370+ :: json_mem "y" (json_string (base64url_encode y))
371371+ :: json_mem "d" (json_string (base64url_encode d))
372372+ :: members
349373 | P384_pub { x; y } ->
350350- json_mem "kty" (json_string "EC") ::
351351- json_mem "crv" (json_string "P-384") ::
352352- json_mem "x" (json_string (base64url_encode x)) ::
353353- json_mem "y" (json_string (base64url_encode y)) :: members
374374+ json_mem "kty" (json_string "EC")
375375+ :: json_mem "crv" (json_string "P-384")
376376+ :: json_mem "x" (json_string (base64url_encode x))
377377+ :: json_mem "y" (json_string (base64url_encode y))
378378+ :: members
354379 | P384_priv { x; y; d } ->
355355- json_mem "kty" (json_string "EC") ::
356356- json_mem "crv" (json_string "P-384") ::
357357- json_mem "x" (json_string (base64url_encode x)) ::
358358- json_mem "y" (json_string (base64url_encode y)) ::
359359- json_mem "d" (json_string (base64url_encode d)) :: members
380380+ json_mem "kty" (json_string "EC")
381381+ :: json_mem "crv" (json_string "P-384")
382382+ :: json_mem "x" (json_string (base64url_encode x))
383383+ :: json_mem "y" (json_string (base64url_encode y))
384384+ :: json_mem "d" (json_string (base64url_encode d))
385385+ :: members
360386 | P521_pub { x; y } ->
361361- json_mem "kty" (json_string "EC") ::
362362- json_mem "crv" (json_string "P-521") ::
363363- json_mem "x" (json_string (base64url_encode x)) ::
364364- json_mem "y" (json_string (base64url_encode y)) :: members
387387+ json_mem "kty" (json_string "EC")
388388+ :: json_mem "crv" (json_string "P-521")
389389+ :: json_mem "x" (json_string (base64url_encode x))
390390+ :: json_mem "y" (json_string (base64url_encode y))
391391+ :: members
365392 | P521_priv { x; y; d } ->
366366- json_mem "kty" (json_string "EC") ::
367367- json_mem "crv" (json_string "P-521") ::
368368- json_mem "x" (json_string (base64url_encode x)) ::
369369- json_mem "y" (json_string (base64url_encode y)) ::
370370- json_mem "d" (json_string (base64url_encode d)) :: members
393393+ json_mem "kty" (json_string "EC")
394394+ :: json_mem "crv" (json_string "P-521")
395395+ :: json_mem "x" (json_string (base64url_encode x))
396396+ :: json_mem "y" (json_string (base64url_encode y))
397397+ :: json_mem "d" (json_string (base64url_encode d))
398398+ :: members
371399 | Rsa_pub { n; e } ->
372372- json_mem "kty" (json_string "RSA") ::
373373- json_mem "n" (json_string (base64url_encode n)) ::
374374- json_mem "e" (json_string (base64url_encode e)) :: members
400400+ json_mem "kty" (json_string "RSA")
401401+ :: json_mem "n" (json_string (base64url_encode n))
402402+ :: json_mem "e" (json_string (base64url_encode e))
403403+ :: members
375404 | Rsa_priv { n; e; d; p; q; dp; dq; qi } ->
376376- json_mem "kty" (json_string "RSA") ::
377377- json_mem "n" (json_string (base64url_encode n)) ::
378378- json_mem "e" (json_string (base64url_encode e)) ::
379379- json_mem "d" (json_string (base64url_encode d)) ::
380380- json_mem "p" (json_string (base64url_encode p)) ::
381381- json_mem "q" (json_string (base64url_encode q)) ::
382382- json_mem "dp" (json_string (base64url_encode dp)) ::
383383- json_mem "dq" (json_string (base64url_encode dq)) ::
384384- json_mem "qi" (json_string (base64url_encode qi)) :: members
405405+ json_mem "kty" (json_string "RSA")
406406+ :: json_mem "n" (json_string (base64url_encode n))
407407+ :: json_mem "e" (json_string (base64url_encode e))
408408+ :: json_mem "d" (json_string (base64url_encode d))
409409+ :: json_mem "p" (json_string (base64url_encode p))
410410+ :: json_mem "q" (json_string (base64url_encode q))
411411+ :: json_mem "dp" (json_string (base64url_encode dp))
412412+ :: json_mem "dq" (json_string (base64url_encode dq))
413413+ :: json_mem "qi" (json_string (base64url_encode qi))
414414+ :: members
385415 in
386386- match Jsont_bytesrw.encode_string Jsont.json (Jsont.Object (members, meta)) with
416416+ match
417417+ Jsont_bytesrw.encode_string Jsont.json (Jsont.Object (members, meta))
418418+ with
387419 | Ok s -> s
388420 | Error _ -> "{}" (* Should not happen *)
389421end
···406438407439 (* Helper to extract string from Jsont.json object members *)
408440 let get_json_string members name =
409409- List.find_map (fun ((n, _), v) ->
410410- if n = name then
411411- match v with
412412- | Jsont.String (s, _) -> Some s
413413- | _ -> None
414414- else None
415415- ) members
441441+ List.find_map
442442+ (fun ((n, _), v) ->
443443+ if n = name then
444444+ match v with Jsont.String (s, _) -> Some s | _ -> None
445445+ else None)
446446+ members
416447417448 let of_json s =
418449 match Jsont_bytesrw.decode_string Jsont.json s with
419450 | Error e -> Error (Invalid_json e)
420451 | Ok (Jsont.Null _) -> Error (Invalid_header "null is not a valid header")
421421- | Ok (Jsont.Object (members, _)) ->
452452+ | Ok (Jsont.Object (members, _)) -> (
422453 let ( let* ) = Result.bind in
423454 let alg_s = get_json_string members "alg" in
424424- (match alg_s with
455455+ match alg_s with
425456 | None -> Error (Invalid_header "missing required 'alg' field")
426457 | Some alg_str ->
427458 let* alg = Algorithm.of_string alg_str in
···436467 let json_mem name value = ((name, meta), value)
437468438469 let to_json h =
439439- let members = [ json_mem "alg" (json_string (Algorithm.to_string h.alg)) ] in
470470+ let members =
471471+ [ json_mem "alg" (json_string (Algorithm.to_string h.alg)) ]
472472+ in
440473 let add_opt name v_opt acc =
441474 match v_opt with
442475 | None -> acc
···445478 let members = add_opt "typ" h.typ members in
446479 let members = add_opt "kid" h.kid members in
447480 let members = add_opt "cty" h.cty members in
448448- match Jsont_bytesrw.encode_string Jsont.json (Jsont.Object (List.rev members, meta)) with
481481+ match
482482+ Jsont_bytesrw.encode_string Jsont.json
483483+ (Jsont.Object (List.rev members, meta))
484484+ with
449485 | Ok s -> s
450486 | Error _ -> "{}"
451487end
···470506 let nbf t = t.nbf
471507 let iat t = t.iat
472508 let jti t = t.jti
473473-474509 let get name t = List.assoc_opt name t.custom
475510476511 let get_string name t =
477477- match get name t with
478478- | Some (Jsont.String (s, _)) -> Some s
479479- | _ -> None
512512+ match get name t with Some (Jsont.String (s, _)) -> Some s | _ -> None
480513481514 let get_int name t =
482515 match get name t with
483483- | Some (Jsont.Number (n, _)) -> (try Some (int_of_float n) with _ -> None)
516516+ | Some (Jsont.Number (n, _)) -> (
517517+ try Some (int_of_float n) with _ -> None)
484518 | _ -> None
485519486520 let get_bool name t =
487487- match get name t with
488488- | Some (Jsont.Bool (b, _)) -> Some b
489489- | _ -> None
521521+ match get name t with Some (Jsont.Bool (b, _)) -> Some b | _ -> None
490522491523 let meta = Jsont.Meta.none
492524 let json_string s = Jsont.String (s, meta)
···496528497529 type builder = t
498530499499- let empty = {
500500- iss = None;
501501- sub = None;
502502- aud = [];
503503- exp = None;
504504- nbf = None;
505505- iat = None;
506506- jti = None;
507507- custom = [];
508508- }
531531+ let empty =
532532+ {
533533+ iss = None;
534534+ sub = None;
535535+ aud = [];
536536+ exp = None;
537537+ nbf = None;
538538+ iat = None;
539539+ jti = None;
540540+ custom = [];
541541+ }
509542510543 let set_iss v t = { t with iss = Some v }
511544 let set_sub v t = { t with sub = Some v }
···524557 let span = Ptime.Span.of_float_s n in
525558 Option.bind span (fun s -> Ptime.of_span s)
526559527527- let numeric_date_of_ptime t =
528528- Ptime.to_span t |> Ptime.Span.to_float_s
560560+ let numeric_date_of_ptime t = Ptime.to_span t |> Ptime.Span.to_float_s
529561530562 (* Helper to extract values from Jsont.json object members *)
531563 let get_json_string members name =
532532- List.find_map (fun ((n, _), v) ->
533533- if n = name then
534534- match v with
535535- | Jsont.String (s, _) -> Some s
536536- | _ -> None
537537- else None
538538- ) members
564564+ List.find_map
565565+ (fun ((n, _), v) ->
566566+ if n = name then
567567+ match v with Jsont.String (s, _) -> Some s | _ -> None
568568+ else None)
569569+ members
539570540571 let get_json_number members name =
541541- List.find_map (fun ((n, _), v) ->
542542- if n = name then
543543- match v with
544544- | Jsont.Number (n, _) -> Some n
545545- | _ -> None
546546- else None
547547- ) members
572572+ List.find_map
573573+ (fun ((n, _), v) ->
574574+ if n = name then
575575+ match v with Jsont.Number (n, _) -> Some n | _ -> None
576576+ else None)
577577+ members
548578549579 let get_json_aud members =
550550- List.find_map (fun ((n, _), v) ->
551551- if n = "aud" then
552552- match v with
553553- | Jsont.String (s, _) -> Some [ s ]
554554- | Jsont.Array (arr, _) ->
555555- Some (List.filter_map (function
556556- | Jsont.String (s, _) -> Some s
557557- | _ -> None
558558- ) arr)
559559- | _ -> None
560560- else None
561561- ) members |> Option.value ~default:[]
580580+ List.find_map
581581+ (fun ((n, _), v) ->
582582+ if n = "aud" then
583583+ match v with
584584+ | Jsont.String (s, _) -> Some [ s ]
585585+ | Jsont.Array (arr, _) ->
586586+ Some
587587+ (List.filter_map
588588+ (function Jsont.String (s, _) -> Some s | _ -> None)
589589+ arr)
590590+ | _ -> None
591591+ else None)
592592+ members
593593+ |> Option.value ~default:[]
562594563595 let of_json ?(strict = true) s =
564596 match Jsont_bytesrw.decode_string Jsont.json s with
565597 | Error e -> Error (Invalid_json e)
566566- | Ok (Jsont.Null _) -> Error (Invalid_claims "null is not a valid claims set")
598598+ | Ok (Jsont.Null _) ->
599599+ Error (Invalid_claims "null is not a valid claims set")
567600 | Ok (Jsont.Object (members, _)) ->
568601 let ( let* ) = Result.bind in
569602 (* Check for duplicates in strict mode *)
···594627 let* _ = validate_string_or_uri s in
595628 Ok (Some s)
596629 in
597597- let exp = Option.bind (get_json_number members "exp") ptime_of_numeric_date in
598598- let nbf = Option.bind (get_json_number members "nbf") ptime_of_numeric_date in
599599- let iat = Option.bind (get_json_number members "iat") ptime_of_numeric_date in
630630+ let exp =
631631+ Option.bind (get_json_number members "exp") ptime_of_numeric_date
632632+ in
633633+ let nbf =
634634+ Option.bind (get_json_number members "nbf") ptime_of_numeric_date
635635+ in
636636+ let iat =
637637+ Option.bind (get_json_number members "iat") ptime_of_numeric_date
638638+ in
600639 let jti = get_json_string members "jti" in
601640 let aud = get_json_aud members in
602641 (* Collect custom claims (everything not registered) *)
603642 let registered = [ "iss"; "sub"; "aud"; "exp"; "nbf"; "iat"; "jti" ] in
604643 let custom =
605605- List.filter_map (fun ((n, _), v) ->
606606- if List.mem n registered then None
607607- else Some (n, v)
608608- ) members
644644+ List.filter_map
645645+ (fun ((n, _), v) ->
646646+ if List.mem n registered then None else Some (n, v))
647647+ members
609648 in
610649 Ok { iss; sub; aud; exp; nbf; iat; jti; custom }
611650 | Ok _ -> Error (Invalid_claims "claims must be a JSON object")
···637676 let members = add_time "iat" t.iat members in
638677 let members = add_string "jti" t.jti members in
639678 let members =
640640- List.fold_left (fun acc (name, value) ->
641641- json_mem name value :: acc
642642- ) members t.custom
679679+ List.fold_left
680680+ (fun acc (name, value) -> json_mem name value :: acc)
681681+ members t.custom
643682 in
644644- match Jsont_bytesrw.encode_string Jsont.json (Jsont.Object (List.rev members, meta)) with
683683+ match
684684+ Jsont_bytesrw.encode_string Jsont.json
685685+ (Jsont.Object (List.rev members, meta))
686686+ with
645687 | Ok s -> s
646688 | Error _ -> "{}"
647689end
···658700let claims t = t.claims
659701let signature t = t.signature
660702let raw t = t.raw
661661-662703let is_nested t = Header.is_nested t.header
663704664705(* Parsing *)
···688729let parse_nested ?(strict = true) ?(max_depth = 5) token =
689730 let ( let* ) = Result.bind in
690731 let rec loop depth acc tok =
691691- if depth > max_depth then
692692- Error Nesting_too_deep
732732+ if depth > max_depth then Error Nesting_too_deep
693733 else
694734 let* jwt = parse ~strict tok in
695735 let acc = jwt :: acc in
···700740 let* inner_token = base64url_decode payload_b64 in
701741 loop (depth + 1) acc inner_token
702742 | _ -> Ok (List.rev acc)
703703- else
704704- Ok (List.rev acc)
743743+ else Ok (List.rev acc)
705744 in
706745 loop 1 [] token
707746···710749 let hmac_sha256 ~key data =
711750 let key = Cstruct.of_string key in
712751 let data = Cstruct.of_string data in
713713- Digestif.SHA256.hmac_string ~key:(Cstruct.to_string key) (Cstruct.to_string data)
752752+ Digestif.SHA256.hmac_string ~key:(Cstruct.to_string key)
753753+ (Cstruct.to_string data)
714754 |> Digestif.SHA256.to_raw_string
715755716756 let hmac_sha384 ~key data =
717757 let key = Cstruct.of_string key in
718758 let data = Cstruct.of_string data in
719719- Digestif.SHA384.hmac_string ~key:(Cstruct.to_string key) (Cstruct.to_string data)
759759+ Digestif.SHA384.hmac_string ~key:(Cstruct.to_string key)
760760+ (Cstruct.to_string data)
720761 |> Digestif.SHA384.to_raw_string
721762722763 let hmac_sha512 ~key data =
723764 let key = Cstruct.of_string key in
724765 let data = Cstruct.of_string data in
725725- Digestif.SHA512.hmac_string ~key:(Cstruct.to_string key) (Cstruct.to_string data)
766766+ Digestif.SHA512.hmac_string ~key:(Cstruct.to_string key)
767767+ (Cstruct.to_string data)
726768 |> Digestif.SHA512.to_raw_string
727769728770 (* EdDSA signing using mirage-crypto-ec *)
···737779 match Mirage_crypto_ec.Ed25519.pub_of_octets pub with
738780 | Error _ -> Error (Key_type_mismatch "Invalid Ed25519 public key")
739781 | Ok pub ->
740740- let valid = Mirage_crypto_ec.Ed25519.verify ~key:pub signature ~msg:data in
782782+ let valid =
783783+ Mirage_crypto_ec.Ed25519.verify ~key:pub signature ~msg:data
784784+ in
741785 if valid then Ok () else Error Signature_mismatch
742786743787 (* P-256 ECDSA *)
···745789 match Mirage_crypto_ec.P256.Dsa.priv_of_octets priv with
746790 | Error _ -> Error (Key_type_mismatch "Invalid P-256 private key")
747791 | Ok priv ->
748748- let hash = Digestif.SHA256.digest_string data |> Digestif.SHA256.to_raw_string in
749749- let (r, s) = Mirage_crypto_ec.P256.Dsa.sign ~key:priv hash in
792792+ let hash =
793793+ Digestif.SHA256.digest_string data |> Digestif.SHA256.to_raw_string
794794+ in
795795+ let r, s = Mirage_crypto_ec.P256.Dsa.sign ~key:priv hash in
750796 (* JWS uses raw R||S format, each 32 bytes for P-256 *)
751797 (* Pad to 32 bytes each *)
752798 let pad32 s =
···757803 Ok (pad32 r ^ pad32 s)
758804759805 let p256_verify ~pub ~signature data =
760760- if String.length signature <> 64 then
761761- Error Signature_mismatch
806806+ if String.length signature <> 64 then Error Signature_mismatch
762807 else
763808 let r = String.sub signature 0 32 in
764809 let s = String.sub signature 32 32 in
765810 match Mirage_crypto_ec.P256.Dsa.pub_of_octets pub with
766811 | Error _ -> Error (Key_type_mismatch "Invalid P-256 public key")
767812 | Ok pub ->
768768- let hash = Digestif.SHA256.digest_string data |> Digestif.SHA256.to_raw_string in
813813+ let hash =
814814+ Digestif.SHA256.digest_string data |> Digestif.SHA256.to_raw_string
815815+ in
769816 let valid = Mirage_crypto_ec.P256.Dsa.verify ~key:pub (r, s) hash in
770817 if valid then Ok () else Error Signature_mismatch
771818···774821 match Mirage_crypto_ec.P384.Dsa.priv_of_octets priv with
775822 | Error _ -> Error (Key_type_mismatch "Invalid P-384 private key")
776823 | Ok priv ->
777777- let hash = Digestif.SHA384.digest_string data |> Digestif.SHA384.to_raw_string in
778778- let (r, s) = Mirage_crypto_ec.P384.Dsa.sign ~key:priv hash in
824824+ let hash =
825825+ Digestif.SHA384.digest_string data |> Digestif.SHA384.to_raw_string
826826+ in
827827+ let r, s = Mirage_crypto_ec.P384.Dsa.sign ~key:priv hash in
779828 let pad48 s =
780829 let len = String.length s in
781830 if len >= 48 then String.sub s (len - 48) 48
···784833 Ok (pad48 r ^ pad48 s)
785834786835 let p384_verify ~pub ~signature data =
787787- if String.length signature <> 96 then
788788- Error Signature_mismatch
836836+ if String.length signature <> 96 then Error Signature_mismatch
789837 else
790838 let r = String.sub signature 0 48 in
791839 let s = String.sub signature 48 48 in
792840 match Mirage_crypto_ec.P384.Dsa.pub_of_octets pub with
793841 | Error _ -> Error (Key_type_mismatch "Invalid P-384 public key")
794842 | Ok pub ->
795795- let hash = Digestif.SHA384.digest_string data |> Digestif.SHA384.to_raw_string in
843843+ let hash =
844844+ Digestif.SHA384.digest_string data |> Digestif.SHA384.to_raw_string
845845+ in
796846 let valid = Mirage_crypto_ec.P384.Dsa.verify ~key:pub (r, s) hash in
797847 if valid then Ok () else Error Signature_mismatch
798848···801851 match Mirage_crypto_ec.P521.Dsa.priv_of_octets priv with
802852 | Error _ -> Error (Key_type_mismatch "Invalid P-521 private key")
803853 | Ok priv ->
804804- let hash = Digestif.SHA512.digest_string data |> Digestif.SHA512.to_raw_string in
805805- let (r, s) = Mirage_crypto_ec.P521.Dsa.sign ~key:priv hash in
854854+ let hash =
855855+ Digestif.SHA512.digest_string data |> Digestif.SHA512.to_raw_string
856856+ in
857857+ let r, s = Mirage_crypto_ec.P521.Dsa.sign ~key:priv hash in
806858 let pad66 s =
807859 let len = String.length s in
808860 if len >= 66 then String.sub s (len - 66) 66
···811863 Ok (pad66 r ^ pad66 s)
812864813865 let p521_verify ~pub ~signature data =
814814- if String.length signature <> 132 then
815815- Error Signature_mismatch
866866+ if String.length signature <> 132 then Error Signature_mismatch
816867 else
817868 let r = String.sub signature 0 66 in
818869 let s = String.sub signature 66 66 in
819870 match Mirage_crypto_ec.P521.Dsa.pub_of_octets pub with
820871 | Error _ -> Error (Key_type_mismatch "Invalid P-521 public key")
821872 | Ok pub ->
822822- let hash = Digestif.SHA512.digest_string data |> Digestif.SHA512.to_raw_string in
873873+ let hash =
874874+ Digestif.SHA512.digest_string data |> Digestif.SHA512.to_raw_string
875875+ in
823876 let valid = Mirage_crypto_ec.P521.Dsa.verify ~key:pub (r, s) hash in
824877 if valid then Ok () else Error Signature_mismatch
825878···847900 let* () =
848901 if alg = Algorithm.None then
849902 (* For alg:none, only allow_none flag matters *)
850850- if allow_none then Ok ()
851851- else Error Unsecured_not_allowed
903903+ if allow_none then Ok () else Error Unsecured_not_allowed
852904 else if List.mem alg allowed_algs then Ok ()
853905 else Error (Algorithm_not_allowed alg_str)
854906 in
855907 let input = signing_input t.raw in
856856- match alg, key.Jwk.key_data with
908908+ match (alg, key.Jwk.key_data) with
857909 | Algorithm.None, _ ->
858910 (* Unsecured JWT - signature must be empty *)
859859- if t.signature = "" then Ok ()
860860- else Error Signature_mismatch
911911+ if t.signature = "" then Ok () else Error Signature_mismatch
861912 | Algorithm.HS256, Jwk.Symmetric { k } ->
862913 let expected = Sign.hmac_sha256 ~key:k input in
863914 if Eqaf.equal expected t.signature then Ok ()
···875926 | Algorithm.EdDSA, Jwk.Ed25519_priv { x; d = _ } ->
876927 Sign.ed25519_verify ~pub:x ~signature:t.signature input
877928 | Algorithm.ES256, Jwk.P256_pub { x; y } ->
878878- let pub = x ^ y in (* Uncompressed point *)
929929+ let pub = x ^ y in
930930+ (* Uncompressed point *)
879931 Sign.p256_verify ~pub ~signature:t.signature input
880932 | Algorithm.ES256, Jwk.P256_priv { x; y; d = _ } ->
881933 let pub = x ^ y in
···899951 | Algorithm.RS512, Jwk.Rsa_pub _ ->
900952 Error (Key_type_mismatch "RSA verification not yet implemented")
901953 | alg, _ ->
902902- Error (Key_type_mismatch
903903- (Printf.sprintf "Key type doesn't match algorithm %s" (Algorithm.to_string alg)))
954954+ Error
955955+ (Key_type_mismatch
956956+ (Printf.sprintf "Key type doesn't match algorithm %s"
957957+ (Algorithm.to_string alg)))
904958905959(* Claims validation *)
906960let validate ~now ?iss ?aud ?(leeway = Ptime.Span.zero) t =
···911965 match Claims.exp claims with
912966 | None -> Ok ()
913967 | Some exp_time ->
914914- let exp_with_leeway = Ptime.add_span exp_time leeway |> Option.value ~default:exp_time in
915915- if Ptime.is_later now ~than:exp_with_leeway then
916916- Error Token_expired
968968+ let exp_with_leeway =
969969+ Ptime.add_span exp_time leeway |> Option.value ~default:exp_time
970970+ in
971971+ if Ptime.is_later now ~than:exp_with_leeway then Error Token_expired
917972 else Ok ()
918973 in
919974 (* Check nbf claim *)
···921976 match Claims.nbf claims with
922977 | None -> Ok ()
923978 | Some nbf_time ->
924924- let nbf_with_leeway = Ptime.sub_span nbf_time leeway |> Option.value ~default:nbf_time in
979979+ let nbf_with_leeway =
980980+ Ptime.sub_span nbf_time leeway |> Option.value ~default:nbf_time
981981+ in
925982 if Ptime.is_earlier now ~than:nbf_with_leeway then
926983 Error Token_not_yet_valid
927984 else Ok ()
···930987 let* () =
931988 match iss with
932989 | None -> Ok ()
933933- | Some expected_iss ->
990990+ | Some expected_iss -> (
934991 match Claims.iss claims with
935992 | None -> Error Invalid_issuer
936993 | Some actual_iss ->
937994 if String.equal expected_iss actual_iss then Ok ()
938938- else Error Invalid_issuer
995995+ else Error Invalid_issuer)
939996 in
940997 (* Check aud claim *)
941998 let* () =
···9481005 in
9491006 Ok ()
9501007951951-let verify_and_validate ~key ~now ?allow_none ?allowed_algs ?iss ?aud ?leeway t =
10081008+let verify_and_validate ~key ~now ?allow_none ?allowed_algs ?iss ?aud ?leeway t
10091009+ =
9521010 let ( let* ) = Result.bind in
9531011 let* () = verify ~key ?allow_none ?allowed_algs t in
9541012 validate ~now ?iss ?aud ?leeway t
···9621020 let payload_b64 = base64url_encode claims_json in
9631021 let signing_input = header_b64 ^ "." ^ payload_b64 in
9641022 let* signature =
965965- match header.Header.alg, key.Jwk.key_data with
10231023+ match (header.Header.alg, key.Jwk.key_data) with
9661024 | Algorithm.None, _ -> Ok ""
9671025 | Algorithm.HS256, Jwk.Symmetric { k } ->
9681026 Ok (Sign.hmac_sha256 ~key:k signing_input)
···9791037 | Algorithm.ES512, Jwk.P521_priv { x = _; y = _; d } ->
9801038 Sign.p521_sign ~priv:d signing_input
9811039 | alg, _ ->
982982- Error (Key_type_mismatch
983983- (Printf.sprintf "Cannot sign with algorithm %s and given key"
984984- (Algorithm.to_string alg)))
10401040+ Error
10411041+ (Key_type_mismatch
10421042+ (Printf.sprintf "Cannot sign with algorithm %s and given key"
10431043+ (Algorithm.to_string alg)))
9851044 in
9861045 let sig_b64 = base64url_encode signature in
9871046 let raw = signing_input ^ "." ^ sig_b64 in
···9941053 match Claims.exp t.claims with
9951054 | None -> false
9961055 | Some exp_time ->
997997- let exp_with_leeway = Ptime.add_span exp_time leeway |> Option.value ~default:exp_time in
10561056+ let exp_with_leeway =
10571057+ Ptime.add_span exp_time leeway |> Option.value ~default:exp_time
10581058+ in
9981059 Ptime.is_later now ~than:exp_with_leeway
999106010001061let time_to_expiry ~now t =
···10021063 | None -> None
10031064 | Some exp_time ->
10041065 let diff = Ptime.diff exp_time now in
10051005- if Ptime.Span.compare diff Ptime.Span.zero <= 0 then None
10061006- else Some diff
10661066+ if Ptime.Span.compare diff Ptime.Span.zero <= 0 then None else Some diff
1007106710081008-(** CBOR Web Token (CWT) support *)
10091068module Cwt = Cwt
10691069+(** CBOR Web Token (CWT) support *)
+177-115
ocaml-jsonwt/lib/jsonwt.mli
···99 {{:https://datatracker.ietf.org/doc/html/rfc7519}RFC 7519}.
10101111 JWTs are compact, URL-safe means of representing claims to be transferred
1212- between two parties. The claims are encoded as a JSON object that is used
1313- as the payload of a JSON Web Signature (JWS) structure, enabling the claims
1414- to be digitally signed or integrity protected with a Message Authentication
1212+ between two parties. The claims are encoded as a JSON object that is used as
1313+ the payload of a JSON Web Signature (JWS) structure, enabling the claims to
1414+ be digitally signed or integrity protected with a Message Authentication
1515 Code (MAC).
16161717 {2 References}
1818- {ul
1919- {- {{:https://datatracker.ietf.org/doc/html/rfc7519}RFC 7519} - JSON Web Token (JWT)}
2020- {- {{:https://datatracker.ietf.org/doc/html/rfc7515}RFC 7515} - JSON Web Signature (JWS)}
2121- {- {{:https://datatracker.ietf.org/doc/html/rfc7517}RFC 7517} - JSON Web Key (JWK)}
2222- {- {{:https://datatracker.ietf.org/doc/html/rfc7518}RFC 7518} - JSON Web Algorithms (JWA)}} *)
1818+ - {{:https://datatracker.ietf.org/doc/html/rfc7519}RFC 7519} - JSON Web
1919+ Token (JWT)
2020+ - {{:https://datatracker.ietf.org/doc/html/rfc7515}RFC 7515} - JSON Web
2121+ Signature (JWS)
2222+ - {{:https://datatracker.ietf.org/doc/html/rfc7517}RFC 7517} - JSON Web Key
2323+ (JWK)
2424+ - {{:https://datatracker.ietf.org/doc/html/rfc7518}RFC 7518} - JSON Web
2525+ Algorithms (JWA) *)
23262427(** {1 Error Handling} *)
25282629type error =
2727- | Invalid_json of string
2828- (** JSON parsing failed *)
2929- | Invalid_base64url of string
3030- (** Base64url decoding failed *)
3030+ | Invalid_json of string (** JSON parsing failed *)
3131+ | Invalid_base64url of string (** Base64url decoding failed *)
3132 | Invalid_structure of string
3233 (** Wrong number of parts or malformed structure *)
3333- | Invalid_header of string
3434- (** Header validation failed *)
3535- | Invalid_claims of string
3636- (** Claims validation failed *)
3434+ | Invalid_header of string (** Header validation failed *)
3535+ | Invalid_claims of string (** Claims validation failed *)
3736 | Invalid_uri of string
3837 (** StringOrURI validation failed per
3939- {{:https://datatracker.ietf.org/doc/html/rfc7519#section-2}RFC 7519 Section 2} *)
3838+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-2}RFC 7519
3939+ Section 2} *)
4040 | Duplicate_claim of string
4141 (** Duplicate claim name found in strict mode per
4242- {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4}RFC 7519 Section 4} *)
4343- | Unsupported_algorithm of string
4444- (** Unknown algorithm identifier *)
4242+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4}RFC 7519
4343+ Section 4} *)
4444+ | Unsupported_algorithm of string (** Unknown algorithm identifier *)
4545 | Algorithm_not_allowed of string
4646 (** Algorithm rejected by allowed_algs policy *)
4747- | Signature_mismatch
4848- (** Signature verification failed *)
4747+ | Signature_mismatch (** Signature verification failed *)
4948 | Token_expired
5049 (** exp claim validation failed per
5151- {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4}RFC 7519 Section 4.1.4} *)
5050+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4}RFC
5151+ 7519 Section 4.1.4} *)
5252 | Token_not_yet_valid
5353 (** nbf claim validation failed per
5454- {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5}RFC 7519 Section 4.1.5} *)
5454+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5}RFC
5555+ 7519 Section 4.1.5} *)
5556 | Invalid_issuer
5657 (** iss claim mismatch per
5757- {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1}RFC 7519 Section 4.1.1} *)
5858+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1}RFC
5959+ 7519 Section 4.1.1} *)
5860 | Invalid_audience
5961 (** aud claim mismatch per
6060- {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3}RFC 7519 Section 4.1.3} *)
6161- | Key_type_mismatch of string
6262- (** Key doesn't match algorithm *)
6262+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3}RFC
6363+ 7519 Section 4.1.3} *)
6464+ | Key_type_mismatch of string (** Key doesn't match algorithm *)
6365 | Unsecured_not_allowed
6466 (** alg:none used without explicit opt-in per
6565- {{:https://datatracker.ietf.org/doc/html/rfc7519#section-6}RFC 7519 Section 6} *)
6666- | Nesting_too_deep
6767- (** Nested JWT exceeds max_depth *)
6767+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-6}RFC 7519
6868+ Section 6} *)
6969+ | Nesting_too_deep (** Nested JWT exceeds max_depth *)
68706971val pp_error : Format.formatter -> error -> unit
7072(** Pretty-print an error. *)
···74767577(** {1 Algorithms}
76787777- Signature and MAC algorithms for JWT.
7878- See {{:https://datatracker.ietf.org/doc/html/rfc7518#section-3}RFC 7518 Section 3}. *)
7979+ Signature and MAC algorithms for JWT. See
8080+ {{:https://datatracker.ietf.org/doc/html/rfc7518#section-3}RFC 7518 Section
8181+ 3}. *)
79828083module Algorithm : sig
8184 type t =
8282- | None (** No digital signature or MAC per
8383- {{:https://datatracker.ietf.org/doc/html/rfc7518#section-3.6}RFC 7518 Section 3.6} *)
8484- | HS256 (** HMAC using SHA-256 per
8585- {{:https://datatracker.ietf.org/doc/html/rfc7518#section-3.2}RFC 7518 Section 3.2} *)
8585+ | None
8686+ (** No digital signature or MAC per
8787+ {{:https://datatracker.ietf.org/doc/html/rfc7518#section-3.6}RFC
8888+ 7518 Section 3.6} *)
8989+ | HS256
9090+ (** HMAC using SHA-256 per
9191+ {{:https://datatracker.ietf.org/doc/html/rfc7518#section-3.2}RFC
9292+ 7518 Section 3.2} *)
8693 | HS384 (** HMAC using SHA-384 *)
8794 | HS512 (** HMAC using SHA-512 *)
8888- | RS256 (** RSASSA-PKCS1-v1_5 using SHA-256 per
8989- {{:https://datatracker.ietf.org/doc/html/rfc7518#section-3.3}RFC 7518 Section 3.3} *)
9595+ | RS256
9696+ (** RSASSA-PKCS1-v1_5 using SHA-256 per
9797+ {{:https://datatracker.ietf.org/doc/html/rfc7518#section-3.3}RFC
9898+ 7518 Section 3.3} *)
9099 | RS384 (** RSASSA-PKCS1-v1_5 using SHA-384 *)
91100 | RS512 (** RSASSA-PKCS1-v1_5 using SHA-512 *)
9292- | ES256 (** ECDSA using P-256 and SHA-256 per
9393- {{:https://datatracker.ietf.org/doc/html/rfc7518#section-3.4}RFC 7518 Section 3.4} *)
101101+ | ES256
102102+ (** ECDSA using P-256 and SHA-256 per
103103+ {{:https://datatracker.ietf.org/doc/html/rfc7518#section-3.4}RFC
104104+ 7518 Section 3.4} *)
94105 | ES384 (** ECDSA using P-384 and SHA-384 *)
95106 | ES512 (** ECDSA using P-521 and SHA-512 *)
9696- | EdDSA (** EdDSA using Ed25519 per
9797- {{:https://datatracker.ietf.org/doc/html/rfc8037}RFC 8037} *)
107107+ | EdDSA
108108+ (** EdDSA using Ed25519 per
109109+ {{:https://datatracker.ietf.org/doc/html/rfc8037}RFC 8037} *)
9811099111 val to_string : t -> string
100112 (** Convert algorithm to JWA identifier string. *)
···111123112124(** {1 JSON Web Key}
113125114114- Key representation for JWT signature verification.
115115- See {{:https://datatracker.ietf.org/doc/html/rfc7517}RFC 7517}. *)
126126+ Key representation for JWT signature verification. See
127127+ {{:https://datatracker.ietf.org/doc/html/rfc7517}RFC 7517}. *)
116128117129module Jwk : sig
118118-119119- (** Key type per {{:https://datatracker.ietf.org/doc/html/rfc7517#section-4.1}RFC 7517 Section 4.1}. *)
130130+ (** Key type per
131131+ {{:https://datatracker.ietf.org/doc/html/rfc7517#section-4.1}RFC 7517
132132+ Section 4.1}. *)
120133 type kty =
121134 | Oct (** Octet sequence (symmetric key) *)
122135 | Rsa (** RSA key *)
123123- | Ec (** Elliptic Curve key *)
136136+ | Ec (** Elliptic Curve key *)
124137 | Okp (** Octet Key Pair (Ed25519, X25519) *)
125138126126- (** Elliptic curve identifiers per {{:https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.1}RFC 7518 Section 6.2.1.1}. *)
139139+ (** Elliptic curve identifiers per
140140+ {{:https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.1}RFC 7518
141141+ Section 6.2.1.1}. *)
127142 type crv =
128128- | P256 (** NIST P-256 curve *)
129129- | P384 (** NIST P-384 curve *)
130130- | P521 (** NIST P-521 curve *)
131131- | Ed25519 (** Ed25519 curve per {{:https://datatracker.ietf.org/doc/html/rfc8037}RFC 8037} *)
143143+ | P256 (** NIST P-256 curve *)
144144+ | P384 (** NIST P-384 curve *)
145145+ | P521 (** NIST P-521 curve *)
146146+ | Ed25519
147147+ (** Ed25519 curve per
148148+ {{:https://datatracker.ietf.org/doc/html/rfc8037}RFC 8037} *)
132149150150+ type t
133151 (** A JSON Web Key. *)
134134- type t
135152136153 (** {2 Constructors} *)
137154138155 val symmetric : string -> t
139139- (** [symmetric k] creates a symmetric key from raw bytes.
140140- Used for HMAC algorithms (HS256, HS384, HS512). *)
156156+ (** [symmetric k] creates a symmetric key from raw bytes. Used for HMAC
157157+ algorithms (HS256, HS384, HS512). *)
141158142159 val ed25519_pub : string -> t
143143- (** [ed25519_pub pub] creates an Ed25519 public key from 32-byte public key. *)
160160+ (** [ed25519_pub pub] creates an Ed25519 public key from 32-byte public key.
161161+ *)
144162145163 val ed25519_priv : pub:string -> priv:string -> t
146164 (** [ed25519_priv ~pub ~priv] creates an Ed25519 private key. *)
···167185 (** [rsa_pub ~n ~e] creates an RSA public key from modulus and exponent. *)
168186169187 val rsa_priv :
170170- n:string -> e:string -> d:string -> p:string -> q:string ->
171171- dp:string -> dq:string -> qi:string -> t
188188+ n:string ->
189189+ e:string ->
190190+ d:string ->
191191+ p:string ->
192192+ q:string ->
193193+ dp:string ->
194194+ dq:string ->
195195+ qi:string ->
196196+ t
172197 (** [rsa_priv ~n ~e ~d ~p ~q ~dp ~dq ~qi] creates an RSA private key. *)
173198174199 (** {2 Accessors} *)
···199224200225(** {1 JOSE Header}
201226202202- The JOSE (JSON Object Signing and Encryption) Header.
203203- See {{:https://datatracker.ietf.org/doc/html/rfc7519#section-5}RFC 7519 Section 5}. *)
227227+ The JOSE (JSON Object Signing and Encryption) Header. See
228228+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-5}RFC 7519 Section
229229+ 5}. *)
204230205231module Header : sig
206232 type t = {
207207- alg : Algorithm.t; (** Algorithm used (REQUIRED) *)
208208- typ : string option; (** Type - RECOMMENDED to be "JWT" per
209209- {{:https://datatracker.ietf.org/doc/html/rfc7519#section-5.1}RFC 7519 Section 5.1} *)
233233+ alg : Algorithm.t; (** Algorithm used (REQUIRED) *)
234234+ typ : string option;
235235+ (** Type - RECOMMENDED to be "JWT" per
236236+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-5.1}RFC
237237+ 7519 Section 5.1} *)
210238 kid : string option; (** Key ID for key lookup *)
211211- cty : string option; (** Content type - MUST be "JWT" for nested JWTs per
212212- {{:https://datatracker.ietf.org/doc/html/rfc7519#section-5.2}RFC 7519 Section 5.2} *)
239239+ cty : string option;
240240+ (** Content type - MUST be "JWT" for nested JWTs per
241241+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-5.2}RFC
242242+ 7519 Section 5.2} *)
213243 }
214244215245 val make : ?typ:string -> ?kid:string -> ?cty:string -> Algorithm.t -> t
···228258229259(** {1 Claims}
230260231231- JWT Claims Set.
232232- See {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4}RFC 7519 Section 4}. *)
261261+ JWT Claims Set. See
262262+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4}RFC 7519 Section
263263+ 4}. *)
233264234265module Claims : sig
235266 type t
236267237268 (** {2 Registered Claim Names}
238269239239- See {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1}RFC 7519 Section 4.1}. *)
270270+ See
271271+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1}RFC 7519
272272+ Section 4.1}. *)
240273241274 val iss : t -> string option
242242- (** Issuer claim per {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1}Section 4.1.1}. *)
275275+ (** Issuer claim per
276276+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1}Section
277277+ 4.1.1}. *)
243278244279 val sub : t -> string option
245245- (** Subject claim per {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2}Section 4.1.2}. *)
280280+ (** Subject claim per
281281+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2}Section
282282+ 4.1.2}. *)
246283247284 val aud : t -> string list
248248- (** Audience claim per {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3}Section 4.1.3}.
249249- Returns empty list if not present. May be single string or array in JWT. *)
285285+ (** Audience claim per
286286+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3}Section
287287+ 4.1.3}. Returns empty list if not present. May be single string or array
288288+ in JWT. *)
250289251290 val exp : t -> Ptime.t option
252252- (** Expiration time claim per {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4}Section 4.1.4}. *)
291291+ (** Expiration time claim per
292292+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4}Section
293293+ 4.1.4}. *)
253294254295 val nbf : t -> Ptime.t option
255255- (** Not Before claim per {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5}Section 4.1.5}. *)
296296+ (** Not Before claim per
297297+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5}Section
298298+ 4.1.5}. *)
256299257300 val iat : t -> Ptime.t option
258258- (** Issued At claim per {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6}Section 4.1.6}. *)
301301+ (** Issued At claim per
302302+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6}Section
303303+ 4.1.6}. *)
259304260305 val jti : t -> string option
261261- (** JWT ID claim per {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7}Section 4.1.7}. *)
306306+ (** JWT ID claim per
307307+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7}Section
308308+ 4.1.7}. *)
262309263310 (** {2 Custom Claims}
264311265312 For Public and Private claims per
266266- {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.2}Sections 4.2} and
267267- {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.3}4.3}. *)
313313+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.2}Sections 4.2}
314314+ and {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.3}4.3}. *)
268315269316 val get : string -> t -> Jsont.json option
270317 (** [get name claims] returns the value of custom claim [name]. *)
271318272319 val get_string : string -> t -> string option
273273- (** [get_string name claims] returns the string value of custom claim [name]. *)
320320+ (** [get_string name claims] returns the string value of custom claim [name].
321321+ *)
274322275323 val get_int : string -> t -> int option
276324 (** [get_int name claims] returns the integer value of custom claim [name]. *)
277325278326 val get_bool : string -> t -> bool option
279279- (** [get_bool name claims] returns the boolean value of custom claim [name]. *)
327327+ (** [get_bool name claims] returns the boolean value of custom claim [name].
328328+ *)
280329281330 (** {2 Construction} *)
282331···326375327376 val of_json : ?strict:bool -> string -> (t, error) result
328377 (** [of_json ?strict json] parses claims from JSON string.
329329- @param strict If true (default), reject duplicate claim names per
330330- {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4}RFC 7519 Section 4}.
331331- If false, use lexically last duplicate. *)
378378+ @param strict
379379+ If true (default), reject duplicate claim names per
380380+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4}RFC 7519
381381+ Section 4}. If false, use lexically last duplicate. *)
332382333383 val to_json : t -> string
334384 (** Serialize claims to JSON string. *)
···337387(** {1 JWT Token} *)
338388339389type t = {
340340- header : Header.t; (** JOSE header *)
341341- claims : Claims.t; (** Claims set *)
342342- signature : string; (** Raw signature bytes *)
343343- raw : string; (** Original compact serialization *)
390390+ header : Header.t; (** JOSE header *)
391391+ claims : Claims.t; (** Claims set *)
392392+ signature : string; (** Raw signature bytes *)
393393+ raw : string; (** Original compact serialization *)
344394}
345395(** A parsed JWT token. *)
346396347397(** {2 Parsing}
348398349349- See {{:https://datatracker.ietf.org/doc/html/rfc7519#section-7.2}RFC 7519 Section 7.2}. *)
399399+ See
400400+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-7.2}RFC 7519
401401+ Section 7.2}. *)
350402351403val parse : ?strict:bool -> string -> (t, error) result
352404(** [parse ?strict token_string] parses a JWT from its compact serialization.
353405354354- This parses the token structure but does NOT verify the signature.
355355- Use {!verify} to validate the signature after parsing.
406406+ This parses the token structure but does NOT verify the signature. Use
407407+ {!verify} to validate the signature after parsing.
356408357409 @param strict If true (default), reject duplicate claim names. *)
358410···362414363415(** {2 Nested JWTs}
364416365365- See {{:https://datatracker.ietf.org/doc/html/rfc7519#section-7.2}RFC 7519 Section 7.2 step 8}
366366- and {{:https://datatracker.ietf.org/doc/html/rfc7519#appendix-A.2}Appendix A.2}. *)
417417+ See
418418+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-7.2}RFC 7519
419419+ Section 7.2 step 8} and
420420+ {{:https://datatracker.ietf.org/doc/html/rfc7519#appendix-A.2}Appendix A.2}.
421421+*)
367422368423val parse_nested :
369369- ?strict:bool ->
370370- ?max_depth:int ->
371371- string ->
372372- (t list, error) result
424424+ ?strict:bool -> ?max_depth:int -> string -> (t list, error) result
373425(** [parse_nested ?strict ?max_depth token] parses a potentially nested JWT.
374426 Returns a list of JWTs from outermost to innermost.
375427 @param max_depth Maximum nesting depth (default 5). *)
376428377429val is_nested : t -> bool
378378-(** [is_nested t] returns true if the JWT has [cty: "JWT"] header,
379379- indicating it contains a nested JWT. *)
430430+(** [is_nested t] returns true if the JWT has [cty: "JWT"] header, indicating it
431431+ contains a nested JWT. *)
380432381433(** {2 Accessors} *)
382434···394446395447(** {2 Verification}
396448397397- See {{:https://datatracker.ietf.org/doc/html/rfc7519#section-7.2}RFC 7519 Section 7.2}. *)
449449+ See
450450+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-7.2}RFC 7519
451451+ Section 7.2}. *)
398452399453val verify :
400454 key:Jwk.t ->
···405459(** [verify ~key ?allow_none ?allowed_algs t] verifies the JWT signature.
406460407461 @param key The key to verify with (must match algorithm)
408408- @param allow_none If true, accept [alg:"none"]. Default: false.
409409- Per {{:https://datatracker.ietf.org/doc/html/rfc7519#section-6}RFC 7519 Section 6},
410410- unsecured JWTs should only be used when security is provided by other means.
411411- @param allowed_algs List of acceptable algorithms. Default: all except none.
412412- Note: "none" is only allowed if BOTH in this list AND [allow_none=true]. *)
462462+ @param allow_none
463463+ If true, accept [alg:"none"]. Default: false. Per
464464+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-6}RFC 7519
465465+ Section 6}, unsecured JWTs should only be used when security is provided
466466+ by other means.
467467+ @param allowed_algs
468468+ List of acceptable algorithms. Default: all except none. Note: "none" is
469469+ only allowed if BOTH in this list AND [allow_none=true]. *)
413470414471val validate :
415472 now:Ptime.t ->
···435492 ?leeway:Ptime.Span.t ->
436493 t ->
437494 (unit, error) result
438438-(** [verify_and_validate ~key ~now ...] verifies signature and validates claims. *)
495495+(** [verify_and_validate ~key ~now ...] verifies signature and validates claims.
496496+*)
439497440498(** {2 Creation}
441499442442- See {{:https://datatracker.ietf.org/doc/html/rfc7519#section-7.1}RFC 7519 Section 7.1}. *)
500500+ See
501501+ {{:https://datatracker.ietf.org/doc/html/rfc7519#section-7.1}RFC 7519
502502+ Section 7.1}. *)
443503444444-val create : header:Header.t -> claims:Claims.t -> key:Jwk.t -> (t, error) result
504504+val create :
505505+ header:Header.t -> claims:Claims.t -> key:Jwk.t -> (t, error) result
445506(** [create ~header ~claims ~key] creates and signs a new JWT.
446507447447- The [key] must be appropriate for the algorithm specified in [header].
448448- For [alg:none], pass any key (it will be ignored). *)
508508+ The [key] must be appropriate for the algorithm specified in [header]. For
509509+ [alg:none], pass any key (it will be ignored). *)
449510450511val encode : t -> string
451512(** [encode t] returns the compact serialization of the JWT. *)
···453514(** {1 Utilities} *)
454515455516val is_expired : now:Ptime.t -> ?leeway:Ptime.Span.t -> t -> bool
456456-(** [is_expired ~now ?leeway t] checks if the token has expired.
457457- Returns false if no exp claim present. *)
517517+(** [is_expired ~now ?leeway t] checks if the token has expired. Returns false
518518+ if no exp claim present. *)
458519459520val time_to_expiry : now:Ptime.t -> t -> Ptime.Span.t option
460460-(** [time_to_expiry ~now t] returns time until expiration, or [None] if
461461- no expiration claim or already expired. *)
521521+(** [time_to_expiry ~now t] returns time until expiration, or [None] if no
522522+ expiration claim or already expired. *)
462523463524(** {1 Base64url Utilities}
464525···466527467528val base64url_encode : string -> string
468529(** Base64url encode without padding per
469469- {{:https://datatracker.ietf.org/doc/html/rfc7515#appendix-C}RFC 7515 Appendix C}. *)
530530+ {{:https://datatracker.ietf.org/doc/html/rfc7515#appendix-C}RFC 7515
531531+ Appendix C}. *)
470532471533val base64url_decode : string -> (string, error) result
472534(** Base64url decode, handling missing padding. *)
+379-296
ocaml-jsonwt/test/test_cbor.ml
···11(** CBOR Encoding Tests
2233- Tests derived from RFC 8949 Appendix A (Examples of Encoded CBOR Data Items). *)
33+ Tests derived from RFC 8949 Appendix A (Examples of Encoded CBOR Data
44+ Items). *)
4556(* Helper to encode to hex string *)
67let encode_to_hex f =
···1011 f enc;
1112 Cbort.Rw.flush_encoder enc;
1213 let bytes = Buffer.contents buf in
1313- String.concat "" (List.init (String.length bytes) (fun i ->
1414- Printf.sprintf "%02x" (Char.code (String.get bytes i))))
1414+ String.concat ""
1515+ (List.init (String.length bytes) (fun i ->
1616+ Printf.sprintf "%02x" (Char.code (String.get bytes i))))
15171618(* Helper to convert hex string to bytes for comparison *)
1719let hex_to_bytes hex =
···6365 Alcotest.(check string) "1000000" "1a000f4240" hex
64666567let test_uint_1000000000000 () =
6666- let hex = encode_to_hex (fun enc -> Cbort.Rw.write_int64 enc 1000000000000L) in
6868+ let hex =
6969+ encode_to_hex (fun enc -> Cbort.Rw.write_int64 enc 1000000000000L)
7070+ in
6771 Alcotest.(check string) "1000000000000" "1b000000e8d4a51000" hex
68726973(* ============= Negative Integer Tests ============= *)
···172176173177let test_text_utf8_emoji () =
174178 (* U+10151 = 𐅑 = 0xf0 0x90 0x85 0x91 in UTF-8 *)
175175- let hex = encode_to_hex (fun enc -> Cbort.Rw.write_text enc "\xf0\x90\x85\x91") in
179179+ let hex =
180180+ encode_to_hex (fun enc -> Cbort.Rw.write_text enc "\xf0\x90\x85\x91")
181181+ in
176182 Alcotest.(check string) "𐅑" "64f0908591" hex
177183178184(* ============= Byte String Tests ============= *)
179185180186let test_bytes_empty () =
181181- let hex = encode_to_hex (fun enc ->
182182- Cbort.Rw.write_bytes_header enc 0) in
187187+ let hex = encode_to_hex (fun enc -> Cbort.Rw.write_bytes_header enc 0) in
183188 Alcotest.(check string) "empty bytes" "40" hex
184189185190let test_bytes_01020304 () =
186186- let hex = encode_to_hex (fun enc ->
187187- Cbort.Rw.write_bytes_header enc 4;
188188- Cbort.Rw.write_bytes enc (hex_to_bytes "01020304")) in
191191+ let hex =
192192+ encode_to_hex (fun enc ->
193193+ Cbort.Rw.write_bytes_header enc 4;
194194+ Cbort.Rw.write_bytes enc (hex_to_bytes "01020304"))
195195+ in
189196 Alcotest.(check string) "h'01020304'" "4401020304" hex
190197191198(* ============= Array Tests ============= *)
···195202 Alcotest.(check string) "[]" "80" hex
196203197204let test_array_123 () =
198198- let hex = encode_to_hex (fun enc ->
199199- Cbort.Rw.write_array_start enc 3;
200200- Cbort.Rw.write_int enc 1;
201201- Cbort.Rw.write_int enc 2;
202202- Cbort.Rw.write_int enc 3) in
205205+ let hex =
206206+ encode_to_hex (fun enc ->
207207+ Cbort.Rw.write_array_start enc 3;
208208+ Cbort.Rw.write_int enc 1;
209209+ Cbort.Rw.write_int enc 2;
210210+ Cbort.Rw.write_int enc 3)
211211+ in
203212 Alcotest.(check string) "[1, 2, 3]" "83010203" hex
204213205214let test_array_nested () =
206215 (* [1, [2, 3], [4, 5]] *)
207207- let hex = encode_to_hex (fun enc ->
208208- Cbort.Rw.write_array_start enc 3;
209209- Cbort.Rw.write_int enc 1;
210210- Cbort.Rw.write_array_start enc 2;
211211- Cbort.Rw.write_int enc 2;
212212- Cbort.Rw.write_int enc 3;
213213- Cbort.Rw.write_array_start enc 2;
214214- Cbort.Rw.write_int enc 4;
215215- Cbort.Rw.write_int enc 5) in
216216+ let hex =
217217+ encode_to_hex (fun enc ->
218218+ Cbort.Rw.write_array_start enc 3;
219219+ Cbort.Rw.write_int enc 1;
220220+ Cbort.Rw.write_array_start enc 2;
221221+ Cbort.Rw.write_int enc 2;
222222+ Cbort.Rw.write_int enc 3;
223223+ Cbort.Rw.write_array_start enc 2;
224224+ Cbort.Rw.write_int enc 4;
225225+ Cbort.Rw.write_int enc 5)
226226+ in
216227 Alcotest.(check string) "[1, [2, 3], [4, 5]]" "8301820203820405" hex
217228218229let test_array_25_items () =
219230 (* [1, 2, 3, ..., 25] - requires 1-byte length encoding *)
220220- let hex = encode_to_hex (fun enc ->
221221- Cbort.Rw.write_array_start enc 25;
222222- for i = 1 to 25 do
223223- Cbort.Rw.write_int enc i
224224- done) in
231231+ let hex =
232232+ encode_to_hex (fun enc ->
233233+ Cbort.Rw.write_array_start enc 25;
234234+ for i = 1 to 25 do
235235+ Cbort.Rw.write_int enc i
236236+ done)
237237+ in
225238 (* 0x98 0x19 = array with 1-byte length (25) *)
226226- Alcotest.(check string) "[1..25]" "98190102030405060708090a0b0c0d0e0f101112131415161718181819" hex
239239+ Alcotest.(check string)
240240+ "[1..25]" "98190102030405060708090a0b0c0d0e0f101112131415161718181819" hex
227241228242(* ============= Map Tests ============= *)
229243···233247234248let test_map_int_keys () =
235249 (* {1: 2, 3: 4} *)
236236- let hex = encode_to_hex (fun enc ->
237237- Cbort.Rw.write_map_start enc 2;
238238- Cbort.Rw.write_int enc 1;
239239- Cbort.Rw.write_int enc 2;
240240- Cbort.Rw.write_int enc 3;
241241- Cbort.Rw.write_int enc 4) in
250250+ let hex =
251251+ encode_to_hex (fun enc ->
252252+ Cbort.Rw.write_map_start enc 2;
253253+ Cbort.Rw.write_int enc 1;
254254+ Cbort.Rw.write_int enc 2;
255255+ Cbort.Rw.write_int enc 3;
256256+ Cbort.Rw.write_int enc 4)
257257+ in
242258 Alcotest.(check string) "{1: 2, 3: 4}" "a201020304" hex
243259244260let test_map_string_keys () =
245261 (* {"a": 1, "b": [2, 3]} *)
246246- let hex = encode_to_hex (fun enc ->
247247- Cbort.Rw.write_map_start enc 2;
248248- Cbort.Rw.write_text enc "a";
249249- Cbort.Rw.write_int enc 1;
250250- Cbort.Rw.write_text enc "b";
251251- Cbort.Rw.write_array_start enc 2;
252252- Cbort.Rw.write_int enc 2;
253253- Cbort.Rw.write_int enc 3) in
262262+ let hex =
263263+ encode_to_hex (fun enc ->
264264+ Cbort.Rw.write_map_start enc 2;
265265+ Cbort.Rw.write_text enc "a";
266266+ Cbort.Rw.write_int enc 1;
267267+ Cbort.Rw.write_text enc "b";
268268+ Cbort.Rw.write_array_start enc 2;
269269+ Cbort.Rw.write_int enc 2;
270270+ Cbort.Rw.write_int enc 3)
271271+ in
254272 Alcotest.(check string) "{\"a\": 1, \"b\": [2, 3]}" "a26161016162820203" hex
255273256274let test_mixed_array_map () =
257275 (* ["a", {"b": "c"}] *)
258258- let hex = encode_to_hex (fun enc ->
259259- Cbort.Rw.write_array_start enc 2;
260260- Cbort.Rw.write_text enc "a";
261261- Cbort.Rw.write_map_start enc 1;
262262- Cbort.Rw.write_text enc "b";
263263- Cbort.Rw.write_text enc "c") in
276276+ let hex =
277277+ encode_to_hex (fun enc ->
278278+ Cbort.Rw.write_array_start enc 2;
279279+ Cbort.Rw.write_text enc "a";
280280+ Cbort.Rw.write_map_start enc 1;
281281+ Cbort.Rw.write_text enc "b";
282282+ Cbort.Rw.write_text enc "c")
283283+ in
264284 Alcotest.(check string) "[\"a\", {\"b\": \"c\"}]" "826161a161626163" hex
265285266286let test_map_5_pairs () =
267287 (* {"a": "A", "b": "B", "c": "C", "d": "D", "e": "E"} *)
268268- let hex = encode_to_hex (fun enc ->
269269- Cbort.Rw.write_map_start enc 5;
270270- Cbort.Rw.write_text enc "a"; Cbort.Rw.write_text enc "A";
271271- Cbort.Rw.write_text enc "b"; Cbort.Rw.write_text enc "B";
272272- Cbort.Rw.write_text enc "c"; Cbort.Rw.write_text enc "C";
273273- Cbort.Rw.write_text enc "d"; Cbort.Rw.write_text enc "D";
274274- Cbort.Rw.write_text enc "e"; Cbort.Rw.write_text enc "E") in
275275- Alcotest.(check string) "{a:A, b:B, c:C, d:D, e:E}" "a56161614161626142616361436164614461656145" hex
288288+ let hex =
289289+ encode_to_hex (fun enc ->
290290+ Cbort.Rw.write_map_start enc 5;
291291+ Cbort.Rw.write_text enc "a";
292292+ Cbort.Rw.write_text enc "A";
293293+ Cbort.Rw.write_text enc "b";
294294+ Cbort.Rw.write_text enc "B";
295295+ Cbort.Rw.write_text enc "c";
296296+ Cbort.Rw.write_text enc "C";
297297+ Cbort.Rw.write_text enc "d";
298298+ Cbort.Rw.write_text enc "D";
299299+ Cbort.Rw.write_text enc "e";
300300+ Cbort.Rw.write_text enc "E")
301301+ in
302302+ Alcotest.(check string)
303303+ "{a:A, b:B, c:C, d:D, e:E}" "a56161614161626142616361436164614461656145" hex
276304277305(* ============= Tag Tests ============= *)
278306279307let test_tag_epoch_timestamp () =
280308 (* 1(1363896240) - epoch-based date/time *)
281281- let hex = encode_to_hex (fun enc ->
282282- Cbort.Rw.write_type_arg enc Cbort.Rw.major_tag 1;
283283- Cbort.Rw.write_int enc 1363896240) in
309309+ let hex =
310310+ encode_to_hex (fun enc ->
311311+ Cbort.Rw.write_type_arg enc Cbort.Rw.major_tag 1;
312312+ Cbort.Rw.write_int enc 1363896240)
313313+ in
284314 Alcotest.(check string) "1(1363896240)" "c11a514b67b0" hex
285315286316(* ============= Major Type Constants Test ============= *)
···313343(* Round-trip tests using Cbort.encode_string and Cbort.decode_string *)
314344315345let test_codec_int_roundtrip () =
316316- let values = [0; 1; 23; 24; 100; 1000; 1000000; -1; -10; -100; -1000] in
317317- List.iter (fun v ->
318318- let encoded = Cbort.encode_string Cbort.int v in
319319- match Cbort.decode_string Cbort.int encoded with
320320- | Ok decoded -> Alcotest.(check int) (Printf.sprintf "int %d" v) v decoded
321321- | Error e -> Alcotest.fail (Cbort.Error.to_string e)
322322- ) values
346346+ let values = [ 0; 1; 23; 24; 100; 1000; 1000000; -1; -10; -100; -1000 ] in
347347+ List.iter
348348+ (fun v ->
349349+ let encoded = Cbort.encode_string Cbort.int v in
350350+ match Cbort.decode_string Cbort.int encoded with
351351+ | Ok decoded -> Alcotest.(check int) (Printf.sprintf "int %d" v) v decoded
352352+ | Error e -> Alcotest.fail (Cbort.Error.to_string e))
353353+ values
323354324355let test_codec_int64_roundtrip () =
325325- let values = [0L; 1L; 1000000000000L; -1L; Int64.max_int; Int64.min_int] in
326326- List.iter (fun v ->
327327- let encoded = Cbort.encode_string Cbort.int64 v in
328328- match Cbort.decode_string Cbort.int64 encoded with
329329- | Ok decoded -> Alcotest.(check int64) (Printf.sprintf "int64 %Ld" v) v decoded
330330- | Error e -> Alcotest.fail (Cbort.Error.to_string e)
331331- ) values
356356+ let values = [ 0L; 1L; 1000000000000L; -1L; Int64.max_int; Int64.min_int ] in
357357+ List.iter
358358+ (fun v ->
359359+ let encoded = Cbort.encode_string Cbort.int64 v in
360360+ match Cbort.decode_string Cbort.int64 encoded with
361361+ | Ok decoded ->
362362+ Alcotest.(check int64) (Printf.sprintf "int64 %Ld" v) v decoded
363363+ | Error e -> Alcotest.fail (Cbort.Error.to_string e))
364364+ values
332365333366let test_codec_bool_roundtrip () =
334334- List.iter (fun v ->
335335- let encoded = Cbort.encode_string Cbort.bool v in
336336- match Cbort.decode_string Cbort.bool encoded with
337337- | Ok decoded -> Alcotest.(check bool) (Printf.sprintf "bool %b" v) v decoded
338338- | Error e -> Alcotest.fail (Cbort.Error.to_string e)
339339- ) [true; false]
367367+ List.iter
368368+ (fun v ->
369369+ let encoded = Cbort.encode_string Cbort.bool v in
370370+ match Cbort.decode_string Cbort.bool encoded with
371371+ | Ok decoded ->
372372+ Alcotest.(check bool) (Printf.sprintf "bool %b" v) v decoded
373373+ | Error e -> Alcotest.fail (Cbort.Error.to_string e))
374374+ [ true; false ]
340375341376let test_codec_null_roundtrip () =
342377 let encoded = Cbort.encode_string Cbort.null () in
···345380 | Error e -> Alcotest.fail (Cbort.Error.to_string e)
346381347382let test_codec_float_roundtrip () =
348348- let values = [0.0; 1.0; -1.0; 1.5; 3.14159; 1e10; -1e-10] in
349349- List.iter (fun v ->
350350- let encoded = Cbort.encode_string Cbort.float v in
351351- match Cbort.decode_string Cbort.float encoded with
352352- | Ok decoded ->
353353- let diff = abs_float (v -. decoded) in
354354- Alcotest.(check bool) (Printf.sprintf "float %g" v) true (diff < 1e-10)
355355- | Error e -> Alcotest.fail (Cbort.Error.to_string e)
356356- ) values
383383+ let values = [ 0.0; 1.0; -1.0; 1.5; 3.14159; 1e10; -1e-10 ] in
384384+ List.iter
385385+ (fun v ->
386386+ let encoded = Cbort.encode_string Cbort.float v in
387387+ match Cbort.decode_string Cbort.float encoded with
388388+ | Ok decoded ->
389389+ let diff = abs_float (v -. decoded) in
390390+ Alcotest.(check bool) (Printf.sprintf "float %g" v) true (diff < 1e-10)
391391+ | Error e -> Alcotest.fail (Cbort.Error.to_string e))
392392+ values
357393358394let test_codec_string_roundtrip () =
359359- let values = [""; "a"; "hello"; "UTF-8: \xc3\xbc \xe6\xb0\xb4"; "with\nnewline"] in
360360- List.iter (fun v ->
361361- let encoded = Cbort.encode_string Cbort.string v in
362362- match Cbort.decode_string Cbort.string encoded with
363363- | Ok decoded -> Alcotest.(check string) (Printf.sprintf "string %S" v) v decoded
364364- | Error e -> Alcotest.fail (Cbort.Error.to_string e)
365365- ) values
395395+ let values =
396396+ [ ""; "a"; "hello"; "UTF-8: \xc3\xbc \xe6\xb0\xb4"; "with\nnewline" ]
397397+ in
398398+ List.iter
399399+ (fun v ->
400400+ let encoded = Cbort.encode_string Cbort.string v in
401401+ match Cbort.decode_string Cbort.string encoded with
402402+ | Ok decoded ->
403403+ Alcotest.(check string) (Printf.sprintf "string %S" v) v decoded
404404+ | Error e -> Alcotest.fail (Cbort.Error.to_string e))
405405+ values
366406367407let test_codec_bytes_roundtrip () =
368368- let values = [""; "\x00\x01\x02\x03"; String.make 100 '\xff'] in
369369- List.iter (fun v ->
370370- let encoded = Cbort.encode_string Cbort.bytes v in
371371- match Cbort.decode_string Cbort.bytes encoded with
372372- | Ok decoded -> Alcotest.(check string) "bytes" v decoded
373373- | Error e -> Alcotest.fail (Cbort.Error.to_string e)
374374- ) values
408408+ let values = [ ""; "\x00\x01\x02\x03"; String.make 100 '\xff' ] in
409409+ List.iter
410410+ (fun v ->
411411+ let encoded = Cbort.encode_string Cbort.bytes v in
412412+ match Cbort.decode_string Cbort.bytes encoded with
413413+ | Ok decoded -> Alcotest.(check string) "bytes" v decoded
414414+ | Error e -> Alcotest.fail (Cbort.Error.to_string e))
415415+ values
375416376417let test_codec_array_roundtrip () =
377377- let values = [[]; [1]; [1;2;3]; List.init 25 (fun i -> i)] in
418418+ let values = [ []; [ 1 ]; [ 1; 2; 3 ]; List.init 25 (fun i -> i) ] in
378419 let int_list = Cbort.array Cbort.int in
379379- List.iter (fun v ->
380380- let encoded = Cbort.encode_string int_list v in
381381- match Cbort.decode_string int_list encoded with
382382- | Ok decoded -> Alcotest.(check (list int)) "array" v decoded
383383- | Error e -> Alcotest.fail (Cbort.Error.to_string e)
384384- ) values
420420+ List.iter
421421+ (fun v ->
422422+ let encoded = Cbort.encode_string int_list v in
423423+ match Cbort.decode_string int_list encoded with
424424+ | Ok decoded -> Alcotest.(check (list int)) "array" v decoded
425425+ | Error e -> Alcotest.fail (Cbort.Error.to_string e))
426426+ values
385427386428let test_codec_nested_array () =
387429 let nested = Cbort.array (Cbort.array Cbort.int) in
388388- let v = [[1;2]; [3;4;5]; []] in
430430+ let v = [ [ 1; 2 ]; [ 3; 4; 5 ]; [] ] in
389431 let encoded = Cbort.encode_string nested v in
390432 match Cbort.decode_string nested encoded with
391433 | Ok decoded -> Alcotest.(check (list (list int))) "nested array" v decoded
···393435394436let test_codec_string_map_roundtrip () =
395437 let map = Cbort.string_map Cbort.int in
396396- let v = [("a", 1); ("b", 2); ("c", 3)] in
438438+ let v = [ ("a", 1); ("b", 2); ("c", 3) ] in
397439 let encoded = Cbort.encode_string map v in
398440 match Cbort.decode_string map encoded with
399441 | Ok decoded ->
400442 (* Maps may reorder, so sort before comparing *)
401443 let sort = List.sort compare in
402402- Alcotest.(check (list (pair string int))) "string map" (sort v) (sort decoded)
444444+ Alcotest.(check (list (pair string int)))
445445+ "string map" (sort v) (sort decoded)
403446 | Error e -> Alcotest.fail (Cbort.Error.to_string e)
404447405448let test_codec_int_map_roundtrip () =
406449 let map = Cbort.int_map Cbort.string in
407407- let v = [(1, "one"); (2, "two"); (3, "three")] in
450450+ let v = [ (1, "one"); (2, "two"); (3, "three") ] in
408451 let encoded = Cbort.encode_string map v in
409452 match Cbort.decode_string map encoded with
410453 | Ok decoded ->
411454 let sort = List.sort compare in
412412- Alcotest.(check (list (pair int string))) "int map" (sort v) (sort decoded)
455455+ Alcotest.(check (list (pair int string)))
456456+ "int map" (sort v) (sort decoded)
413457 | Error e -> Alcotest.fail (Cbort.Error.to_string e)
414458415459let test_codec_tuple2 () =
···426470 let encoded = Cbort.encode_string codec v in
427471 match Cbort.decode_string codec encoded with
428472 | Ok decoded ->
429429- let (a, b, c) = decoded in
473473+ let a, b, c = decoded in
430474 Alcotest.(check int) "tuple3.0" 42 a;
431475 Alcotest.(check string) "tuple3.1" "hello" b;
432476 Alcotest.(check bool) "tuple3.2" true c
···438482 let v1 = Some 42 in
439483 let encoded1 = Cbort.encode_string codec v1 in
440484 (match Cbort.decode_string codec encoded1 with
441441- | Ok decoded -> Alcotest.(check (option int)) "nullable some" v1 decoded
442442- | Error e -> Alcotest.fail (Cbort.Error.to_string e));
485485+ | Ok decoded -> Alcotest.(check (option int)) "nullable some" v1 decoded
486486+ | Error e -> Alcotest.fail (Cbort.Error.to_string e));
443487 (* Test None *)
444488 let v2 = None in
445489 let encoded2 = Cbort.encode_string codec v2 in
···452496type person = { name : string; age : int; email : string option }
453497454498let person_codec =
455455- Cbort.Obj.finish @@
499499+ Cbort.Obj.finish
500500+ @@
456501 let open Cbort.Obj in
457502 let* name = mem "name" (fun p -> p.name) Cbort.string in
458503 let* age = mem "age" (fun p -> p.age) Cbort.int in
···484529(* CWT-style claims with integer keys per RFC 8392:
485530 1=iss, 2=sub, 3=aud, 4=exp, 5=nbf, 6=iat, 7=cti *)
486531type cwt_claims = {
487487- iss : string option; (* key 1 *)
488488- sub : string option; (* key 2 *)
489489- exp : int64 option; (* key 4 *)
532532+ iss : string option; (* key 1 *)
533533+ sub : string option; (* key 2 *)
534534+ exp : int64 option; (* key 4 *)
490535}
491536492537let cwt_claims_codec =
493493- Cbort.Obj_int.finish @@
538538+ Cbort.Obj_int.finish
539539+ @@
494540 let open Cbort.Obj_int in
495541 let* iss = mem_opt 1 (fun c -> c.iss) Cbort.string in
496542 let* sub = mem_opt 2 (fun c -> c.sub) Cbort.string in
···498544 return { iss; sub; exp }
499545500546let test_obj_int_codec () =
501501- let v = { iss = Some "https://example.com"; sub = Some "user123"; exp = Some 1700000000L } in
547547+ let v =
548548+ {
549549+ iss = Some "https://example.com";
550550+ sub = Some "user123";
551551+ exp = Some 1700000000L;
552552+ }
553553+ in
502554 let encoded = Cbort.encode_string cwt_claims_codec v in
503555 match Cbort.decode_string cwt_claims_codec encoded with
504556 | Ok decoded ->
···525577 let v = 1363896240L in
526578 let encoded = Cbort.encode_string epoch_codec v in
527579 (* Should match RFC 8949 example: c11a514b67b0 *)
528528- let hex = String.concat "" (List.init (String.length encoded) (fun i ->
529529- Printf.sprintf "%02x" (Char.code (String.get encoded i)))) in
580580+ let hex =
581581+ String.concat ""
582582+ (List.init (String.length encoded) (fun i ->
583583+ Printf.sprintf "%02x" (Char.code (String.get encoded i))))
584584+ in
530585 Alcotest.(check string) "epoch tag hex" "c11a514b67b0" hex;
531586 match Cbort.decode_string epoch_codec encoded with
532587 | Ok decoded -> Alcotest.(check int64) "epoch value" v decoded
···539594 (* Encode with tag *)
540595 let encoded = Cbort.encode_string uri_codec v in
541596 (match Cbort.decode_string uri_codec encoded with
542542- | Ok decoded -> Alcotest.(check string) "uri tagged" v decoded
543543- | Error e -> Alcotest.fail (Cbort.Error.to_string e));
597597+ | Ok decoded -> Alcotest.(check string) "uri tagged" v decoded
598598+ | Error e -> Alcotest.fail (Cbort.Error.to_string e));
544599 (* Decode without tag should also work *)
545600 let plain = Cbort.encode_string Cbort.string v in
546601 match Cbort.decode_string uri_codec plain with
···551606552607let test_decode_rfc_integers () =
553608 (* RFC 8949 Appendix A test vectors *)
554554- let tests = [
555555- ("00", 0L);
556556- ("01", 1L);
557557- ("0a", 10L);
558558- ("17", 23L);
559559- ("1818", 24L);
560560- ("1819", 25L);
561561- ("1864", 100L);
562562- ("1903e8", 1000L);
563563- ("1a000f4240", 1000000L);
564564- ("1b000000e8d4a51000", 1000000000000L);
565565- ("20", -1L);
566566- ("29", -10L);
567567- ("3863", -100L);
568568- ("3903e7", -1000L);
569569- ] in
570570- List.iter (fun (hex, expected) ->
571571- let bytes = hex_to_bytes hex in
572572- match Cbort.decode_string Cbort.int64 bytes with
573573- | Ok decoded -> Alcotest.(check int64) hex expected decoded
574574- | Error e -> Alcotest.fail (Printf.sprintf "%s: %s" hex (Cbort.Error.to_string e))
575575- ) tests
609609+ let tests =
610610+ [
611611+ ("00", 0L);
612612+ ("01", 1L);
613613+ ("0a", 10L);
614614+ ("17", 23L);
615615+ ("1818", 24L);
616616+ ("1819", 25L);
617617+ ("1864", 100L);
618618+ ("1903e8", 1000L);
619619+ ("1a000f4240", 1000000L);
620620+ ("1b000000e8d4a51000", 1000000000000L);
621621+ ("20", -1L);
622622+ ("29", -10L);
623623+ ("3863", -100L);
624624+ ("3903e7", -1000L);
625625+ ]
626626+ in
627627+ List.iter
628628+ (fun (hex, expected) ->
629629+ let bytes = hex_to_bytes hex in
630630+ match Cbort.decode_string Cbort.int64 bytes with
631631+ | Ok decoded -> Alcotest.(check int64) hex expected decoded
632632+ | Error e ->
633633+ Alcotest.fail (Printf.sprintf "%s: %s" hex (Cbort.Error.to_string e)))
634634+ tests
576635577636let test_decode_rfc_strings () =
578578- let tests = [
579579- ("60", "");
580580- ("6161", "a");
581581- ("6449455446", "IETF");
582582- ("62225c", "\"\\");
583583- ("62c3bc", "\xc3\xbc"); (* ü *)
584584- ("63e6b0b4", "\xe6\xb0\xb4"); (* 水 *)
585585- ] in
586586- List.iter (fun (hex, expected) ->
587587- let bytes = hex_to_bytes hex in
588588- match Cbort.decode_string Cbort.string bytes with
589589- | Ok decoded -> Alcotest.(check string) hex expected decoded
590590- | Error e -> Alcotest.fail (Printf.sprintf "%s: %s" hex (Cbort.Error.to_string e))
591591- ) tests
637637+ let tests =
638638+ [
639639+ ("60", "");
640640+ ("6161", "a");
641641+ ("6449455446", "IETF");
642642+ ("62225c", "\"\\");
643643+ ("62c3bc", "\xc3\xbc");
644644+ (* ü *)
645645+ ("63e6b0b4", "\xe6\xb0\xb4");
646646+ (* 水 *)
647647+ ]
648648+ in
649649+ List.iter
650650+ (fun (hex, expected) ->
651651+ let bytes = hex_to_bytes hex in
652652+ match Cbort.decode_string Cbort.string bytes with
653653+ | Ok decoded -> Alcotest.(check string) hex expected decoded
654654+ | Error e ->
655655+ Alcotest.fail (Printf.sprintf "%s: %s" hex (Cbort.Error.to_string e)))
656656+ tests
592657593658let test_decode_rfc_arrays () =
594659 let int_list = Cbort.array Cbort.int in
595595- let tests = [
596596- ("80", []);
597597- ("83010203", [1; 2; 3]);
598598- ] in
599599- List.iter (fun (hex, expected) ->
600600- let bytes = hex_to_bytes hex in
601601- match Cbort.decode_string int_list bytes with
602602- | Ok decoded -> Alcotest.(check (list int)) hex expected decoded
603603- | Error e -> Alcotest.fail (Printf.sprintf "%s: %s" hex (Cbort.Error.to_string e))
604604- ) tests
660660+ let tests = [ ("80", []); ("83010203", [ 1; 2; 3 ]) ] in
661661+ List.iter
662662+ (fun (hex, expected) ->
663663+ let bytes = hex_to_bytes hex in
664664+ match Cbort.decode_string int_list bytes with
665665+ | Ok decoded -> Alcotest.(check (list int)) hex expected decoded
666666+ | Error e ->
667667+ Alcotest.fail (Printf.sprintf "%s: %s" hex (Cbort.Error.to_string e)))
668668+ tests
605669606670let test_decode_rfc_booleans () =
607607- let tests = [
608608- ("f4", false);
609609- ("f5", true);
610610- ] in
611611- List.iter (fun (hex, expected) ->
612612- let bytes = hex_to_bytes hex in
613613- match Cbort.decode_string Cbort.bool bytes with
614614- | Ok decoded -> Alcotest.(check bool) hex expected decoded
615615- | Error e -> Alcotest.fail (Printf.sprintf "%s: %s" hex (Cbort.Error.to_string e))
616616- ) tests
671671+ let tests = [ ("f4", false); ("f5", true) ] in
672672+ List.iter
673673+ (fun (hex, expected) ->
674674+ let bytes = hex_to_bytes hex in
675675+ match Cbort.decode_string Cbort.bool bytes with
676676+ | Ok decoded -> Alcotest.(check bool) hex expected decoded
677677+ | Error e ->
678678+ Alcotest.fail (Printf.sprintf "%s: %s" hex (Cbort.Error.to_string e)))
679679+ tests
617680618681let test_decode_rfc_null () =
619682 let bytes = hex_to_bytes "f6" in
···625688626689let test_decode_type_mismatch () =
627690 (* Try to decode an integer as a string *)
628628- let bytes = hex_to_bytes "01" in (* integer 1 *)
691691+ let bytes = hex_to_bytes "01" in
692692+ (* integer 1 *)
629693 match Cbort.decode_string Cbort.string bytes with
630694 | Ok _ -> Alcotest.fail "Expected type mismatch error"
631695 | Error e ->
632696 let msg = Cbort.Error.to_string e in
633633- Alcotest.(check bool) "error contains type info" true (String.length msg > 0)
697697+ Alcotest.(check bool)
698698+ "error contains type info" true
699699+ (String.length msg > 0)
634700635701let test_decode_truncated () =
636702 (* Truncated integer (header says 4 bytes follow but only 2 provided) *)
···642708(* ============= Test Runner ============= *)
643709644710let () =
645645- Alcotest.run "Cbort" [
646646- (* Low-level encoding tests *)
647647- "Unsigned Integers (RFC 8949)", [
648648- Alcotest.test_case "0" `Quick test_uint_0;
649649- Alcotest.test_case "1" `Quick test_uint_1;
650650- Alcotest.test_case "10" `Quick test_uint_10;
651651- Alcotest.test_case "23" `Quick test_uint_23;
652652- Alcotest.test_case "24" `Quick test_uint_24;
653653- Alcotest.test_case "25" `Quick test_uint_25;
654654- Alcotest.test_case "100" `Quick test_uint_100;
655655- Alcotest.test_case "1000" `Quick test_uint_1000;
656656- Alcotest.test_case "1000000" `Quick test_uint_1000000;
657657- Alcotest.test_case "1000000000000" `Quick test_uint_1000000000000;
658658- ];
659659- "Negative Integers (RFC 8949)", [
660660- Alcotest.test_case "-1" `Quick test_nint_minus1;
661661- Alcotest.test_case "-10" `Quick test_nint_minus10;
662662- Alcotest.test_case "-100" `Quick test_nint_minus100;
663663- Alcotest.test_case "-1000" `Quick test_nint_minus1000;
664664- ];
665665- "Booleans and Null (RFC 8949)", [
666666- Alcotest.test_case "false" `Quick test_false;
667667- Alcotest.test_case "true" `Quick test_true;
668668- Alcotest.test_case "null" `Quick test_null;
669669- ];
670670- "Floats (RFC 8949)", [
671671- Alcotest.test_case "1.0" `Quick test_float_1_0;
672672- Alcotest.test_case "1.1" `Quick test_float_1_1;
673673- Alcotest.test_case "-4.1" `Quick test_float_neg_4_1;
674674- Alcotest.test_case "1.0e+300" `Quick test_float_1e300;
675675- Alcotest.test_case "Infinity" `Quick test_float_infinity;
676676- Alcotest.test_case "-Infinity" `Quick test_float_neg_infinity;
677677- Alcotest.test_case "NaN" `Quick test_float_nan;
678678- ];
679679- "Text Strings (RFC 8949)", [
680680- Alcotest.test_case "empty" `Quick test_text_empty;
681681- Alcotest.test_case "a" `Quick test_text_a;
682682- Alcotest.test_case "IETF" `Quick test_text_ietf;
683683- Alcotest.test_case "quote_backslash" `Quick test_text_quote_backslash;
684684- Alcotest.test_case "utf8_umlaut" `Quick test_text_utf8_umlaut;
685685- Alcotest.test_case "utf8_water" `Quick test_text_utf8_water;
686686- Alcotest.test_case "utf8_emoji" `Quick test_text_utf8_emoji;
687687- ];
688688- "Byte Strings (RFC 8949)", [
689689- Alcotest.test_case "empty" `Quick test_bytes_empty;
690690- Alcotest.test_case "01020304" `Quick test_bytes_01020304;
691691- ];
692692- "Arrays (RFC 8949)", [
693693- Alcotest.test_case "empty" `Quick test_array_empty;
694694- Alcotest.test_case "[1,2,3]" `Quick test_array_123;
695695- Alcotest.test_case "nested" `Quick test_array_nested;
696696- Alcotest.test_case "25_items" `Quick test_array_25_items;
697697- ];
698698- "Maps (RFC 8949)", [
699699- Alcotest.test_case "empty" `Quick test_map_empty;
700700- Alcotest.test_case "int_keys" `Quick test_map_int_keys;
701701- Alcotest.test_case "string_keys" `Quick test_map_string_keys;
702702- Alcotest.test_case "mixed" `Quick test_mixed_array_map;
703703- Alcotest.test_case "5_pairs" `Quick test_map_5_pairs;
704704- ];
705705- "Tags (RFC 8949)", [
706706- Alcotest.test_case "epoch_timestamp" `Quick test_tag_epoch_timestamp;
707707- ];
708708- "Constants", [
709709- Alcotest.test_case "major_types" `Quick test_major_type_constants;
710710- Alcotest.test_case "simple_values" `Quick test_simple_value_constants;
711711- Alcotest.test_case "additional_info" `Quick test_additional_info_constants;
712712- ];
713713- (* High-level codec roundtrip tests *)
714714- "Codec Roundtrip", [
715715- Alcotest.test_case "int" `Quick test_codec_int_roundtrip;
716716- Alcotest.test_case "int64" `Quick test_codec_int64_roundtrip;
717717- Alcotest.test_case "bool" `Quick test_codec_bool_roundtrip;
718718- Alcotest.test_case "null" `Quick test_codec_null_roundtrip;
719719- Alcotest.test_case "float" `Quick test_codec_float_roundtrip;
720720- Alcotest.test_case "string" `Quick test_codec_string_roundtrip;
721721- Alcotest.test_case "bytes" `Quick test_codec_bytes_roundtrip;
722722- Alcotest.test_case "array" `Quick test_codec_array_roundtrip;
723723- Alcotest.test_case "nested_array" `Quick test_codec_nested_array;
724724- Alcotest.test_case "string_map" `Quick test_codec_string_map_roundtrip;
725725- Alcotest.test_case "int_map" `Quick test_codec_int_map_roundtrip;
726726- Alcotest.test_case "tuple2" `Quick test_codec_tuple2;
727727- Alcotest.test_case "tuple3" `Quick test_codec_tuple3;
728728- Alcotest.test_case "nullable" `Quick test_codec_nullable;
729729- ];
730730- "Obj Codec (String Keys)", [
731731- Alcotest.test_case "basic" `Quick test_obj_codec_basic;
732732- Alcotest.test_case "with_optional" `Quick test_obj_codec_with_optional;
733733- ];
734734- "Obj_int Codec (Integer Keys)", [
735735- Alcotest.test_case "full" `Quick test_obj_int_codec;
736736- Alcotest.test_case "partial" `Quick test_obj_int_partial;
737737- ];
738738- "Tag Codec", [
739739- Alcotest.test_case "tag" `Quick test_codec_tag;
740740- Alcotest.test_case "tag_opt" `Quick test_codec_tag_opt;
741741- ];
742742- "Decode RFC Vectors", [
743743- Alcotest.test_case "integers" `Quick test_decode_rfc_integers;
744744- Alcotest.test_case "strings" `Quick test_decode_rfc_strings;
745745- Alcotest.test_case "arrays" `Quick test_decode_rfc_arrays;
746746- Alcotest.test_case "booleans" `Quick test_decode_rfc_booleans;
747747- Alcotest.test_case "null" `Quick test_decode_rfc_null;
748748- ];
749749- "Error Handling", [
750750- Alcotest.test_case "type_mismatch" `Quick test_decode_type_mismatch;
751751- Alcotest.test_case "truncated" `Quick test_decode_truncated;
752752- ];
753753- ]
711711+ Alcotest.run "Cbort"
712712+ [
713713+ (* Low-level encoding tests *)
714714+ ( "Unsigned Integers (RFC 8949)",
715715+ [
716716+ Alcotest.test_case "0" `Quick test_uint_0;
717717+ Alcotest.test_case "1" `Quick test_uint_1;
718718+ Alcotest.test_case "10" `Quick test_uint_10;
719719+ Alcotest.test_case "23" `Quick test_uint_23;
720720+ Alcotest.test_case "24" `Quick test_uint_24;
721721+ Alcotest.test_case "25" `Quick test_uint_25;
722722+ Alcotest.test_case "100" `Quick test_uint_100;
723723+ Alcotest.test_case "1000" `Quick test_uint_1000;
724724+ Alcotest.test_case "1000000" `Quick test_uint_1000000;
725725+ Alcotest.test_case "1000000000000" `Quick test_uint_1000000000000;
726726+ ] );
727727+ ( "Negative Integers (RFC 8949)",
728728+ [
729729+ Alcotest.test_case "-1" `Quick test_nint_minus1;
730730+ Alcotest.test_case "-10" `Quick test_nint_minus10;
731731+ Alcotest.test_case "-100" `Quick test_nint_minus100;
732732+ Alcotest.test_case "-1000" `Quick test_nint_minus1000;
733733+ ] );
734734+ ( "Booleans and Null (RFC 8949)",
735735+ [
736736+ Alcotest.test_case "false" `Quick test_false;
737737+ Alcotest.test_case "true" `Quick test_true;
738738+ Alcotest.test_case "null" `Quick test_null;
739739+ ] );
740740+ ( "Floats (RFC 8949)",
741741+ [
742742+ Alcotest.test_case "1.0" `Quick test_float_1_0;
743743+ Alcotest.test_case "1.1" `Quick test_float_1_1;
744744+ Alcotest.test_case "-4.1" `Quick test_float_neg_4_1;
745745+ Alcotest.test_case "1.0e+300" `Quick test_float_1e300;
746746+ Alcotest.test_case "Infinity" `Quick test_float_infinity;
747747+ Alcotest.test_case "-Infinity" `Quick test_float_neg_infinity;
748748+ Alcotest.test_case "NaN" `Quick test_float_nan;
749749+ ] );
750750+ ( "Text Strings (RFC 8949)",
751751+ [
752752+ Alcotest.test_case "empty" `Quick test_text_empty;
753753+ Alcotest.test_case "a" `Quick test_text_a;
754754+ Alcotest.test_case "IETF" `Quick test_text_ietf;
755755+ Alcotest.test_case "quote_backslash" `Quick test_text_quote_backslash;
756756+ Alcotest.test_case "utf8_umlaut" `Quick test_text_utf8_umlaut;
757757+ Alcotest.test_case "utf8_water" `Quick test_text_utf8_water;
758758+ Alcotest.test_case "utf8_emoji" `Quick test_text_utf8_emoji;
759759+ ] );
760760+ ( "Byte Strings (RFC 8949)",
761761+ [
762762+ Alcotest.test_case "empty" `Quick test_bytes_empty;
763763+ Alcotest.test_case "01020304" `Quick test_bytes_01020304;
764764+ ] );
765765+ ( "Arrays (RFC 8949)",
766766+ [
767767+ Alcotest.test_case "empty" `Quick test_array_empty;
768768+ Alcotest.test_case "[1,2,3]" `Quick test_array_123;
769769+ Alcotest.test_case "nested" `Quick test_array_nested;
770770+ Alcotest.test_case "25_items" `Quick test_array_25_items;
771771+ ] );
772772+ ( "Maps (RFC 8949)",
773773+ [
774774+ Alcotest.test_case "empty" `Quick test_map_empty;
775775+ Alcotest.test_case "int_keys" `Quick test_map_int_keys;
776776+ Alcotest.test_case "string_keys" `Quick test_map_string_keys;
777777+ Alcotest.test_case "mixed" `Quick test_mixed_array_map;
778778+ Alcotest.test_case "5_pairs" `Quick test_map_5_pairs;
779779+ ] );
780780+ ( "Tags (RFC 8949)",
781781+ [ Alcotest.test_case "epoch_timestamp" `Quick test_tag_epoch_timestamp ]
782782+ );
783783+ ( "Constants",
784784+ [
785785+ Alcotest.test_case "major_types" `Quick test_major_type_constants;
786786+ Alcotest.test_case "simple_values" `Quick test_simple_value_constants;
787787+ Alcotest.test_case "additional_info" `Quick
788788+ test_additional_info_constants;
789789+ ] );
790790+ (* High-level codec roundtrip tests *)
791791+ ( "Codec Roundtrip",
792792+ [
793793+ Alcotest.test_case "int" `Quick test_codec_int_roundtrip;
794794+ Alcotest.test_case "int64" `Quick test_codec_int64_roundtrip;
795795+ Alcotest.test_case "bool" `Quick test_codec_bool_roundtrip;
796796+ Alcotest.test_case "null" `Quick test_codec_null_roundtrip;
797797+ Alcotest.test_case "float" `Quick test_codec_float_roundtrip;
798798+ Alcotest.test_case "string" `Quick test_codec_string_roundtrip;
799799+ Alcotest.test_case "bytes" `Quick test_codec_bytes_roundtrip;
800800+ Alcotest.test_case "array" `Quick test_codec_array_roundtrip;
801801+ Alcotest.test_case "nested_array" `Quick test_codec_nested_array;
802802+ Alcotest.test_case "string_map" `Quick test_codec_string_map_roundtrip;
803803+ Alcotest.test_case "int_map" `Quick test_codec_int_map_roundtrip;
804804+ Alcotest.test_case "tuple2" `Quick test_codec_tuple2;
805805+ Alcotest.test_case "tuple3" `Quick test_codec_tuple3;
806806+ Alcotest.test_case "nullable" `Quick test_codec_nullable;
807807+ ] );
808808+ ( "Obj Codec (String Keys)",
809809+ [
810810+ Alcotest.test_case "basic" `Quick test_obj_codec_basic;
811811+ Alcotest.test_case "with_optional" `Quick test_obj_codec_with_optional;
812812+ ] );
813813+ ( "Obj_int Codec (Integer Keys)",
814814+ [
815815+ Alcotest.test_case "full" `Quick test_obj_int_codec;
816816+ Alcotest.test_case "partial" `Quick test_obj_int_partial;
817817+ ] );
818818+ ( "Tag Codec",
819819+ [
820820+ Alcotest.test_case "tag" `Quick test_codec_tag;
821821+ Alcotest.test_case "tag_opt" `Quick test_codec_tag_opt;
822822+ ] );
823823+ ( "Decode RFC Vectors",
824824+ [
825825+ Alcotest.test_case "integers" `Quick test_decode_rfc_integers;
826826+ Alcotest.test_case "strings" `Quick test_decode_rfc_strings;
827827+ Alcotest.test_case "arrays" `Quick test_decode_rfc_arrays;
828828+ Alcotest.test_case "booleans" `Quick test_decode_rfc_booleans;
829829+ Alcotest.test_case "null" `Quick test_decode_rfc_null;
830830+ ] );
831831+ ( "Error Handling",
832832+ [
833833+ Alcotest.test_case "type_mismatch" `Quick test_decode_type_mismatch;
834834+ Alcotest.test_case "truncated" `Quick test_decode_truncated;
835835+ ] );
836836+ ]
+415-280
ocaml-jsonwt/test/test_cwt.ml
···11(** CWT Library Tests
2233- Tests derived from RFC 8392 (CBOR Web Token) and
44- RFC 9052/9053 (COSE) specifications. *)
33+ Tests derived from RFC 8392 (CBOR Web Token) and RFC 9052/9053 (COSE)
44+ specifications. *)
5566module Cwt = Jsonwt.Cwt
77···18181919(* RFC 8392 Appendix A.1: Example CWT Claims Set *)
2020let rfc_claims_hex =
2121- "a70175636f61703a2f2f61732e6578616d706c652e636f6d02656572696b7703" ^
2222- "7818636f61703a2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0" ^
2323- "051a5610d9f0061a5610d9f007420b71"
2121+ "a70175636f61703a2f2f61732e6578616d706c652e636f6d02656572696b7703"
2222+ ^ "7818636f61703a2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0"
2323+ ^ "051a5610d9f0061a5610d9f007420b71"
24242525(* RFC 8392 Appendix A.2.2: 256-Bit Symmetric Key *)
2626let rfc_256bit_key_hex =
2727- "a4205820403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d" ^
2828- "795693880104024c53796d6d6574726963323536030a"
2727+ "a4205820403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d"
2828+ ^ "795693880104024c53796d6d6574726963323536030a"
29293030(* Just the raw key bytes for HMAC *)
3131let rfc_256bit_key_bytes =
3232- hex_to_bytes "403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388"
3232+ hex_to_bytes
3333+ "403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388"
33343435(* RFC 8392 Appendix A.2.3: ECDSA P-256 Key *)
3535-let rfc_p256_d = hex_to_bytes "6c1382765aec5358f117733d281c1c7bdc39884d04a45a1e6c67c858bc206c19"
3636-let rfc_p256_x = hex_to_bytes "143329cce7868e416927599cf65a34f3ce2ffda55a7eca69ed8919a394d42f0f"
3737-let rfc_p256_y = hex_to_bytes "60f7f1a780d8a783bfb7a2dd6b2796e8128dbbcef9d3d168db9529971a36e7b9"
3636+let rfc_p256_d =
3737+ hex_to_bytes
3838+ "6c1382765aec5358f117733d281c1c7bdc39884d04a45a1e6c67c858bc206c19"
3939+4040+let rfc_p256_x =
4141+ hex_to_bytes
4242+ "143329cce7868e416927599cf65a34f3ce2ffda55a7eca69ed8919a394d42f0f"
4343+4444+let rfc_p256_y =
4545+ hex_to_bytes
4646+ "60f7f1a780d8a783bfb7a2dd6b2796e8128dbbcef9d3d168db9529971a36e7b9"
38473948(* RFC 8392 Appendix A.3: Signed CWT *)
4049let rfc_signed_cwt_hex =
4141- "d28443a10126a104524173796d6d657472696345434453413235365850a70175" ^
4242- "636f61703a2f2f61732e6578616d706c652e636f6d02656572696b77037818636f" ^
4343- "61703a2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0051a5610d" ^
4444- "9f0061a5610d9f007420b7158405427c1ff28d23fbad1f29c4c7c6a555e601d6f" ^
4545- "a29f9179bc3d7438bacaca5acd08c8d4d4f96131680c429a01f85951ecee743a5" ^
4646- "2b9b63632c57209120e1c9e30"
5050+ "d28443a10126a104524173796d6d657472696345434453413235365850a70175"
5151+ ^ "636f61703a2f2f61732e6578616d706c652e636f6d02656572696b77037818636f"
5252+ ^ "61703a2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0051a5610d"
5353+ ^ "9f0061a5610d9f007420b7158405427c1ff28d23fbad1f29c4c7c6a555e601d6f"
5454+ ^ "a29f9179bc3d7438bacaca5acd08c8d4d4f96131680c429a01f85951ecee743a5"
5555+ ^ "2b9b63632c57209120e1c9e30"
47564857(* RFC 8392 Appendix A.4: MACed CWT with CWT tag *)
4958let rfc_maced_cwt_hex =
5050- "d83dd18443a10104a1044c53796d6d65747269633235365850a70175636f6170" ^
5151- "3a2f2f61732e6578616d706c652e636f6d02656572696b77037818636f61703a" ^
5252- "2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0051a5610d9f006" ^
5353- "1a5610d9f007420b7148093101ef6d789200"
5959+ "d83dd18443a10104a1044c53796d6d65747269633235365850a70175636f6170"
6060+ ^ "3a2f2f61732e6578616d706c652e636f6d02656572696b77037818636f61703a"
6161+ ^ "2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0051a5610d9f006"
6262+ ^ "1a5610d9f007420b7148093101ef6d789200"
54635564(* ============= COSE Algorithm Tests ============= *)
56655766let test_algorithm_roundtrip () =
5867 let open Cwt.Algorithm in
5959- let algs = [ ES256; ES384; ES512; EdDSA; HMAC_256_64; HMAC_256; HMAC_384; HMAC_512 ] in
6060- List.iter (fun alg ->
6161- let cose_int = to_cose_int alg in
6262- match of_cose_int cose_int with
6363- | Ok alg' ->
6464- Alcotest.(check int) "roundtrip" cose_int (to_cose_int alg')
6565- | Error e ->
6666- Alcotest.fail (Cwt.error_to_string e)
6767- ) algs
6868+ let algs =
6969+ [ ES256; ES384; ES512; EdDSA; HMAC_256_64; HMAC_256; HMAC_384; HMAC_512 ]
7070+ in
7171+ List.iter
7272+ (fun alg ->
7373+ let cose_int = to_cose_int alg in
7474+ match of_cose_int cose_int with
7575+ | Ok alg' -> Alcotest.(check int) "roundtrip" cose_int (to_cose_int alg')
7676+ | Error e -> Alcotest.fail (Cwt.error_to_string e))
7777+ algs
68786979let test_algorithm_cose_values () =
7080 let open Cwt.Algorithm in
···88988999let test_cose_key_symmetric () =
90100 let key = Cwt.Cose_key.symmetric "my-secret-key-32-bytes-long!!!!!" in
9191- Alcotest.(check bool) "kty is Symmetric" true (Cwt.Cose_key.kty key = Cwt.Cose_key.Symmetric)
101101+ Alcotest.(check bool)
102102+ "kty is Symmetric" true
103103+ (Cwt.Cose_key.kty key = Cwt.Cose_key.Symmetric)
9210493105let test_cose_key_ed25519 () =
94106 let pub = String.make 32 '\x00' in
95107 let key = Cwt.Cose_key.ed25519_pub pub in
9696- Alcotest.(check bool) "kty is Okp" true (Cwt.Cose_key.kty key = Cwt.Cose_key.Okp);
9797- Alcotest.(check bool) "alg is EdDSA" true (Cwt.Cose_key.alg key = Some Cwt.Algorithm.EdDSA)
108108+ Alcotest.(check bool)
109109+ "kty is Okp" true
110110+ (Cwt.Cose_key.kty key = Cwt.Cose_key.Okp);
111111+ Alcotest.(check bool)
112112+ "alg is EdDSA" true
113113+ (Cwt.Cose_key.alg key = Some Cwt.Algorithm.EdDSA)
9811499115let test_cose_key_p256 () =
100116 let x = String.make 32 '\x00' in
101117 let y = String.make 32 '\x00' in
102118 let key = Cwt.Cose_key.p256_pub ~x ~y in
103103- Alcotest.(check bool) "kty is Ec2" true (Cwt.Cose_key.kty key = Cwt.Cose_key.Ec2);
104104- Alcotest.(check bool) "alg is ES256" true (Cwt.Cose_key.alg key = Some Cwt.Algorithm.ES256)
119119+ Alcotest.(check bool)
120120+ "kty is Ec2" true
121121+ (Cwt.Cose_key.kty key = Cwt.Cose_key.Ec2);
122122+ Alcotest.(check bool)
123123+ "alg is ES256" true
124124+ (Cwt.Cose_key.alg key = Some Cwt.Algorithm.ES256)
105125106126let test_cose_key_with_kid () =
107127 let key = Cwt.Cose_key.symmetric "secret" in
108128 Alcotest.(check (option string)) "no kid" None (Cwt.Cose_key.kid key);
109129 let key' = Cwt.Cose_key.with_kid "my-key-id" key in
110110- Alcotest.(check (option string)) "has kid" (Some "my-key-id") (Cwt.Cose_key.kid key')
130130+ Alcotest.(check (option string))
131131+ "has kid" (Some "my-key-id") (Cwt.Cose_key.kid key')
111132112133(* ============= Claims Tests ============= *)
113134···118139 |> Cwt.Claims.set_sub "test-subject"
119140 |> Cwt.Claims.build
120141 in
121121- Alcotest.(check (option string)) "iss" (Some "test-issuer") (Cwt.Claims.iss claims);
122122- Alcotest.(check (option string)) "sub" (Some "test-subject") (Cwt.Claims.sub claims)
142142+ Alcotest.(check (option string))
143143+ "iss" (Some "test-issuer") (Cwt.Claims.iss claims);
144144+ Alcotest.(check (option string))
145145+ "sub" (Some "test-subject") (Cwt.Claims.sub claims)
123146124147let test_claims_with_timestamps () =
125125- let now = Ptime.of_float_s 1443944944. |> Option.get in (* RFC 8392 example iat *)
126126- let exp = Ptime.of_float_s 1444064944. |> Option.get in (* RFC 8392 example exp *)
148148+ let now = Ptime.of_float_s 1443944944. |> Option.get in
149149+ (* RFC 8392 example iat *)
150150+ let exp = Ptime.of_float_s 1444064944. |> Option.get in
151151+ (* RFC 8392 example exp *)
127152 let claims =
128128- Cwt.Claims.empty
129129- |> Cwt.Claims.set_iat now
130130- |> Cwt.Claims.set_nbf now
131131- |> Cwt.Claims.set_exp exp
132132- |> Cwt.Claims.build
153153+ Cwt.Claims.empty |> Cwt.Claims.set_iat now |> Cwt.Claims.set_nbf now
154154+ |> Cwt.Claims.set_exp exp |> Cwt.Claims.build
133155 in
134134- Alcotest.(check (option bool)) "has exp" (Some true) (Option.map (fun _ -> true) (Cwt.Claims.exp claims));
135135- Alcotest.(check (option bool)) "has iat" (Some true) (Option.map (fun _ -> true) (Cwt.Claims.iat claims));
136136- Alcotest.(check (option bool)) "has nbf" (Some true) (Option.map (fun _ -> true) (Cwt.Claims.nbf claims))
156156+ Alcotest.(check (option bool))
157157+ "has exp" (Some true)
158158+ (Option.map (fun _ -> true) (Cwt.Claims.exp claims));
159159+ Alcotest.(check (option bool))
160160+ "has iat" (Some true)
161161+ (Option.map (fun _ -> true) (Cwt.Claims.iat claims));
162162+ Alcotest.(check (option bool))
163163+ "has nbf" (Some true)
164164+ (Option.map (fun _ -> true) (Cwt.Claims.nbf claims))
137165138166let test_claims_audience_single () =
139167 let claims =
···141169 |> Cwt.Claims.set_aud [ "coap://light.example.com" ]
142170 |> Cwt.Claims.build
143171 in
144144- Alcotest.(check (list string)) "aud" [ "coap://light.example.com" ] (Cwt.Claims.aud claims)
172172+ Alcotest.(check (list string))
173173+ "aud"
174174+ [ "coap://light.example.com" ]
175175+ (Cwt.Claims.aud claims)
145176146177let test_claims_audience_multiple () =
147178 let claims =
···149180 |> Cwt.Claims.set_aud [ "aud1"; "aud2"; "aud3" ]
150181 |> Cwt.Claims.build
151182 in
152152- Alcotest.(check (list string)) "aud" [ "aud1"; "aud2"; "aud3" ] (Cwt.Claims.aud claims)
183183+ Alcotest.(check (list string))
184184+ "aud" [ "aud1"; "aud2"; "aud3" ] (Cwt.Claims.aud claims)
153185154186let test_claims_cti () =
155187 let claims =
156156- Cwt.Claims.empty
157157- |> Cwt.Claims.set_cti "\x0b\x71"
158158- |> Cwt.Claims.build
188188+ Cwt.Claims.empty |> Cwt.Claims.set_cti "\x0b\x71" |> Cwt.Claims.build
159189 in
160160- Alcotest.(check (option string)) "cti" (Some "\x0b\x71") (Cwt.Claims.cti claims)
190190+ Alcotest.(check (option string))
191191+ "cti" (Some "\x0b\x71") (Cwt.Claims.cti claims)
161192162193let test_claims_to_cbor () =
163194 (* Build claims like RFC 8392 example *)
···169200 |> Cwt.Claims.set_iss "coap://as.example.com"
170201 |> Cwt.Claims.set_sub "erikw"
171202 |> Cwt.Claims.set_aud [ "coap://light.example.com" ]
172172- |> Cwt.Claims.set_exp exp
173173- |> Cwt.Claims.set_nbf nbf
203203+ |> Cwt.Claims.set_exp exp |> Cwt.Claims.set_nbf nbf
174204 |> Cwt.Claims.set_iat iat
175205 |> Cwt.Claims.set_cti "\x0b\x71"
176206 |> Cwt.Claims.build
···193223 let key = Cwt.Cose_key.symmetric rfc_256bit_key_bytes in
194224 match Cwt.create ~algorithm:Cwt.Algorithm.HMAC_256 ~claims ~key with
195225 | Ok cwt ->
196196- Alcotest.(check (option string)) "iss" (Some "test-issuer") (Cwt.Claims.iss (Cwt.claims cwt));
197197- Alcotest.(check bool) "has algorithm" true (Option.is_some (Cwt.algorithm cwt));
226226+ Alcotest.(check (option string))
227227+ "iss" (Some "test-issuer")
228228+ (Cwt.Claims.iss (Cwt.claims cwt));
229229+ Alcotest.(check bool)
230230+ "has algorithm" true
231231+ (Option.is_some (Cwt.algorithm cwt));
198232 let encoded = Cwt.encode cwt in
199233 Alcotest.(check bool) "non-empty encoding" true (String.length encoded > 0)
200234 | Error e ->
201201- Alcotest.fail (Printf.sprintf "CWT creation failed: %s" (Cwt.error_to_string e))
235235+ Alcotest.fail
236236+ (Printf.sprintf "CWT creation failed: %s" (Cwt.error_to_string e))
202237203238let test_create_hmac_256_64_cwt () =
204239 let claims =
205205- Cwt.Claims.empty
206206- |> Cwt.Claims.set_iss "test-issuer"
207207- |> Cwt.Claims.build
240240+ Cwt.Claims.empty |> Cwt.Claims.set_iss "test-issuer" |> Cwt.Claims.build
208241 in
209242 let key = Cwt.Cose_key.symmetric rfc_256bit_key_bytes in
210243 match Cwt.create ~algorithm:Cwt.Algorithm.HMAC_256_64 ~claims ~key with
211244 | Ok cwt ->
212212- Alcotest.(check bool) "alg is HMAC_256_64" true
245245+ Alcotest.(check bool)
246246+ "alg is HMAC_256_64" true
213247 (Cwt.algorithm cwt = Some Cwt.Algorithm.HMAC_256_64)
214248 | Error e ->
215215- Alcotest.fail (Printf.sprintf "CWT creation failed: %s" (Cwt.error_to_string e))
249249+ Alcotest.fail
250250+ (Printf.sprintf "CWT creation failed: %s" (Cwt.error_to_string e))
216251217252let test_create_es256_cwt () =
218253 let claims =
219219- Cwt.Claims.empty
220220- |> Cwt.Claims.set_iss "test-issuer"
221221- |> Cwt.Claims.build
254254+ Cwt.Claims.empty |> Cwt.Claims.set_iss "test-issuer" |> Cwt.Claims.build
222255 in
223256 let key = Cwt.Cose_key.p256_priv ~x:rfc_p256_x ~y:rfc_p256_y ~d:rfc_p256_d in
224257 match Cwt.create ~algorithm:Cwt.Algorithm.ES256 ~claims ~key with
225258 | Ok cwt ->
226226- Alcotest.(check bool) "alg is ES256" true (Cwt.algorithm cwt = Some Cwt.Algorithm.ES256);
259259+ Alcotest.(check bool)
260260+ "alg is ES256" true
261261+ (Cwt.algorithm cwt = Some Cwt.Algorithm.ES256);
227262 let encoded = Cwt.encode cwt in
228263 (* Should start with COSE_Sign1 tag (0xd2 = 18) *)
229229- Alcotest.(check int) "COSE_Sign1 tag" 0xd2 (Char.code (String.get encoded 0))
264264+ Alcotest.(check int)
265265+ "COSE_Sign1 tag" 0xd2
266266+ (Char.code (String.get encoded 0))
230267 | Error e ->
231231- Alcotest.fail (Printf.sprintf "CWT creation failed: %s" (Cwt.error_to_string e))
268268+ Alcotest.fail
269269+ (Printf.sprintf "CWT creation failed: %s" (Cwt.error_to_string e))
232270233271let test_create_key_mismatch () =
234272 let claims =
235235- Cwt.Claims.empty
236236- |> Cwt.Claims.set_iss "test"
237237- |> Cwt.Claims.build
273273+ Cwt.Claims.empty |> Cwt.Claims.set_iss "test" |> Cwt.Claims.build
238274 in
239275 (* Symmetric key with ES256 algorithm *)
240276 let key = Cwt.Cose_key.symmetric "secret" in
241277 match Cwt.create ~algorithm:Cwt.Algorithm.ES256 ~claims ~key with
242278 | Error (Cwt.Key_type_mismatch _) -> ()
243243- | Error e -> Alcotest.fail (Printf.sprintf "Expected Key_type_mismatch, got: %s" (Cwt.error_to_string e))
279279+ | Error e ->
280280+ Alcotest.fail
281281+ (Printf.sprintf "Expected Key_type_mismatch, got: %s"
282282+ (Cwt.error_to_string e))
244283 | Ok _ -> Alcotest.fail "Expected key type mismatch error"
245284246285(* ============= Claims Validation Tests ============= *)
247286248287let test_validate_expired_token () =
249288 let exp = Ptime.of_float_s 1300819380. |> Option.get in
250250- let now = Ptime.of_float_s 1400000000. |> Option.get in (* After exp *)
251251- let claims =
252252- Cwt.Claims.empty
253253- |> Cwt.Claims.set_exp exp
254254- |> Cwt.Claims.build
255255- in
289289+ let now = Ptime.of_float_s 1400000000. |> Option.get in
290290+ (* After exp *)
291291+ let claims = Cwt.Claims.empty |> Cwt.Claims.set_exp exp |> Cwt.Claims.build in
256292 let key = Cwt.Cose_key.symmetric rfc_256bit_key_bytes in
257293 match Cwt.create ~algorithm:Cwt.Algorithm.HMAC_256 ~claims ~key with
258258- | Ok cwt ->
259259- begin match Cwt.validate ~now cwt with
294294+ | Ok cwt -> begin
295295+ match Cwt.validate ~now cwt with
260296 | Error Cwt.Token_expired -> ()
261261- | Error e -> Alcotest.fail (Printf.sprintf "Expected Token_expired, got: %s" (Cwt.error_to_string e))
297297+ | Error e ->
298298+ Alcotest.fail
299299+ (Printf.sprintf "Expected Token_expired, got: %s"
300300+ (Cwt.error_to_string e))
262301 | Ok () -> Alcotest.fail "Expected Token_expired error"
263263- end
302302+ end
264303 | Error e -> Alcotest.fail (Cwt.error_to_string e)
265304266305let test_validate_not_yet_valid_token () =
267306 let nbf = Ptime.of_float_s 1500000000. |> Option.get in
268268- let now = Ptime.of_float_s 1400000000. |> Option.get in (* Before nbf *)
269269- let claims =
270270- Cwt.Claims.empty
271271- |> Cwt.Claims.set_nbf nbf
272272- |> Cwt.Claims.build
273273- in
307307+ let now = Ptime.of_float_s 1400000000. |> Option.get in
308308+ (* Before nbf *)
309309+ let claims = Cwt.Claims.empty |> Cwt.Claims.set_nbf nbf |> Cwt.Claims.build in
274310 let key = Cwt.Cose_key.symmetric rfc_256bit_key_bytes in
275311 match Cwt.create ~algorithm:Cwt.Algorithm.HMAC_256 ~claims ~key with
276276- | Ok cwt ->
277277- begin match Cwt.validate ~now cwt with
312312+ | Ok cwt -> begin
313313+ match Cwt.validate ~now cwt with
278314 | Error Cwt.Token_not_yet_valid -> ()
279279- | Error e -> Alcotest.fail (Printf.sprintf "Expected Token_not_yet_valid, got: %s" (Cwt.error_to_string e))
315315+ | Error e ->
316316+ Alcotest.fail
317317+ (Printf.sprintf "Expected Token_not_yet_valid, got: %s"
318318+ (Cwt.error_to_string e))
280319 | Ok () -> Alcotest.fail "Expected Token_not_yet_valid error"
281281- end
320320+ end
282321 | Error e -> Alcotest.fail (Cwt.error_to_string e)
283322284323let test_validate_with_leeway () =
285324 let exp = Ptime.of_float_s 1300819380. |> Option.get in
286286- let now = Ptime.of_float_s 1300819390. |> Option.get in (* 10 seconds after exp *)
287287- let leeway = Ptime.Span.of_int_s 60 in (* 60 second leeway *)
288288- let claims =
289289- Cwt.Claims.empty
290290- |> Cwt.Claims.set_exp exp
291291- |> Cwt.Claims.build
292292- in
325325+ let now = Ptime.of_float_s 1300819390. |> Option.get in
326326+ (* 10 seconds after exp *)
327327+ let leeway = Ptime.Span.of_int_s 60 in
328328+ (* 60 second leeway *)
329329+ let claims = Cwt.Claims.empty |> Cwt.Claims.set_exp exp |> Cwt.Claims.build in
293330 let key = Cwt.Cose_key.symmetric rfc_256bit_key_bytes in
294331 match Cwt.create ~algorithm:Cwt.Algorithm.HMAC_256 ~claims ~key with
295295- | Ok cwt ->
296296- begin match Cwt.validate ~now ~leeway cwt with
332332+ | Ok cwt -> begin
333333+ match Cwt.validate ~now ~leeway cwt with
297334 | Ok () -> ()
298298- | Error e -> Alcotest.fail (Printf.sprintf "Expected validation to pass with leeway, got: %s" (Cwt.error_to_string e))
299299- end
335335+ | Error e ->
336336+ Alcotest.fail
337337+ (Printf.sprintf "Expected validation to pass with leeway, got: %s"
338338+ (Cwt.error_to_string e))
339339+ end
300340 | Error e -> Alcotest.fail (Cwt.error_to_string e)
301341302342let test_validate_issuer_match () =
303343 let now = Ptime.of_float_s 1400000000. |> Option.get in
304344 let claims =
305305- Cwt.Claims.empty
306306- |> Cwt.Claims.set_iss "expected-issuer"
307307- |> Cwt.Claims.build
345345+ Cwt.Claims.empty |> Cwt.Claims.set_iss "expected-issuer" |> Cwt.Claims.build
308346 in
309347 let key = Cwt.Cose_key.symmetric rfc_256bit_key_bytes in
310348 match Cwt.create ~algorithm:Cwt.Algorithm.HMAC_256 ~claims ~key with
311311- | Ok cwt ->
312312- begin match Cwt.validate ~now ~iss:"expected-issuer" cwt with
349349+ | Ok cwt -> begin
350350+ match Cwt.validate ~now ~iss:"expected-issuer" cwt with
313351 | Ok () -> ()
314314- | Error e -> Alcotest.fail (Printf.sprintf "Expected validation to pass, got: %s" (Cwt.error_to_string e))
315315- end
352352+ | Error e ->
353353+ Alcotest.fail
354354+ (Printf.sprintf "Expected validation to pass, got: %s"
355355+ (Cwt.error_to_string e))
356356+ end
316357 | Error e -> Alcotest.fail (Cwt.error_to_string e)
317358318359let test_validate_issuer_mismatch () =
319360 let now = Ptime.of_float_s 1400000000. |> Option.get in
320361 let claims =
321321- Cwt.Claims.empty
322322- |> Cwt.Claims.set_iss "actual-issuer"
323323- |> Cwt.Claims.build
362362+ Cwt.Claims.empty |> Cwt.Claims.set_iss "actual-issuer" |> Cwt.Claims.build
324363 in
325364 let key = Cwt.Cose_key.symmetric rfc_256bit_key_bytes in
326365 match Cwt.create ~algorithm:Cwt.Algorithm.HMAC_256 ~claims ~key with
327327- | Ok cwt ->
328328- begin match Cwt.validate ~now ~iss:"expected-issuer" cwt with
366366+ | Ok cwt -> begin
367367+ match Cwt.validate ~now ~iss:"expected-issuer" cwt with
329368 | Error Cwt.Invalid_issuer -> ()
330330- | Error e -> Alcotest.fail (Printf.sprintf "Expected Invalid_issuer, got: %s" (Cwt.error_to_string e))
369369+ | Error e ->
370370+ Alcotest.fail
371371+ (Printf.sprintf "Expected Invalid_issuer, got: %s"
372372+ (Cwt.error_to_string e))
331373 | Ok () -> Alcotest.fail "Expected Invalid_issuer error"
332332- end
374374+ end
333375 | Error e -> Alcotest.fail (Cwt.error_to_string e)
334376335377let test_validate_audience_match () =
···341383 in
342384 let key = Cwt.Cose_key.symmetric rfc_256bit_key_bytes in
343385 match Cwt.create ~algorithm:Cwt.Algorithm.HMAC_256 ~claims ~key with
344344- | Ok cwt ->
345345- begin match Cwt.validate ~now ~aud:"my-app" cwt with
386386+ | Ok cwt -> begin
387387+ match Cwt.validate ~now ~aud:"my-app" cwt with
346388 | Ok () -> ()
347347- | Error e -> Alcotest.fail (Printf.sprintf "Expected validation to pass, got: %s" (Cwt.error_to_string e))
348348- end
389389+ | Error e ->
390390+ Alcotest.fail
391391+ (Printf.sprintf "Expected validation to pass, got: %s"
392392+ (Cwt.error_to_string e))
393393+ end
349394 | Error e -> Alcotest.fail (Cwt.error_to_string e)
350395351396let test_validate_audience_mismatch () =
···357402 in
358403 let key = Cwt.Cose_key.symmetric rfc_256bit_key_bytes in
359404 match Cwt.create ~algorithm:Cwt.Algorithm.HMAC_256 ~claims ~key with
360360- | Ok cwt ->
361361- begin match Cwt.validate ~now ~aud:"my-app" cwt with
405405+ | Ok cwt -> begin
406406+ match Cwt.validate ~now ~aud:"my-app" cwt with
362407 | Error Cwt.Invalid_audience -> ()
363363- | Error e -> Alcotest.fail (Printf.sprintf "Expected Invalid_audience, got: %s" (Cwt.error_to_string e))
408408+ | Error e ->
409409+ Alcotest.fail
410410+ (Printf.sprintf "Expected Invalid_audience, got: %s"
411411+ (Cwt.error_to_string e))
364412 | Ok () -> Alcotest.fail "Expected Invalid_audience error"
365365- end
413413+ end
366414 | Error e -> Alcotest.fail (Cwt.error_to_string e)
367415368416(* ============= Helper Function Tests ============= *)
369417370418let test_is_expired () =
371419 let exp = Ptime.of_float_s 1300819380. |> Option.get in
372372- let claims =
373373- Cwt.Claims.empty
374374- |> Cwt.Claims.set_exp exp
375375- |> Cwt.Claims.build
376376- in
420420+ let claims = Cwt.Claims.empty |> Cwt.Claims.set_exp exp |> Cwt.Claims.build in
377421 let key = Cwt.Cose_key.symmetric rfc_256bit_key_bytes in
378422 match Cwt.create ~algorithm:Cwt.Algorithm.HMAC_256 ~claims ~key with
379423 | Ok cwt ->
380424 let now_before = Ptime.of_float_s 1300819370. |> Option.get in
381425 let now_after = Ptime.of_float_s 1300819390. |> Option.get in
382382- Alcotest.(check bool) "not expired before" false (Cwt.is_expired ~now:now_before cwt);
383383- Alcotest.(check bool) "expired after" true (Cwt.is_expired ~now:now_after cwt)
426426+ Alcotest.(check bool)
427427+ "not expired before" false
428428+ (Cwt.is_expired ~now:now_before cwt);
429429+ Alcotest.(check bool)
430430+ "expired after" true
431431+ (Cwt.is_expired ~now:now_after cwt)
384432 | Error e -> Alcotest.fail (Cwt.error_to_string e)
385433386434let test_time_to_expiry () =
387435 let exp = Ptime.of_float_s 1300819380. |> Option.get in
388388- let claims =
389389- Cwt.Claims.empty
390390- |> Cwt.Claims.set_exp exp
391391- |> Cwt.Claims.build
392392- in
436436+ let claims = Cwt.Claims.empty |> Cwt.Claims.set_exp exp |> Cwt.Claims.build in
393437 let key = Cwt.Cose_key.symmetric rfc_256bit_key_bytes in
394438 match Cwt.create ~algorithm:Cwt.Algorithm.HMAC_256 ~claims ~key with
395439 | Ok cwt ->
···398442 | Some span ->
399443 let seconds = Ptime.Span.to_float_s span |> int_of_float in
400444 Alcotest.(check int) "time to expiry" 10 seconds
401401- | None ->
402402- Alcotest.fail "Expected Some time to expiry"
445445+ | None -> Alcotest.fail "Expected Some time to expiry"
403446 end
404447 | Error e -> Alcotest.fail (Cwt.error_to_string e)
405448406449(* ============= Error Type Tests ============= *)
407450408451let test_error_to_string () =
409409- let errors = [
410410- (Cwt.Invalid_cbor "test", "Invalid CBOR: test");
411411- (Cwt.Invalid_cose "test", "Invalid COSE: test");
412412- (Cwt.Invalid_claims "test", "Invalid claims: test");
413413- (Cwt.Token_expired, "Token expired");
414414- (Cwt.Token_not_yet_valid, "Token not yet valid");
415415- (Cwt.Signature_mismatch, "Signature mismatch");
416416- ] in
417417- List.iter (fun (err, expected) ->
418418- let actual = Cwt.error_to_string err in
419419- Alcotest.(check string) "error string" expected actual
420420- ) errors
452452+ let errors =
453453+ [
454454+ (Cwt.Invalid_cbor "test", "Invalid CBOR: test");
455455+ (Cwt.Invalid_cose "test", "Invalid COSE: test");
456456+ (Cwt.Invalid_claims "test", "Invalid claims: test");
457457+ (Cwt.Token_expired, "Token expired");
458458+ (Cwt.Token_not_yet_valid, "Token not yet valid");
459459+ (Cwt.Signature_mismatch, "Signature mismatch");
460460+ ]
461461+ in
462462+ List.iter
463463+ (fun (err, expected) ->
464464+ let actual = Cwt.error_to_string err in
465465+ Alcotest.(check string) "error string" expected actual)
466466+ errors
421467422468(* ============= RFC 8392 Test Vector References ============= *)
423469···432478 |> Cwt.Claims.set_iss "coap://as.example.com"
433479 |> Cwt.Claims.set_sub "erikw"
434480 |> Cwt.Claims.set_aud [ "coap://light.example.com" ]
435435- |> Cwt.Claims.set_exp exp
436436- |> Cwt.Claims.set_nbf nbf
481481+ |> Cwt.Claims.set_exp exp |> Cwt.Claims.set_nbf nbf
437482 |> Cwt.Claims.set_iat iat
438483 |> Cwt.Claims.set_cti "\x0b\x71"
439484 |> Cwt.Claims.build
440485 in
441441- Alcotest.(check (option string)) "iss" (Some "coap://as.example.com") (Cwt.Claims.iss claims);
486486+ Alcotest.(check (option string))
487487+ "iss" (Some "coap://as.example.com") (Cwt.Claims.iss claims);
442488 Alcotest.(check (option string)) "sub" (Some "erikw") (Cwt.Claims.sub claims);
443443- Alcotest.(check (list string)) "aud" ["coap://light.example.com"] (Cwt.Claims.aud claims);
444444- Alcotest.(check (option string)) "cti" (Some "\x0b\x71") (Cwt.Claims.cti claims)
489489+ Alcotest.(check (list string))
490490+ "aud"
491491+ [ "coap://light.example.com" ]
492492+ (Cwt.Claims.aud claims);
493493+ Alcotest.(check (option string))
494494+ "cti" (Some "\x0b\x71") (Cwt.Claims.cti claims)
445495446496(* ============= More Algorithm Coverage Tests ============= *)
447497448498let test_create_hmac_384_cwt () =
449499 let claims =
450450- Cwt.Claims.empty
451451- |> Cwt.Claims.set_iss "test-issuer"
452452- |> Cwt.Claims.build
500500+ Cwt.Claims.empty |> Cwt.Claims.set_iss "test-issuer" |> Cwt.Claims.build
453501 in
454502 (* Need 48-byte key for HMAC-384 *)
455503 let key = Cwt.Cose_key.symmetric (String.make 48 'k') in
456504 match Cwt.create ~algorithm:Cwt.Algorithm.HMAC_384 ~claims ~key with
457505 | Ok cwt ->
458458- Alcotest.(check bool) "alg is HMAC_384" true (Cwt.algorithm cwt = Some Cwt.Algorithm.HMAC_384);
506506+ Alcotest.(check bool)
507507+ "alg is HMAC_384" true
508508+ (Cwt.algorithm cwt = Some Cwt.Algorithm.HMAC_384);
459509 let encoded = Cwt.encode cwt in
460510 Alcotest.(check bool) "non-empty encoding" true (String.length encoded > 0)
461511 | Error e ->
462462- Alcotest.fail (Printf.sprintf "CWT creation failed: %s" (Cwt.error_to_string e))
512512+ Alcotest.fail
513513+ (Printf.sprintf "CWT creation failed: %s" (Cwt.error_to_string e))
463514464515let test_create_hmac_512_cwt () =
465516 let claims =
466466- Cwt.Claims.empty
467467- |> Cwt.Claims.set_iss "test-issuer"
468468- |> Cwt.Claims.build
517517+ Cwt.Claims.empty |> Cwt.Claims.set_iss "test-issuer" |> Cwt.Claims.build
469518 in
470519 (* Need 64-byte key for HMAC-512 *)
471520 let key = Cwt.Cose_key.symmetric (String.make 64 'k') in
472521 match Cwt.create ~algorithm:Cwt.Algorithm.HMAC_512 ~claims ~key with
473522 | Ok cwt ->
474474- Alcotest.(check bool) "alg is HMAC_512" true (Cwt.algorithm cwt = Some Cwt.Algorithm.HMAC_512);
523523+ Alcotest.(check bool)
524524+ "alg is HMAC_512" true
525525+ (Cwt.algorithm cwt = Some Cwt.Algorithm.HMAC_512);
475526 let encoded = Cwt.encode cwt in
476527 Alcotest.(check bool) "non-empty encoding" true (String.length encoded > 0)
477528 | Error e ->
478478- Alcotest.fail (Printf.sprintf "CWT creation failed: %s" (Cwt.error_to_string e))
529529+ Alcotest.fail
530530+ (Printf.sprintf "CWT creation failed: %s" (Cwt.error_to_string e))
479531480532(* ============= COSE Key Serialization Tests ============= *)
481533···509561 let cbor = hex_to_bytes rfc_256bit_key_hex in
510562 match Cwt.Cose_key.of_cbor cbor with
511563 | Ok key ->
512512- Alcotest.(check bool) "key type is symmetric" true
564564+ Alcotest.(check bool)
565565+ "key type is symmetric" true
513566 (Cwt.Cose_key.kty key = Cwt.Cose_key.Symmetric);
514514- Alcotest.(check (option string)) "kid" (Some "Symmetric256") (Cwt.Cose_key.kid key)
515515- | Error e -> Alcotest.fail (Printf.sprintf "Failed to decode key: %s" (Cwt.error_to_string e))
567567+ Alcotest.(check (option string))
568568+ "kid" (Some "Symmetric256") (Cwt.Cose_key.kid key)
569569+ | Error e ->
570570+ Alcotest.fail
571571+ (Printf.sprintf "Failed to decode key: %s" (Cwt.error_to_string e))
516572517573(* ============= CWT Encoding Tests ============= *)
518574···532588 let encoded = Cwt.encode cwt in
533589 (* COSE_Mac0 has tag 17 (0xd1) *)
534590 Alcotest.(check bool) "non-empty" true (String.length encoded > 0);
535535- Alcotest.(check (option string)) "iss preserved" (Some "roundtrip-issuer") (Cwt.Claims.iss (Cwt.claims cwt));
536536- Alcotest.(check (option string)) "sub preserved" (Some "roundtrip-subject") (Cwt.Claims.sub (Cwt.claims cwt))
537537- | Error e -> Alcotest.fail (Printf.sprintf "Create failed: %s" (Cwt.error_to_string e))
591591+ Alcotest.(check (option string))
592592+ "iss preserved" (Some "roundtrip-issuer")
593593+ (Cwt.Claims.iss (Cwt.claims cwt));
594594+ Alcotest.(check (option string))
595595+ "sub preserved" (Some "roundtrip-subject")
596596+ (Cwt.Claims.sub (Cwt.claims cwt))
597597+ | Error e ->
598598+ Alcotest.fail (Printf.sprintf "Create failed: %s" (Cwt.error_to_string e))
538599539600let test_cwt_es256_encoding () =
540601 let claims =
541541- Cwt.Claims.empty
542542- |> Cwt.Claims.set_iss "es256-issuer"
543543- |> Cwt.Claims.build
602602+ Cwt.Claims.empty |> Cwt.Claims.set_iss "es256-issuer" |> Cwt.Claims.build
603603+ in
604604+ let priv_key =
605605+ Cwt.Cose_key.p256_priv ~x:rfc_p256_x ~y:rfc_p256_y ~d:rfc_p256_d
544606 in
545545- let priv_key = Cwt.Cose_key.p256_priv ~x:rfc_p256_x ~y:rfc_p256_y ~d:rfc_p256_d in
546607 match Cwt.create ~algorithm:Cwt.Algorithm.ES256 ~claims ~key:priv_key with
547608 | Ok cwt ->
548609 let encoded = Cwt.encode cwt in
549610 (* COSE_Sign1 has tag 18 (0xd2) *)
550550- Alcotest.(check int) "COSE_Sign1 tag" 0xd2 (Char.code (String.get encoded 0));
551551- Alcotest.(check (option string)) "iss preserved" (Some "es256-issuer") (Cwt.Claims.iss (Cwt.claims cwt))
552552- | Error e -> Alcotest.fail (Printf.sprintf "Create failed: %s" (Cwt.error_to_string e))
611611+ Alcotest.(check int)
612612+ "COSE_Sign1 tag" 0xd2
613613+ (Char.code (String.get encoded 0));
614614+ Alcotest.(check (option string))
615615+ "iss preserved" (Some "es256-issuer")
616616+ (Cwt.Claims.iss (Cwt.claims cwt))
617617+ | Error e ->
618618+ Alcotest.fail (Printf.sprintf "Create failed: %s" (Cwt.error_to_string e))
553619554620let test_cwt_parse_roundtrip () =
555621 (* Test that parse correctly round-trips a created CWT *)
···565631 let encoded = Cwt.encode cwt in
566632 begin match Cwt.parse encoded with
567633 | Ok parsed ->
568568- Alcotest.(check (option string)) "iss" (Some "test-issuer") (Cwt.Claims.iss (Cwt.claims parsed));
569569- Alcotest.(check (option string)) "sub" (Some "test-subject") (Cwt.Claims.sub (Cwt.claims parsed));
570570- Alcotest.(check (option string)) "algorithm"
571571- (Some "HMAC 256/256")
634634+ Alcotest.(check (option string))
635635+ "iss" (Some "test-issuer")
636636+ (Cwt.Claims.iss (Cwt.claims parsed));
637637+ Alcotest.(check (option string))
638638+ "sub" (Some "test-subject")
639639+ (Cwt.Claims.sub (Cwt.claims parsed));
640640+ Alcotest.(check (option string))
641641+ "algorithm" (Some "HMAC 256/256")
572642 (Option.map Cwt.Algorithm.to_string (Cwt.algorithm parsed))
573573- | Error e -> Alcotest.fail (Printf.sprintf "Parse failed: %s" (Cwt.error_to_string e))
643643+ | Error e ->
644644+ Alcotest.fail
645645+ (Printf.sprintf "Parse failed: %s" (Cwt.error_to_string e))
574646 end
575575- | Error e -> Alcotest.fail (Printf.sprintf "Create failed: %s" (Cwt.error_to_string e))
647647+ | Error e ->
648648+ Alcotest.fail (Printf.sprintf "Create failed: %s" (Cwt.error_to_string e))
576649577650(* ============= RFC 8392 Test Vector Tests ============= *)
578651···586659 |> Cwt.Claims.set_iss "coap://as.example.com"
587660 |> Cwt.Claims.set_sub "erikw"
588661 |> Cwt.Claims.set_aud [ "coap://light.example.com" ]
589589- |> Cwt.Claims.set_exp exp
590590- |> Cwt.Claims.set_nbf nbf
662662+ |> Cwt.Claims.set_exp exp |> Cwt.Claims.set_nbf nbf
591663 |> Cwt.Claims.set_iat iat
592664 |> Cwt.Claims.set_cti "\x0b\x71"
593665 |> Cwt.Claims.build
···595667 let cbor = Cwt.Claims.to_cbor claims in
596668 let expected = hex_to_bytes rfc_claims_hex in
597669 (* Compare lengths first, then content *)
598598- Alcotest.(check int) "length matches RFC" (String.length expected) (String.length cbor);
670670+ Alcotest.(check int)
671671+ "length matches RFC" (String.length expected) (String.length cbor);
599672 Alcotest.(check string) "CBOR matches RFC 8392 Appendix A.1" expected cbor
600673601674let test_rfc_claims_cbor_decoding () =
···604677 let cbor = hex_to_bytes rfc_claims_hex in
605678 match Cwt.Claims.of_cbor cbor with
606679 | Ok claims ->
607607- Alcotest.(check (option string)) "iss" (Some "coap://as.example.com") (Cwt.Claims.iss claims);
608608- Alcotest.(check (option string)) "sub" (Some "erikw") (Cwt.Claims.sub claims);
609609- Alcotest.(check (list string)) "aud" ["coap://light.example.com"] (Cwt.Claims.aud claims);
610610- Alcotest.(check (option string)) "cti" (Some "\x0b\x71") (Cwt.Claims.cti claims);
680680+ Alcotest.(check (option string))
681681+ "iss" (Some "coap://as.example.com") (Cwt.Claims.iss claims);
682682+ Alcotest.(check (option string))
683683+ "sub" (Some "erikw") (Cwt.Claims.sub claims);
684684+ Alcotest.(check (list string))
685685+ "aud"
686686+ [ "coap://light.example.com" ]
687687+ (Cwt.Claims.aud claims);
688688+ Alcotest.(check (option string))
689689+ "cti" (Some "\x0b\x71") (Cwt.Claims.cti claims);
611690 (* Check timestamps *)
612691 begin match Cwt.Claims.exp claims with
613692 | Some exp ->
614693 let exp_float = Ptime.to_float_s exp in
615615- Alcotest.(check bool) "exp timestamp" true (abs_float (exp_float -. 1444064944.) < 1.0)
694694+ Alcotest.(check bool)
695695+ "exp timestamp" true
696696+ (abs_float (exp_float -. 1444064944.) < 1.0)
616697 | None -> Alcotest.fail "Expected exp claim"
617698 end;
618699 begin match Cwt.Claims.nbf claims with
619700 | Some nbf ->
620701 let nbf_float = Ptime.to_float_s nbf in
621621- Alcotest.(check bool) "nbf timestamp" true (abs_float (nbf_float -. 1443944944.) < 1.0)
702702+ Alcotest.(check bool)
703703+ "nbf timestamp" true
704704+ (abs_float (nbf_float -. 1443944944.) < 1.0)
622705 | None -> Alcotest.fail "Expected nbf claim"
623706 end;
624707 begin match Cwt.Claims.iat claims with
625708 | Some iat ->
626709 let iat_float = Ptime.to_float_s iat in
627627- Alcotest.(check bool) "iat timestamp" true (abs_float (iat_float -. 1443944944.) < 1.0)
710710+ Alcotest.(check bool)
711711+ "iat timestamp" true
712712+ (abs_float (iat_float -. 1443944944.) < 1.0)
628713 | None -> Alcotest.fail "Expected iat claim"
629714 end
630715 | Error (Cwt.Invalid_cbor msg) ->
···644729 match Cwt.parse cwt_bytes with
645730 | Ok cwt ->
646731 (* If parsing succeeds, verify the claims *)
647647- Alcotest.(check (option string)) "iss" (Some "coap://as.example.com") (Cwt.Claims.iss (Cwt.claims cwt));
648648- Alcotest.(check (option string)) "sub" (Some "erikw") (Cwt.Claims.sub (Cwt.claims cwt));
649649- Alcotest.(check (option bool)) "alg is ES256" (Some true)
732732+ Alcotest.(check (option string))
733733+ "iss" (Some "coap://as.example.com")
734734+ (Cwt.Claims.iss (Cwt.claims cwt));
735735+ Alcotest.(check (option string))
736736+ "sub" (Some "erikw")
737737+ (Cwt.Claims.sub (Cwt.claims cwt));
738738+ Alcotest.(check (option bool))
739739+ "alg is ES256" (Some true)
650740 (Option.map (fun a -> a = Cwt.Algorithm.ES256) (Cwt.algorithm cwt))
651741 | Error _ ->
652742 (* Parse not yet implemented - that's expected *)
···659749 match Cwt.parse cwt_bytes with
660750 | Ok cwt ->
661751 (* If parsing succeeds, verify the claims *)
662662- Alcotest.(check (option string)) "iss" (Some "coap://as.example.com") (Cwt.Claims.iss (Cwt.claims cwt));
663663- Alcotest.(check (option string)) "sub" (Some "erikw") (Cwt.Claims.sub (Cwt.claims cwt));
664664- Alcotest.(check (option bool)) "alg is HMAC_256_64" (Some true)
665665- (Option.map (fun a -> a = Cwt.Algorithm.HMAC_256_64) (Cwt.algorithm cwt))
752752+ Alcotest.(check (option string))
753753+ "iss" (Some "coap://as.example.com")
754754+ (Cwt.Claims.iss (Cwt.claims cwt));
755755+ Alcotest.(check (option string))
756756+ "sub" (Some "erikw")
757757+ (Cwt.Claims.sub (Cwt.claims cwt));
758758+ Alcotest.(check (option bool))
759759+ "alg is HMAC_256_64" (Some true)
760760+ (Option.map
761761+ (fun a -> a = Cwt.Algorithm.HMAC_256_64)
762762+ (Cwt.algorithm cwt))
666763 | Error _ ->
667764 (* Parse not yet implemented - that's expected *)
668765 ()
···673770 let x = String.make 48 '\x01' in
674771 let y = String.make 48 '\x02' in
675772 let key = Cwt.Cose_key.p384_pub ~x ~y in
676676- Alcotest.(check bool) "kty is Ec2" true (Cwt.Cose_key.kty key = Cwt.Cose_key.Ec2);
677677- Alcotest.(check bool) "alg is ES384" true (Cwt.Cose_key.alg key = Some Cwt.Algorithm.ES384)
773773+ Alcotest.(check bool)
774774+ "kty is Ec2" true
775775+ (Cwt.Cose_key.kty key = Cwt.Cose_key.Ec2);
776776+ Alcotest.(check bool)
777777+ "alg is ES384" true
778778+ (Cwt.Cose_key.alg key = Some Cwt.Algorithm.ES384)
678779679780let test_cose_key_p521 () =
680781 let x = String.make 66 '\x01' in
681782 let y = String.make 66 '\x02' in
682783 let key = Cwt.Cose_key.p521_pub ~x ~y in
683683- Alcotest.(check bool) "kty is Ec2" true (Cwt.Cose_key.kty key = Cwt.Cose_key.Ec2);
684684- Alcotest.(check bool) "alg is ES512" true (Cwt.Cose_key.alg key = Some Cwt.Algorithm.ES512)
784784+ Alcotest.(check bool)
785785+ "kty is Ec2" true
786786+ (Cwt.Cose_key.kty key = Cwt.Cose_key.Ec2);
787787+ Alcotest.(check bool)
788788+ "alg is ES512" true
789789+ (Cwt.Cose_key.alg key = Some Cwt.Algorithm.ES512)
685790686791(* ============= Algorithm Tests ============= *)
687792···692797 Alcotest.(check bool) "has ES384" true (List.mem Cwt.Algorithm.ES384 all);
693798 Alcotest.(check bool) "has ES512" true (List.mem Cwt.Algorithm.ES512 all);
694799 Alcotest.(check bool) "has EdDSA" true (List.mem Cwt.Algorithm.EdDSA all);
695695- Alcotest.(check bool) "has HMAC_256" true (List.mem Cwt.Algorithm.HMAC_256 all);
696696- Alcotest.(check bool) "has HMAC_384" true (List.mem Cwt.Algorithm.HMAC_384 all);
697697- Alcotest.(check bool) "has HMAC_512" true (List.mem Cwt.Algorithm.HMAC_512 all);
698698- Alcotest.(check bool) "has HMAC_256_64" true (List.mem Cwt.Algorithm.HMAC_256_64 all)
800800+ Alcotest.(check bool)
801801+ "has HMAC_256" true
802802+ (List.mem Cwt.Algorithm.HMAC_256 all);
803803+ Alcotest.(check bool)
804804+ "has HMAC_384" true
805805+ (List.mem Cwt.Algorithm.HMAC_384 all);
806806+ Alcotest.(check bool)
807807+ "has HMAC_512" true
808808+ (List.mem Cwt.Algorithm.HMAC_512 all);
809809+ Alcotest.(check bool)
810810+ "has HMAC_256_64" true
811811+ (List.mem Cwt.Algorithm.HMAC_256_64 all)
699812700813let test_algorithm_to_string () =
701814 let open Cwt.Algorithm in
702815 Alcotest.(check bool) "ES256 name" true (String.length (to_string ES256) > 0);
703703- Alcotest.(check bool) "HMAC_256 name" true (String.length (to_string HMAC_256) > 0)
816816+ Alcotest.(check bool)
817817+ "HMAC_256 name" true
818818+ (String.length (to_string HMAC_256) > 0)
704819705820(* ============= Test Runner ============= *)
706821707822let () =
708708- Alcotest.run "Cwt" [
709709- "Algorithm", [
710710- Alcotest.test_case "roundtrip" `Quick test_algorithm_roundtrip;
711711- Alcotest.test_case "cose_values" `Quick test_algorithm_cose_values;
712712- Alcotest.test_case "unknown" `Quick test_algorithm_unknown;
713713- Alcotest.test_case "all_list" `Quick test_algorithm_all_list;
714714- Alcotest.test_case "to_string" `Quick test_algorithm_to_string;
715715- ];
716716- "COSE Key", [
717717- Alcotest.test_case "symmetric" `Quick test_cose_key_symmetric;
718718- Alcotest.test_case "ed25519" `Quick test_cose_key_ed25519;
719719- Alcotest.test_case "p256" `Quick test_cose_key_p256;
720720- Alcotest.test_case "p384" `Quick test_cose_key_p384;
721721- Alcotest.test_case "p521" `Quick test_cose_key_p521;
722722- Alcotest.test_case "with_kid" `Quick test_cose_key_with_kid;
723723- ];
724724- "COSE Key Serialization", [
725725- Alcotest.test_case "to_cbor_symmetric" `Quick test_cose_key_to_cbor_symmetric;
726726- Alcotest.test_case "to_cbor_ed25519" `Quick test_cose_key_to_cbor_ed25519;
727727- Alcotest.test_case "to_cbor_p256" `Quick test_cose_key_to_cbor_p256;
728728- Alcotest.test_case "of_cbor" `Quick test_cose_key_of_cbor;
729729- ];
730730- "Claims", [
731731- Alcotest.test_case "builder" `Quick test_claims_builder;
732732- Alcotest.test_case "timestamps" `Quick test_claims_with_timestamps;
733733- Alcotest.test_case "audience_single" `Quick test_claims_audience_single;
734734- Alcotest.test_case "audience_multiple" `Quick test_claims_audience_multiple;
735735- Alcotest.test_case "cti" `Quick test_claims_cti;
736736- Alcotest.test_case "to_cbor" `Quick test_claims_to_cbor;
737737- ];
738738- "CWT Creation", [
739739- Alcotest.test_case "hmac" `Quick test_create_hmac_cwt;
740740- Alcotest.test_case "hmac_256_64" `Quick test_create_hmac_256_64_cwt;
741741- Alcotest.test_case "hmac_384" `Quick test_create_hmac_384_cwt;
742742- Alcotest.test_case "hmac_512" `Quick test_create_hmac_512_cwt;
743743- Alcotest.test_case "es256" `Quick test_create_es256_cwt;
744744- Alcotest.test_case "key_mismatch" `Quick test_create_key_mismatch;
745745- ];
746746- "CWT Encoding", [
747747- Alcotest.test_case "hmac" `Quick test_cwt_hmac_encoding;
748748- Alcotest.test_case "es256" `Quick test_cwt_es256_encoding;
749749- Alcotest.test_case "parse_roundtrip" `Quick test_cwt_parse_roundtrip;
750750- ];
751751- "Claims Validation", [
752752- Alcotest.test_case "expired" `Quick test_validate_expired_token;
753753- Alcotest.test_case "not_yet_valid" `Quick test_validate_not_yet_valid_token;
754754- Alcotest.test_case "with_leeway" `Quick test_validate_with_leeway;
755755- Alcotest.test_case "issuer_match" `Quick test_validate_issuer_match;
756756- Alcotest.test_case "issuer_mismatch" `Quick test_validate_issuer_mismatch;
757757- Alcotest.test_case "audience_match" `Quick test_validate_audience_match;
758758- Alcotest.test_case "audience_mismatch" `Quick test_validate_audience_mismatch;
759759- ];
760760- "Helper Functions", [
761761- Alcotest.test_case "is_expired" `Quick test_is_expired;
762762- Alcotest.test_case "time_to_expiry" `Quick test_time_to_expiry;
763763- ];
764764- "Error Types", [
765765- Alcotest.test_case "to_string" `Quick test_error_to_string;
766766- ];
767767- "RFC 8392 Test Vectors", [
768768- Alcotest.test_case "claims_timestamps" `Quick test_rfc_claims_timestamps;
769769- Alcotest.test_case "claims_cbor_encoding" `Quick test_rfc_claims_cbor_encoding;
770770- Alcotest.test_case "claims_cbor_decoding" `Quick test_rfc_claims_cbor_decoding;
771771- Alcotest.test_case "signed_cwt_parse" `Quick test_rfc_signed_cwt_parse;
772772- Alcotest.test_case "maced_cwt_parse" `Quick test_rfc_maced_cwt_parse;
773773- ];
774774- ]
823823+ Alcotest.run "Cwt"
824824+ [
825825+ ( "Algorithm",
826826+ [
827827+ Alcotest.test_case "roundtrip" `Quick test_algorithm_roundtrip;
828828+ Alcotest.test_case "cose_values" `Quick test_algorithm_cose_values;
829829+ Alcotest.test_case "unknown" `Quick test_algorithm_unknown;
830830+ Alcotest.test_case "all_list" `Quick test_algorithm_all_list;
831831+ Alcotest.test_case "to_string" `Quick test_algorithm_to_string;
832832+ ] );
833833+ ( "COSE Key",
834834+ [
835835+ Alcotest.test_case "symmetric" `Quick test_cose_key_symmetric;
836836+ Alcotest.test_case "ed25519" `Quick test_cose_key_ed25519;
837837+ Alcotest.test_case "p256" `Quick test_cose_key_p256;
838838+ Alcotest.test_case "p384" `Quick test_cose_key_p384;
839839+ Alcotest.test_case "p521" `Quick test_cose_key_p521;
840840+ Alcotest.test_case "with_kid" `Quick test_cose_key_with_kid;
841841+ ] );
842842+ ( "COSE Key Serialization",
843843+ [
844844+ Alcotest.test_case "to_cbor_symmetric" `Quick
845845+ test_cose_key_to_cbor_symmetric;
846846+ Alcotest.test_case "to_cbor_ed25519" `Quick
847847+ test_cose_key_to_cbor_ed25519;
848848+ Alcotest.test_case "to_cbor_p256" `Quick test_cose_key_to_cbor_p256;
849849+ Alcotest.test_case "of_cbor" `Quick test_cose_key_of_cbor;
850850+ ] );
851851+ ( "Claims",
852852+ [
853853+ Alcotest.test_case "builder" `Quick test_claims_builder;
854854+ Alcotest.test_case "timestamps" `Quick test_claims_with_timestamps;
855855+ Alcotest.test_case "audience_single" `Quick
856856+ test_claims_audience_single;
857857+ Alcotest.test_case "audience_multiple" `Quick
858858+ test_claims_audience_multiple;
859859+ Alcotest.test_case "cti" `Quick test_claims_cti;
860860+ Alcotest.test_case "to_cbor" `Quick test_claims_to_cbor;
861861+ ] );
862862+ ( "CWT Creation",
863863+ [
864864+ Alcotest.test_case "hmac" `Quick test_create_hmac_cwt;
865865+ Alcotest.test_case "hmac_256_64" `Quick test_create_hmac_256_64_cwt;
866866+ Alcotest.test_case "hmac_384" `Quick test_create_hmac_384_cwt;
867867+ Alcotest.test_case "hmac_512" `Quick test_create_hmac_512_cwt;
868868+ Alcotest.test_case "es256" `Quick test_create_es256_cwt;
869869+ Alcotest.test_case "key_mismatch" `Quick test_create_key_mismatch;
870870+ ] );
871871+ ( "CWT Encoding",
872872+ [
873873+ Alcotest.test_case "hmac" `Quick test_cwt_hmac_encoding;
874874+ Alcotest.test_case "es256" `Quick test_cwt_es256_encoding;
875875+ Alcotest.test_case "parse_roundtrip" `Quick test_cwt_parse_roundtrip;
876876+ ] );
877877+ ( "Claims Validation",
878878+ [
879879+ Alcotest.test_case "expired" `Quick test_validate_expired_token;
880880+ Alcotest.test_case "not_yet_valid" `Quick
881881+ test_validate_not_yet_valid_token;
882882+ Alcotest.test_case "with_leeway" `Quick test_validate_with_leeway;
883883+ Alcotest.test_case "issuer_match" `Quick test_validate_issuer_match;
884884+ Alcotest.test_case "issuer_mismatch" `Quick
885885+ test_validate_issuer_mismatch;
886886+ Alcotest.test_case "audience_match" `Quick
887887+ test_validate_audience_match;
888888+ Alcotest.test_case "audience_mismatch" `Quick
889889+ test_validate_audience_mismatch;
890890+ ] );
891891+ ( "Helper Functions",
892892+ [
893893+ Alcotest.test_case "is_expired" `Quick test_is_expired;
894894+ Alcotest.test_case "time_to_expiry" `Quick test_time_to_expiry;
895895+ ] );
896896+ ( "Error Types",
897897+ [ Alcotest.test_case "to_string" `Quick test_error_to_string ] );
898898+ ( "RFC 8392 Test Vectors",
899899+ [
900900+ Alcotest.test_case "claims_timestamps" `Quick
901901+ test_rfc_claims_timestamps;
902902+ Alcotest.test_case "claims_cbor_encoding" `Quick
903903+ test_rfc_claims_cbor_encoding;
904904+ Alcotest.test_case "claims_cbor_decoding" `Quick
905905+ test_rfc_claims_cbor_decoding;
906906+ Alcotest.test_case "signed_cwt_parse" `Quick test_rfc_signed_cwt_parse;
907907+ Alcotest.test_case "maced_cwt_parse" `Quick test_rfc_maced_cwt_parse;
908908+ ] );
909909+ ]
+235-152
ocaml-jsonwt/test/test_jsonwt.ml
···11(** JWT Library Tests
2233- Comprehensive tests derived from RFC 7519 (JSON Web Token)
44- and RFC 7515 (JSON Web Signature) specifications. *)
33+ Comprehensive tests derived from RFC 7519 (JSON Web Token) and RFC 7515
44+ (JSON Web Signature) specifications. *)
5566(* RFC 7515 Appendix A.1 symmetric key for HS256 *)
77let rfc_hs256_key_b64 =
···991010(* RFC 7519 Section 3.1 example JWT (HS256) *)
1111let rfc_section3_1_token =
1212- "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.\
1313- eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.\
1414- dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
1212+ "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
15131614(* RFC 7519 Section 6.1 unsecured JWT *)
1715let rfc_section6_1_token =
1818- "eyJhbGciOiJub25lIn0.\
1919- eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.\
2020- "
1616+ "eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ."
21172218(* Helper to decode base64url to bytes *)
2319let b64url_decode s =
2420 (* Pad to multiple of 4 *)
2525- let pad = match String.length s mod 4 with
2121+ let pad =
2222+ match String.length s mod 4 with
2623 | 0 -> ""
2724 | 2 -> "=="
2825 | 3 -> "="
···36333734let test_algorithm_roundtrip () =
3835 let open Jsonwt.Algorithm in
3939- let algs = [ None; HS256; HS384; HS512; RS256; RS384; RS512; ES256; ES384; ES512; EdDSA ] in
4040- List.iter (fun alg ->
4141- let s = to_string alg in
4242- match of_string s with
4343- | Ok alg' ->
4444- Alcotest.(check string) "roundtrip" s (to_string alg')
4545- | Error e ->
4646- Alcotest.fail (Jsonwt.error_to_string e)
4747- ) algs
3636+ let algs =
3737+ [
3838+ None; HS256; HS384; HS512; RS256; RS384; RS512; ES256; ES384; ES512; EdDSA;
3939+ ]
4040+ in
4141+ List.iter
4242+ (fun alg ->
4343+ let s = to_string alg in
4444+ match of_string s with
4545+ | Ok alg' -> Alcotest.(check string) "roundtrip" s (to_string alg')
4646+ | Error e -> Alcotest.fail (Jsonwt.error_to_string e))
4747+ algs
48484949let test_algorithm_unknown () =
5050 match Jsonwt.Algorithm.of_string "UNKNOWN" with
···7474 |> Jsonwt.Claims.set_string "custom" "value"
7575 |> Jsonwt.Claims.build
7676 in
7777- Alcotest.(check (option string)) "iss" (Some "test-issuer") (Jsonwt.Claims.iss claims);
7878- Alcotest.(check (option string)) "sub" (Some "test-subject") (Jsonwt.Claims.sub claims);
7979- Alcotest.(check (option string)) "custom" (Some "value") (Jsonwt.Claims.get_string "custom" claims)
7777+ Alcotest.(check (option string))
7878+ "iss" (Some "test-issuer") (Jsonwt.Claims.iss claims);
7979+ Alcotest.(check (option string))
8080+ "sub" (Some "test-subject") (Jsonwt.Claims.sub claims);
8181+ Alcotest.(check (option string))
8282+ "custom" (Some "value")
8383+ (Jsonwt.Claims.get_string "custom" claims)
80848185let test_claims_with_timestamps () =
8282- let now = Ptime.of_float_s 1609459200. |> Option.get in (* 2021-01-01 00:00:00 UTC *)
8383- let exp = Ptime.of_float_s 1609545600. |> Option.get in (* 2021-01-02 00:00:00 UTC *)
8686+ let now = Ptime.of_float_s 1609459200. |> Option.get in
8787+ (* 2021-01-01 00:00:00 UTC *)
8888+ let exp = Ptime.of_float_s 1609545600. |> Option.get in
8989+ (* 2021-01-02 00:00:00 UTC *)
8490 let claims =
8585- Jsonwt.Claims.empty
8686- |> Jsonwt.Claims.set_iat now
8787- |> Jsonwt.Claims.set_exp exp
8888- |> Jsonwt.Claims.set_nbf now
9191+ Jsonwt.Claims.empty |> Jsonwt.Claims.set_iat now
9292+ |> Jsonwt.Claims.set_exp exp |> Jsonwt.Claims.set_nbf now
8993 |> Jsonwt.Claims.build
9094 in
9191- Alcotest.(check (option bool)) "has exp" (Some true) (Option.map (fun _ -> true) (Jsonwt.Claims.exp claims));
9292- Alcotest.(check (option bool)) "has iat" (Some true) (Option.map (fun _ -> true) (Jsonwt.Claims.iat claims));
9393- Alcotest.(check (option bool)) "has nbf" (Some true) (Option.map (fun _ -> true) (Jsonwt.Claims.nbf claims))
9595+ Alcotest.(check (option bool))
9696+ "has exp" (Some true)
9797+ (Option.map (fun _ -> true) (Jsonwt.Claims.exp claims));
9898+ Alcotest.(check (option bool))
9999+ "has iat" (Some true)
100100+ (Option.map (fun _ -> true) (Jsonwt.Claims.iat claims));
101101+ Alcotest.(check (option bool))
102102+ "has nbf" (Some true)
103103+ (Option.map (fun _ -> true) (Jsonwt.Claims.nbf claims))
9410495105let test_claims_audience_single () =
96106 let claims =
···106116 |> Jsonwt.Claims.set_aud [ "app1"; "app2"; "app3" ]
107117 |> Jsonwt.Claims.build
108118 in
109109- Alcotest.(check (list string)) "aud" [ "app1"; "app2"; "app3" ] (Jsonwt.Claims.aud claims)
119119+ Alcotest.(check (list string))
120120+ "aud" [ "app1"; "app2"; "app3" ] (Jsonwt.Claims.aud claims)
110121111122(* ============= Parse Tests ============= *)
112123···125136let test_parse_invalid_base64 () =
126137 match Jsonwt.parse "!!!.@@@.###" with
127138 | Error (Jsonwt.Invalid_base64url _) -> ()
128128- | Error e -> Alcotest.fail (Printf.sprintf "Expected Invalid_base64url, got %s" (Jsonwt.error_to_string e))
139139+ | Error e ->
140140+ Alcotest.fail
141141+ (Printf.sprintf "Expected Invalid_base64url, got %s"
142142+ (Jsonwt.error_to_string e))
129143 | Ok _ -> Alcotest.fail "Expected parse to fail with invalid base64"
130144131145(* ============= RFC 7519 Test Vectors ============= *)
···134148let test_rfc_unsecured_jwt_parse () =
135149 match Jsonwt.parse rfc_section6_1_token with
136150 | Ok jwt ->
137137- Alcotest.(check bool) "alg is none" true (jwt.header.alg = Jsonwt.Algorithm.None);
138138- Alcotest.(check (option string)) "iss is joe" (Some "joe") (Jsonwt.Claims.iss jwt.claims);
151151+ Alcotest.(check bool)
152152+ "alg is none" true
153153+ (jwt.header.alg = Jsonwt.Algorithm.None);
154154+ Alcotest.(check (option string))
155155+ "iss is joe" (Some "joe")
156156+ (Jsonwt.Claims.iss jwt.claims);
139157 Alcotest.(check string) "signature is empty" "" jwt.signature
140158 | Error e ->
141141- Alcotest.fail (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e))
159159+ Alcotest.fail
160160+ (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e))
142161143162let test_rfc_unsecured_jwt_verify_rejected_by_default () =
144163 match Jsonwt.parse rfc_section6_1_token with
145164 | Ok jwt ->
146146- let key = Jsonwt.Jwk.symmetric "" in (* dummy key *)
165165+ let key = Jsonwt.Jwk.symmetric "" in
166166+ (* dummy key *)
147167 begin match Jsonwt.verify ~key jwt with
148168 | Error Jsonwt.Unsecured_not_allowed -> ()
149149- | Error e -> Alcotest.fail (Printf.sprintf "Expected Unsecured_not_allowed, got: %s" (Jsonwt.error_to_string e))
169169+ | Error e ->
170170+ Alcotest.fail
171171+ (Printf.sprintf "Expected Unsecured_not_allowed, got: %s"
172172+ (Jsonwt.error_to_string e))
150173 | Ok () -> Alcotest.fail "Unsecured JWT should be rejected by default"
151174 end
152175 | Error e ->
153153- Alcotest.fail (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e))
176176+ Alcotest.fail
177177+ (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e))
154178155179let test_rfc_unsecured_jwt_verify_allowed_with_opt_in () =
156180 match Jsonwt.parse rfc_section6_1_token with
157181 | Ok jwt ->
158158- let key = Jsonwt.Jwk.symmetric "" in (* dummy key *)
182182+ let key = Jsonwt.Jwk.symmetric "" in
183183+ (* dummy key *)
159184 begin match Jsonwt.verify ~key ~allow_none:true jwt with
160185 | Ok () -> ()
161161- | Error e -> Alcotest.fail (Printf.sprintf "Verification failed: %s" (Jsonwt.error_to_string e))
186186+ | Error e ->
187187+ Alcotest.fail
188188+ (Printf.sprintf "Verification failed: %s" (Jsonwt.error_to_string e))
162189 end
163190 | Error e ->
164164- Alcotest.fail (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e))
191191+ Alcotest.fail
192192+ (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e))
165193166194(* RFC 7519 Section 3.1: HS256 JWT *)
167195let test_rfc_hs256_jwt_parse () =
168196 match Jsonwt.parse rfc_section3_1_token with
169197 | Ok jwt ->
170170- Alcotest.(check bool) "alg is HS256" true (jwt.header.alg = Jsonwt.Algorithm.HS256);
198198+ Alcotest.(check bool)
199199+ "alg is HS256" true
200200+ (jwt.header.alg = Jsonwt.Algorithm.HS256);
171201 Alcotest.(check (option string)) "typ is JWT" (Some "JWT") jwt.header.typ;
172172- Alcotest.(check (option string)) "iss is joe" (Some "joe") (Jsonwt.Claims.iss jwt.claims)
202202+ Alcotest.(check (option string))
203203+ "iss is joe" (Some "joe")
204204+ (Jsonwt.Claims.iss jwt.claims)
173205 | Error e ->
174174- Alcotest.fail (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e))
206206+ Alcotest.fail
207207+ (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e))
175208176209let test_rfc_hs256_jwt_verify () =
177210 match Jsonwt.parse rfc_section3_1_token with
···180213 let key = Jsonwt.Jwk.symmetric key_bytes in
181214 begin match Jsonwt.verify ~key jwt with
182215 | Ok () -> ()
183183- | Error e -> Alcotest.fail (Printf.sprintf "Verification failed: %s" (Jsonwt.error_to_string e))
216216+ | Error e ->
217217+ Alcotest.fail
218218+ (Printf.sprintf "Verification failed: %s" (Jsonwt.error_to_string e))
184219 end
185220 | Error e ->
186186- Alcotest.fail (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e))
221221+ Alcotest.fail
222222+ (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e))
187223188224let test_rfc_hs256_jwt_verify_wrong_key () =
189225 match Jsonwt.parse rfc_section3_1_token with
190226 | Ok jwt ->
191191- let wrong_key = Jsonwt.Jwk.symmetric "wrong-key-material-that-is-long-enough" in
227227+ let wrong_key =
228228+ Jsonwt.Jwk.symmetric "wrong-key-material-that-is-long-enough"
229229+ in
192230 begin match Jsonwt.verify ~key:wrong_key jwt with
193231 | Error Jsonwt.Signature_mismatch -> ()
194194- | Error e -> Alcotest.fail (Printf.sprintf "Expected Signature_mismatch, got: %s" (Jsonwt.error_to_string e))
232232+ | Error e ->
233233+ Alcotest.fail
234234+ (Printf.sprintf "Expected Signature_mismatch, got: %s"
235235+ (Jsonwt.error_to_string e))
195236 | Ok () -> Alcotest.fail "Verification should fail with wrong key"
196237 end
197238 | Error e ->
198198- Alcotest.fail (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e))
239239+ Alcotest.fail
240240+ (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e))
199241200242(* ============= Claims Validation Tests ============= *)
201243202244let test_validate_expired_token () =
203203- let exp = Ptime.of_float_s 1300819380. |> Option.get in (* RFC example exp *)
204204- let now = Ptime.of_float_s 1400000000. |> Option.get in (* After exp *)
245245+ let exp = Ptime.of_float_s 1300819380. |> Option.get in
246246+ (* RFC example exp *)
247247+ let now = Ptime.of_float_s 1400000000. |> Option.get in
248248+ (* After exp *)
205249 let claims =
206206- Jsonwt.Claims.empty
207207- |> Jsonwt.Claims.set_exp exp
208208- |> Jsonwt.Claims.build
250250+ Jsonwt.Claims.empty |> Jsonwt.Claims.set_exp exp |> Jsonwt.Claims.build
209251 in
210252 let header = Jsonwt.Header.make Jsonwt.Algorithm.None in
211253 let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in
212254 match Jsonwt.validate ~now jwt with
213255 | Error Jsonwt.Token_expired -> ()
214214- | Error e -> Alcotest.fail (Printf.sprintf "Expected Token_expired, got: %s" (Jsonwt.error_to_string e))
256256+ | Error e ->
257257+ Alcotest.fail
258258+ (Printf.sprintf "Expected Token_expired, got: %s"
259259+ (Jsonwt.error_to_string e))
215260 | Ok () -> Alcotest.fail "Expected Token_expired error"
216261217262let test_validate_not_yet_valid_token () =
218263 let nbf = Ptime.of_float_s 1500000000. |> Option.get in
219219- let now = Ptime.of_float_s 1400000000. |> Option.get in (* Before nbf *)
264264+ let now = Ptime.of_float_s 1400000000. |> Option.get in
265265+ (* Before nbf *)
220266 let claims =
221221- Jsonwt.Claims.empty
222222- |> Jsonwt.Claims.set_nbf nbf
223223- |> Jsonwt.Claims.build
267267+ Jsonwt.Claims.empty |> Jsonwt.Claims.set_nbf nbf |> Jsonwt.Claims.build
224268 in
225269 let header = Jsonwt.Header.make Jsonwt.Algorithm.None in
226270 let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in
227271 match Jsonwt.validate ~now jwt with
228272 | Error Jsonwt.Token_not_yet_valid -> ()
229229- | Error e -> Alcotest.fail (Printf.sprintf "Expected Token_not_yet_valid, got: %s" (Jsonwt.error_to_string e))
273273+ | Error e ->
274274+ Alcotest.fail
275275+ (Printf.sprintf "Expected Token_not_yet_valid, got: %s"
276276+ (Jsonwt.error_to_string e))
230277 | Ok () -> Alcotest.fail "Expected Token_not_yet_valid error"
231278232279let test_validate_with_leeway () =
233280 let exp = Ptime.of_float_s 1300819380. |> Option.get in
234234- let now = Ptime.of_float_s 1300819390. |> Option.get in (* 10 seconds after exp *)
235235- let leeway = Ptime.Span.of_int_s 60 in (* 60 second leeway *)
281281+ let now = Ptime.of_float_s 1300819390. |> Option.get in
282282+ (* 10 seconds after exp *)
283283+ let leeway = Ptime.Span.of_int_s 60 in
284284+ (* 60 second leeway *)
236285 let claims =
237237- Jsonwt.Claims.empty
238238- |> Jsonwt.Claims.set_exp exp
239239- |> Jsonwt.Claims.build
286286+ Jsonwt.Claims.empty |> Jsonwt.Claims.set_exp exp |> Jsonwt.Claims.build
240287 in
241288 let header = Jsonwt.Header.make Jsonwt.Algorithm.None in
242289 let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in
243290 match Jsonwt.validate ~now ~leeway jwt with
244291 | Ok () -> ()
245245- | Error e -> Alcotest.fail (Printf.sprintf "Expected validation to pass with leeway, got: %s" (Jsonwt.error_to_string e))
292292+ | Error e ->
293293+ Alcotest.fail
294294+ (Printf.sprintf "Expected validation to pass with leeway, got: %s"
295295+ (Jsonwt.error_to_string e))
246296247297let test_validate_issuer_match () =
248298 let now = Ptime.of_float_s 1400000000. |> Option.get in
···255305 let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in
256306 match Jsonwt.validate ~now ~iss:"expected-issuer" jwt with
257307 | Ok () -> ()
258258- | Error e -> Alcotest.fail (Printf.sprintf "Expected validation to pass, got: %s" (Jsonwt.error_to_string e))
308308+ | Error e ->
309309+ Alcotest.fail
310310+ (Printf.sprintf "Expected validation to pass, got: %s"
311311+ (Jsonwt.error_to_string e))
259312260313let test_validate_issuer_mismatch () =
261314 let now = Ptime.of_float_s 1400000000. |> Option.get in
···268321 let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in
269322 match Jsonwt.validate ~now ~iss:"expected-issuer" jwt with
270323 | Error Jsonwt.Invalid_issuer -> ()
271271- | Error e -> Alcotest.fail (Printf.sprintf "Expected Invalid_issuer, got: %s" (Jsonwt.error_to_string e))
324324+ | Error e ->
325325+ Alcotest.fail
326326+ (Printf.sprintf "Expected Invalid_issuer, got: %s"
327327+ (Jsonwt.error_to_string e))
272328 | Ok () -> Alcotest.fail "Expected Invalid_issuer error"
273329274330let test_validate_audience_match () =
···282338 let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in
283339 match Jsonwt.validate ~now ~aud:"my-app" jwt with
284340 | Ok () -> ()
285285- | Error e -> Alcotest.fail (Printf.sprintf "Expected validation to pass, got: %s" (Jsonwt.error_to_string e))
341341+ | Error e ->
342342+ Alcotest.fail
343343+ (Printf.sprintf "Expected validation to pass, got: %s"
344344+ (Jsonwt.error_to_string e))
286345287346let test_validate_audience_mismatch () =
288347 let now = Ptime.of_float_s 1400000000. |> Option.get in
···295354 let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in
296355 match Jsonwt.validate ~now ~aud:"my-app" jwt with
297356 | Error Jsonwt.Invalid_audience -> ()
298298- | Error e -> Alcotest.fail (Printf.sprintf "Expected Invalid_audience, got: %s" (Jsonwt.error_to_string e))
357357+ | Error e ->
358358+ Alcotest.fail
359359+ (Printf.sprintf "Expected Invalid_audience, got: %s"
360360+ (Jsonwt.error_to_string e))
299361 | Ok () -> Alcotest.fail "Expected Invalid_audience error"
300362301363(* ============= Algorithm Restriction Tests ============= *)
···309371 let allowed_algs = [ Jsonwt.Algorithm.HS384; Jsonwt.Algorithm.HS512 ] in
310372 begin match Jsonwt.verify ~key ~allowed_algs jwt with
311373 | Error (Jsonwt.Algorithm_not_allowed "HS256") -> ()
312312- | Error e -> Alcotest.fail (Printf.sprintf "Expected Algorithm_not_allowed, got: %s" (Jsonwt.error_to_string e))
313313- | Ok () -> Alcotest.fail "Verification should fail when algorithm is not allowed"
374374+ | Error e ->
375375+ Alcotest.fail
376376+ (Printf.sprintf "Expected Algorithm_not_allowed, got: %s"
377377+ (Jsonwt.error_to_string e))
378378+ | Ok () ->
379379+ Alcotest.fail "Verification should fail when algorithm is not allowed"
314380 end
315381 | Error e ->
316316- Alcotest.fail (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e))
382382+ Alcotest.fail
383383+ (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e))
317384318385(* ============= Helper Function Tests ============= *)
319386320387let test_is_expired () =
321388 let exp = Ptime.of_float_s 1300819380. |> Option.get in
322389 let claims =
323323- Jsonwt.Claims.empty
324324- |> Jsonwt.Claims.set_exp exp
325325- |> Jsonwt.Claims.build
390390+ Jsonwt.Claims.empty |> Jsonwt.Claims.set_exp exp |> Jsonwt.Claims.build
326391 in
327392 let header = Jsonwt.Header.make Jsonwt.Algorithm.None in
328393 let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in
329394 let now_before = Ptime.of_float_s 1300819370. |> Option.get in
330395 let now_after = Ptime.of_float_s 1300819390. |> Option.get in
331331- Alcotest.(check bool) "not expired before" false (Jsonwt.is_expired ~now:now_before jwt);
332332- Alcotest.(check bool) "expired after" true (Jsonwt.is_expired ~now:now_after jwt)
396396+ Alcotest.(check bool)
397397+ "not expired before" false
398398+ (Jsonwt.is_expired ~now:now_before jwt);
399399+ Alcotest.(check bool)
400400+ "expired after" true
401401+ (Jsonwt.is_expired ~now:now_after jwt)
333402334403let test_time_to_expiry () =
335404 let exp = Ptime.of_float_s 1300819380. |> Option.get in
336405 let claims =
337337- Jsonwt.Claims.empty
338338- |> Jsonwt.Claims.set_exp exp
339339- |> Jsonwt.Claims.build
406406+ Jsonwt.Claims.empty |> Jsonwt.Claims.set_exp exp |> Jsonwt.Claims.build
340407 in
341408 let header = Jsonwt.Header.make Jsonwt.Algorithm.None in
342409 let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in
···345412 | Some span ->
346413 let seconds = Ptime.Span.to_float_s span |> int_of_float in
347414 Alcotest.(check int) "time to expiry" 10 seconds
348348- | None ->
349349- Alcotest.fail "Expected Some time to expiry"
415415+ | None -> Alcotest.fail "Expected Some time to expiry"
350416351417let test_time_to_expiry_already_expired () =
352418 let exp = Ptime.of_float_s 1300819380. |> Option.get in
353419 let claims =
354354- Jsonwt.Claims.empty
355355- |> Jsonwt.Claims.set_exp exp
356356- |> Jsonwt.Claims.build
420420+ Jsonwt.Claims.empty |> Jsonwt.Claims.set_exp exp |> Jsonwt.Claims.build
357421 in
358422 let header = Jsonwt.Header.make Jsonwt.Algorithm.None in
359423 let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in
···365429(* ============= Error Type Tests ============= *)
366430367431let test_error_to_string () =
368368- let errors = [
369369- (Jsonwt.Invalid_json "test", "Invalid JSON: test");
370370- (Jsonwt.Invalid_base64url "test", "Invalid base64url: test");
371371- (Jsonwt.Invalid_structure "test", "Invalid structure: test");
372372- (Jsonwt.Token_expired, "Token expired");
373373- (Jsonwt.Token_not_yet_valid, "Token not yet valid");
374374- (Jsonwt.Signature_mismatch, "Signature mismatch");
375375- ] in
376376- List.iter (fun (err, expected) ->
377377- let actual = Jsonwt.error_to_string err in
378378- Alcotest.(check string) "error string" expected actual
379379- ) errors
432432+ let errors =
433433+ [
434434+ (Jsonwt.Invalid_json "test", "Invalid JSON: test");
435435+ (Jsonwt.Invalid_base64url "test", "Invalid base64url: test");
436436+ (Jsonwt.Invalid_structure "test", "Invalid structure: test");
437437+ (Jsonwt.Token_expired, "Token expired");
438438+ (Jsonwt.Token_not_yet_valid, "Token not yet valid");
439439+ (Jsonwt.Signature_mismatch, "Signature mismatch");
440440+ ]
441441+ in
442442+ List.iter
443443+ (fun (err, expected) ->
444444+ let actual = Jsonwt.error_to_string err in
445445+ Alcotest.(check string) "error string" expected actual)
446446+ errors
380447381448(* ============= JWK Tests ============= *)
382449···388455(* ============= Test Runner ============= *)
389456390457let () =
391391- Alcotest.run "Jsonwt" [
392392- "Algorithm", [
393393- Alcotest.test_case "roundtrip" `Quick test_algorithm_roundtrip;
394394- Alcotest.test_case "unknown" `Quick test_algorithm_unknown;
395395- ];
396396- "Header", [
397397- Alcotest.test_case "create" `Quick test_header_create;
398398- Alcotest.test_case "with_kid" `Quick test_header_with_kid;
399399- ];
400400- "Claims", [
401401- Alcotest.test_case "builder" `Quick test_claims_builder;
402402- Alcotest.test_case "timestamps" `Quick test_claims_with_timestamps;
403403- Alcotest.test_case "audience_single" `Quick test_claims_audience_single;
404404- Alcotest.test_case "audience_multiple" `Quick test_claims_audience_multiple;
405405- ];
406406- "Parse", [
407407- Alcotest.test_case "invalid" `Quick test_parse_invalid;
408408- Alcotest.test_case "malformed" `Quick test_parse_malformed;
409409- Alcotest.test_case "invalid_base64" `Quick test_parse_invalid_base64;
410410- ];
411411- "RFC 7519 Section 6.1 - Unsecured JWT", [
412412- Alcotest.test_case "parse" `Quick test_rfc_unsecured_jwt_parse;
413413- Alcotest.test_case "rejected_by_default" `Quick test_rfc_unsecured_jwt_verify_rejected_by_default;
414414- Alcotest.test_case "allowed_with_opt_in" `Quick test_rfc_unsecured_jwt_verify_allowed_with_opt_in;
415415- ];
416416- "RFC 7519 Section 3.1 - HS256 JWT", [
417417- Alcotest.test_case "parse" `Quick test_rfc_hs256_jwt_parse;
418418- Alcotest.test_case "verify" `Quick test_rfc_hs256_jwt_verify;
419419- Alcotest.test_case "verify_wrong_key" `Quick test_rfc_hs256_jwt_verify_wrong_key;
420420- ];
421421- "Claims Validation", [
422422- Alcotest.test_case "expired" `Quick test_validate_expired_token;
423423- Alcotest.test_case "not_yet_valid" `Quick test_validate_not_yet_valid_token;
424424- Alcotest.test_case "with_leeway" `Quick test_validate_with_leeway;
425425- Alcotest.test_case "issuer_match" `Quick test_validate_issuer_match;
426426- Alcotest.test_case "issuer_mismatch" `Quick test_validate_issuer_mismatch;
427427- Alcotest.test_case "audience_match" `Quick test_validate_audience_match;
428428- Alcotest.test_case "audience_mismatch" `Quick test_validate_audience_mismatch;
429429- ];
430430- "Algorithm Restrictions", [
431431- Alcotest.test_case "not_allowed" `Quick test_algorithm_not_allowed;
432432- ];
433433- "Helper Functions", [
434434- Alcotest.test_case "is_expired" `Quick test_is_expired;
435435- Alcotest.test_case "time_to_expiry" `Quick test_time_to_expiry;
436436- Alcotest.test_case "time_to_expiry_expired" `Quick test_time_to_expiry_already_expired;
437437- ];
438438- "Error Types", [
439439- Alcotest.test_case "to_string" `Quick test_error_to_string;
440440- ];
441441- "JWK", [
442442- Alcotest.test_case "symmetric" `Quick test_jwk_symmetric;
443443- ];
444444- ]
458458+ Alcotest.run "Jsonwt"
459459+ [
460460+ ( "Algorithm",
461461+ [
462462+ Alcotest.test_case "roundtrip" `Quick test_algorithm_roundtrip;
463463+ Alcotest.test_case "unknown" `Quick test_algorithm_unknown;
464464+ ] );
465465+ ( "Header",
466466+ [
467467+ Alcotest.test_case "create" `Quick test_header_create;
468468+ Alcotest.test_case "with_kid" `Quick test_header_with_kid;
469469+ ] );
470470+ ( "Claims",
471471+ [
472472+ Alcotest.test_case "builder" `Quick test_claims_builder;
473473+ Alcotest.test_case "timestamps" `Quick test_claims_with_timestamps;
474474+ Alcotest.test_case "audience_single" `Quick
475475+ test_claims_audience_single;
476476+ Alcotest.test_case "audience_multiple" `Quick
477477+ test_claims_audience_multiple;
478478+ ] );
479479+ ( "Parse",
480480+ [
481481+ Alcotest.test_case "invalid" `Quick test_parse_invalid;
482482+ Alcotest.test_case "malformed" `Quick test_parse_malformed;
483483+ Alcotest.test_case "invalid_base64" `Quick test_parse_invalid_base64;
484484+ ] );
485485+ ( "RFC 7519 Section 6.1 - Unsecured JWT",
486486+ [
487487+ Alcotest.test_case "parse" `Quick test_rfc_unsecured_jwt_parse;
488488+ Alcotest.test_case "rejected_by_default" `Quick
489489+ test_rfc_unsecured_jwt_verify_rejected_by_default;
490490+ Alcotest.test_case "allowed_with_opt_in" `Quick
491491+ test_rfc_unsecured_jwt_verify_allowed_with_opt_in;
492492+ ] );
493493+ ( "RFC 7519 Section 3.1 - HS256 JWT",
494494+ [
495495+ Alcotest.test_case "parse" `Quick test_rfc_hs256_jwt_parse;
496496+ Alcotest.test_case "verify" `Quick test_rfc_hs256_jwt_verify;
497497+ Alcotest.test_case "verify_wrong_key" `Quick
498498+ test_rfc_hs256_jwt_verify_wrong_key;
499499+ ] );
500500+ ( "Claims Validation",
501501+ [
502502+ Alcotest.test_case "expired" `Quick test_validate_expired_token;
503503+ Alcotest.test_case "not_yet_valid" `Quick
504504+ test_validate_not_yet_valid_token;
505505+ Alcotest.test_case "with_leeway" `Quick test_validate_with_leeway;
506506+ Alcotest.test_case "issuer_match" `Quick test_validate_issuer_match;
507507+ Alcotest.test_case "issuer_mismatch" `Quick
508508+ test_validate_issuer_mismatch;
509509+ Alcotest.test_case "audience_match" `Quick
510510+ test_validate_audience_match;
511511+ Alcotest.test_case "audience_mismatch" `Quick
512512+ test_validate_audience_mismatch;
513513+ ] );
514514+ ( "Algorithm Restrictions",
515515+ [ Alcotest.test_case "not_allowed" `Quick test_algorithm_not_allowed ]
516516+ );
517517+ ( "Helper Functions",
518518+ [
519519+ Alcotest.test_case "is_expired" `Quick test_is_expired;
520520+ Alcotest.test_case "time_to_expiry" `Quick test_time_to_expiry;
521521+ Alcotest.test_case "time_to_expiry_expired" `Quick
522522+ test_time_to_expiry_already_expired;
523523+ ] );
524524+ ( "Error Types",
525525+ [ Alcotest.test_case "to_string" `Quick test_error_to_string ] );
526526+ ("JWK", [ Alcotest.test_case "symmetric" `Quick test_jwk_symmetric ]);
527527+ ]