JSON web tokens in OCaml
at main 972 lines 32 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6(** CBOR Web Token (CWT) - RFC 8392 *) 7 8(* Error handling *) 9 10type error = 11 | Invalid_cbor of string 12 | Invalid_cose of string 13 | Invalid_claims of string 14 | Unsupported_algorithm of string 15 | Algorithm_not_allowed of string 16 | Signature_mismatch 17 | Token_expired 18 | Token_not_yet_valid 19 | Invalid_issuer 20 | Invalid_audience 21 | Key_type_mismatch of string 22 23let pp_error ppf = function 24 | Invalid_cbor s -> Format.fprintf ppf "Invalid CBOR: %s" s 25 | Invalid_cose s -> Format.fprintf ppf "Invalid COSE: %s" s 26 | Invalid_claims s -> Format.fprintf ppf "Invalid claims: %s" s 27 | Unsupported_algorithm s -> Format.fprintf ppf "Unsupported algorithm: %s" s 28 | Algorithm_not_allowed s -> Format.fprintf ppf "Algorithm not allowed: %s" s 29 | Signature_mismatch -> Format.fprintf ppf "Signature mismatch" 30 | Token_expired -> Format.fprintf ppf "Token expired" 31 | Token_not_yet_valid -> Format.fprintf ppf "Token not yet valid" 32 | Invalid_issuer -> Format.fprintf ppf "Invalid issuer" 33 | Invalid_audience -> Format.fprintf ppf "Invalid audience" 34 | Key_type_mismatch s -> Format.fprintf ppf "Key type mismatch: %s" s 35 36let error_to_string e = Format.asprintf "%a" pp_error e 37 38(* Cbort codec helpers *) 39 40let cbort_error_to_error e = Invalid_cbor (Cbort.Error.to_string e) 41 42(* COSE Algorithms - RFC 9053 *) 43 44module Algorithm = struct 45 type t = 46 | ES256 47 | ES384 48 | ES512 49 | EdDSA 50 | HMAC_256_64 51 | HMAC_256 52 | HMAC_384 53 | HMAC_512 54 55 let to_cose_int = function 56 | ES256 -> -7 57 | ES384 -> -35 58 | ES512 -> -36 59 | EdDSA -> -8 60 | HMAC_256_64 -> 4 61 | HMAC_256 -> 5 62 | HMAC_384 -> 6 63 | HMAC_512 -> 7 64 65 let of_cose_int = function 66 | -7 -> Ok ES256 67 | -35 -> Ok ES384 68 | -36 -> Ok ES512 69 | -8 -> Ok EdDSA 70 | 4 -> Ok HMAC_256_64 71 | 5 -> Ok HMAC_256 72 | 6 -> Ok HMAC_384 73 | 7 -> Ok HMAC_512 74 | n -> Error (Unsupported_algorithm (Printf.sprintf "COSE algorithm %d" n)) 75 76 let to_string = function 77 | ES256 -> "ES256" 78 | ES384 -> "ES384" 79 | ES512 -> "ES512" 80 | EdDSA -> "EdDSA" 81 | HMAC_256_64 -> "HMAC 256/64" 82 | HMAC_256 -> "HMAC 256/256" 83 | HMAC_384 -> "HMAC 384/384" 84 | HMAC_512 -> "HMAC 512/512" 85 86 let all = 87 [ ES256; ES384; ES512; EdDSA; HMAC_256_64; HMAC_256; HMAC_384; HMAC_512 ] 88end 89 90(* COSE Key - RFC 9052 Section 7 *) 91 92module Cose_key = struct 93 type kty = Okp | Ec2 | Symmetric 94 95 (* COSE key labels *) 96 let label_kty = 1 97 let label_kid = 2 98 let label_alg = 3 99 let label_crv = -1 100 let label_x = -2 101 let label_y = -3 102 let label_d = -4 103 let label_k = -1 (* for symmetric *) 104 105 (* COSE key type values *) 106 let kty_okp = 1 107 let kty_ec2 = 2 108 let kty_symmetric = 4 109 110 (* COSE curve values *) 111 let crv_p256 = 1 112 let crv_p384 = 2 113 let crv_p521 = 3 114 let crv_ed25519 = 6 115 116 type key_data = 117 | Symmetric_key of { k : string } 118 | Ed25519_pub of { x : string } 119 | Ed25519_priv of { x : string; d : string } 120 | P256_pub of { x : string; y : string } 121 | P256_priv of { x : string; y : string; d : string } 122 | P384_pub of { x : string; y : string } 123 | P384_priv of { x : string; y : string; d : string } 124 | P521_pub of { x : string; y : string } 125 | P521_priv of { x : string; y : string; d : string } 126 127 type t = { 128 key_data : key_data; 129 kid : string option; 130 alg : Algorithm.t option; 131 } 132 133 let symmetric k = { key_data = Symmetric_key { k }; kid = None; alg = None } 134 135 let ed25519_pub x = 136 { key_data = Ed25519_pub { x }; kid = None; alg = Some Algorithm.EdDSA } 137 138 let ed25519_priv ~pub ~priv = 139 { 140 key_data = Ed25519_priv { x = pub; d = priv }; 141 kid = None; 142 alg = Some Algorithm.EdDSA; 143 } 144 145 let p256_pub ~x ~y = 146 { key_data = P256_pub { x; y }; kid = None; alg = Some Algorithm.ES256 } 147 148 let p256_priv ~x ~y ~d = 149 { key_data = P256_priv { x; y; d }; kid = None; alg = Some Algorithm.ES256 } 150 151 let p384_pub ~x ~y = 152 { key_data = P384_pub { x; y }; kid = None; alg = Some Algorithm.ES384 } 153 154 let p384_priv ~x ~y ~d = 155 { key_data = P384_priv { x; y; d }; kid = None; alg = Some Algorithm.ES384 } 156 157 let p521_pub ~x ~y = 158 { key_data = P521_pub { x; y }; kid = None; alg = Some Algorithm.ES512 } 159 160 let p521_priv ~x ~y ~d = 161 { key_data = P521_priv { x; y; d }; kid = None; alg = Some Algorithm.ES512 } 162 163 let kty t = 164 match t.key_data with 165 | Symmetric_key _ -> Symmetric 166 | Ed25519_pub _ | Ed25519_priv _ -> Okp 167 | P256_pub _ | P256_priv _ | P384_pub _ | P384_priv _ | P521_pub _ 168 | P521_priv _ -> 169 Ec2 170 171 let kid t = t.kid 172 let alg t = t.alg 173 let with_kid id t = { t with kid = Some id } 174 let with_alg a t = { t with alg = Some a } 175 176 (* Helper to build CBOR map pairs *) 177 let int_key k = Cbort.Cbor.Int (Z.of_int k) 178 179 (* CBOR encoding/decoding for COSE keys *) 180 let of_cbor bytes = 181 match Cbort.decode_string Cbort.any bytes with 182 | Error e -> Error (cbort_error_to_error e) 183 | Ok cbor -> 184 let find_int key = Cbort.Cbor.find (int_key key) cbor in 185 let find_bytes key = 186 match find_int key with 187 | Some (Cbort.Cbor.Bytes s) -> Some s 188 | _ -> None 189 in 190 (* kid can be Text or Bytes per RFC 9052 *) 191 let find_kid key = 192 match find_int key with 193 | Some (Cbort.Cbor.Bytes s) -> Some s 194 | Some (Cbort.Cbor.Text s) -> Some s 195 | _ -> None 196 in 197 let get_int_value = function 198 | Some (Cbort.Cbor.Int z) -> Some (Z.to_int z) 199 | _ -> None 200 in 201 let kty_val = get_int_value (find_int label_kty) in 202 let crv_val = get_int_value (find_int label_crv) in 203 let kid = find_kid label_kid in 204 let alg = 205 match get_int_value (find_int label_alg) with 206 | None -> None 207 | Some n -> ( 208 match Algorithm.of_cose_int n with 209 | Ok a -> Some a 210 | Error _ -> None) 211 in 212 let x = find_bytes label_x in 213 let y = find_bytes label_y in 214 let d = find_bytes label_d in 215 let k = find_bytes label_k in 216 let key_data = 217 match (kty_val, crv_val, x, y, d, k) with 218 | Some 4, _, _, _, _, Some k -> Ok (Symmetric_key { k }) 219 | Some 1, Some 6, Some x, _, None, _ -> Ok (Ed25519_pub { x }) 220 | Some 1, Some 6, Some x, _, Some d, _ -> Ok (Ed25519_priv { x; d }) 221 | Some 2, Some 1, Some x, Some y, None, _ -> Ok (P256_pub { x; y }) 222 | Some 2, Some 1, Some x, Some y, Some d, _ -> 223 Ok (P256_priv { x; y; d }) 224 | Some 2, Some 2, Some x, Some y, None, _ -> Ok (P384_pub { x; y }) 225 | Some 2, Some 2, Some x, Some y, Some d, _ -> 226 Ok (P384_priv { x; y; d }) 227 | Some 2, Some 3, Some x, Some y, None, _ -> Ok (P521_pub { x; y }) 228 | Some 2, Some 3, Some x, Some y, Some d, _ -> 229 Ok (P521_priv { x; y; d }) 230 | _ -> 231 Error (Invalid_cose "unsupported or invalid COSE key structure") 232 in 233 Result.map (fun key_data -> { key_data; kid; alg }) key_data 234 235 let to_cbor t = 236 let pairs = ref [] in 237 let add k v = pairs := (int_key k, v) :: !pairs in 238 let add_bytes k s = add k (Cbort.Cbor.Bytes s) in 239 let add_int k i = add k (Cbort.Cbor.Int (Z.of_int i)) in 240 241 (* kty - always present *) 242 (match t.key_data with 243 | Symmetric_key _ -> add_int label_kty kty_symmetric 244 | Ed25519_pub _ | Ed25519_priv _ -> add_int label_kty kty_okp 245 | _ -> add_int label_kty kty_ec2); 246 247 (* kid (optional) *) 248 Option.iter (fun kid -> add_bytes label_kid kid) t.kid; 249 250 (* alg (optional) *) 251 Option.iter (fun alg -> add_int label_alg (Algorithm.to_cose_int alg)) t.alg; 252 253 (* Key-type specific parameters *) 254 (match t.key_data with 255 | Symmetric_key { k } -> add_bytes label_k k 256 | Ed25519_pub { x } -> 257 add_int label_crv crv_ed25519; 258 add_bytes label_x x 259 | Ed25519_priv { x; d } -> 260 add_int label_crv crv_ed25519; 261 add_bytes label_x x; 262 add_bytes label_d d 263 | P256_pub { x; y } -> 264 add_int label_crv crv_p256; 265 add_bytes label_x x; 266 add_bytes label_y y 267 | P256_priv { x; y; d } -> 268 add_int label_crv crv_p256; 269 add_bytes label_x x; 270 add_bytes label_y y; 271 add_bytes label_d d 272 | P384_pub { x; y } -> 273 add_int label_crv crv_p384; 274 add_bytes label_x x; 275 add_bytes label_y y 276 | P384_priv { x; y; d } -> 277 add_int label_crv crv_p384; 278 add_bytes label_x x; 279 add_bytes label_y y; 280 add_bytes label_d d 281 | P521_pub { x; y } -> 282 add_int label_crv crv_p521; 283 add_bytes label_x x; 284 add_bytes label_y y 285 | P521_priv { x; y; d } -> 286 add_int label_crv crv_p521; 287 add_bytes label_x x; 288 add_bytes label_y y; 289 add_bytes label_d d); 290 291 Cbort.encode_string Cbort.any (Cbort.Cbor.Map (List.rev !pairs)) 292end 293 294(* CWT Claims - RFC 8392 Section 3 *) 295 296module Claims = struct 297 (* Claim keys (integers per RFC 8392) *) 298 let key_iss = 1 299 let key_sub = 2 300 let key_aud = 3 301 let key_exp = 4 302 let key_nbf = 5 303 let key_iat = 6 304 let key_cti = 7 305 306 type claim_key = Int_key of int | String_key of string 307 308 type t = { 309 iss : string option; 310 sub : string option; 311 aud : string list; 312 exp : Ptime.t option; 313 nbf : Ptime.t option; 314 iat : Ptime.t option; 315 cti : string option; 316 custom : (claim_key * Cbort.Cbor.t) list; 317 } 318 319 let iss t = t.iss 320 let sub t = t.sub 321 let aud t = t.aud 322 let exp t = t.exp 323 let nbf t = t.nbf 324 let iat t = t.iat 325 let cti t = t.cti 326 327 let get_int_key key t = 328 List.find_map 329 (function Int_key k, v when k = key -> Some v | _ -> None) 330 t.custom 331 332 let get_string_key key t = 333 List.find_map 334 (function String_key k, v when k = key -> Some v | _ -> None) 335 t.custom 336 337 type builder = t 338 339 let empty = 340 { 341 iss = None; 342 sub = None; 343 aud = []; 344 exp = None; 345 nbf = None; 346 iat = None; 347 cti = None; 348 custom = []; 349 } 350 351 let set_iss v t = { t with iss = Some v } 352 let set_sub v t = { t with sub = Some v } 353 let set_aud v t = { t with aud = v } 354 let set_exp v t = { t with exp = Some v } 355 let set_nbf v t = { t with nbf = Some v } 356 let set_iat v t = { t with iat = Some v } 357 let set_cti v t = { t with cti = Some v } 358 359 let set_int_key key value t = 360 { t with custom = (Int_key key, value) :: t.custom } 361 362 let set_string_key key value t = 363 { t with custom = (String_key key, value) :: t.custom } 364 365 let build t = t 366 367 (* Standard claim keys *) 368 let standard_keys = 369 [ key_iss; key_sub; key_aud; key_exp; key_nbf; key_iat; key_cti ] 370 371 (* Helper to convert claim_key to CBOR *) 372 let claim_key_to_cbor = function 373 | Int_key i -> Cbort.Cbor.Int (Z.of_int i) 374 | String_key s -> Cbort.Cbor.Text s 375 376 (* Helper to find value by integer key in CBOR map *) 377 let find_int_key key pairs = 378 let target = Cbort.Cbor.Int (Z.of_int key) in 379 List.find_map 380 (fun (k, v) -> if Cbort.Cbor.equal k target then Some v else None) 381 pairs 382 383 (* Helper to extract string from CBOR *) 384 let cbor_to_string = function Cbort.Cbor.Text s -> Some s | _ -> None 385 386 (* Helper to extract bytes from CBOR *) 387 let cbor_to_bytes = function Cbort.Cbor.Bytes s -> Some s | _ -> None 388 389 (* Helper to extract ptime from CBOR integer *) 390 let cbor_to_ptime = function 391 | Cbort.Cbor.Int z -> Ptime.of_float_s (Z.to_float z) 392 | _ -> None 393 394 (* Helper to extract audience (string or array of strings) *) 395 let cbor_to_aud = function 396 | Cbort.Cbor.Text s -> Some [ s ] 397 | Cbort.Cbor.Array items -> 398 let strings = List.filter_map cbor_to_string items in 399 if List.length strings = List.length items then Some strings else None 400 | _ -> None 401 402 (* Decode claims from CBOR map pairs *) 403 let decode_from_pairs pairs = 404 let iss = Option.bind (find_int_key key_iss pairs) cbor_to_string in 405 let sub = Option.bind (find_int_key key_sub pairs) cbor_to_string in 406 let aud = 407 Option.value ~default:[] 408 (Option.bind (find_int_key key_aud pairs) cbor_to_aud) 409 in 410 let exp = Option.bind (find_int_key key_exp pairs) cbor_to_ptime in 411 let nbf = Option.bind (find_int_key key_nbf pairs) cbor_to_ptime in 412 let iat = Option.bind (find_int_key key_iat pairs) cbor_to_ptime in 413 let cti = Option.bind (find_int_key key_cti pairs) cbor_to_bytes in 414 (* Collect custom claims (non-standard keys) *) 415 let custom = 416 List.filter_map 417 (fun (k, v) -> 418 match k with 419 | Cbort.Cbor.Int z -> 420 let i = Z.to_int z in 421 if List.mem i standard_keys then None else Some (Int_key i, v) 422 | Cbort.Cbor.Text s -> Some (String_key s, v) 423 | _ -> None) 424 pairs 425 in 426 { iss; sub; aud; exp; nbf; iat; cti; custom } 427 428 (* Encode claims to CBOR map pairs *) 429 let encode_to_pairs t = 430 let open Cbort.Cbor in 431 let pairs = ref [] in 432 let add_int k v = pairs := (Int (Z.of_int k), v) :: !pairs in 433 (* Standard claims *) 434 Option.iter (fun v -> add_int key_iss (Text v)) t.iss; 435 Option.iter (fun v -> add_int key_sub (Text v)) t.sub; 436 (match t.aud with 437 | [] -> () 438 | [ s ] -> add_int key_aud (Text s) 439 | lst -> add_int key_aud (Array (List.map (fun s -> Text s) lst))); 440 Option.iter 441 (fun v -> add_int key_exp (Int (Z.of_float (Ptime.to_float_s v)))) 442 t.exp; 443 Option.iter 444 (fun v -> add_int key_nbf (Int (Z.of_float (Ptime.to_float_s v)))) 445 t.nbf; 446 Option.iter 447 (fun v -> add_int key_iat (Int (Z.of_float (Ptime.to_float_s v)))) 448 t.iat; 449 Option.iter (fun v -> add_int key_cti (Bytes v)) t.cti; 450 (* Custom claims *) 451 List.iter 452 (fun (k, v) -> pairs := (claim_key_to_cbor k, v) :: !pairs) 453 t.custom; 454 List.rev !pairs 455 456 let claims_not_map_error = "claims must be a CBOR map" 457 458 (** Full codec for claims including custom claims *) 459 let codec : t Cbort.t = 460 Cbort.conv 461 (fun cbor -> 462 match cbor with 463 | Cbort.Cbor.Map pairs -> Ok (decode_from_pairs pairs) 464 | _ -> Error claims_not_map_error) 465 (fun t -> Cbort.Cbor.Map (encode_to_pairs t)) 466 Cbort.any 467 468 let of_cbor bytes = 469 match Cbort.decode_string codec bytes with 470 | Ok t -> Ok t 471 | Error e -> 472 (* Distinguish CBOR parse errors from claims structure errors *) 473 let msg = Cbort.Error.to_string e in 474 if msg = claims_not_map_error then Error (Invalid_claims msg) 475 else Error (Invalid_cbor msg) 476 477 let to_cbor t = Cbort.encode_string codec t 478end 479 480(* CWT Token *) 481 482(* COSE tags *) 483let cose_sign1_tag = 18 484let cose_mac0_tag = 17 485 486(* COSE header labels *) 487let header_alg = 1 488let header_kid = 4 489 490type t = { 491 claims : Claims.t; 492 algorithm : Algorithm.t option; 493 kid : string option; 494 protected_header : string; (* CBOR-encoded protected header *) 495 signature : string; (* Signature or MAC tag *) 496 raw : string; (* Original CBOR bytes *) 497} 498 499let claims t = t.claims 500let algorithm t = t.algorithm 501let kid t = t.kid 502let raw t = t.raw 503 504(** Extract kid from header - can be Text or Bytes per RFC 9052 *) 505let extract_kid_from_header pairs = 506 let kid_key = Cbort.Cbor.Int (Z.of_int header_kid) in 507 List.find_map 508 (fun (k, v) -> 509 if Cbort.Cbor.equal k kid_key then 510 match v with 511 | Cbort.Cbor.Bytes s -> Some s 512 | Cbort.Cbor.Text s -> Some s 513 | _ -> None 514 else None) 515 pairs 516 517(** Decode protected header to extract algorithm and kid *) 518let decode_protected_header bytes = 519 match Cbort.decode_string Cbort.any bytes with 520 | Error _ -> (None, None) 521 | Ok (Cbort.Cbor.Map pairs) -> 522 let alg_key = Cbort.Cbor.Int (Z.of_int header_alg) in 523 let alg_int = 524 List.find_map 525 (fun (k, v) -> 526 if Cbort.Cbor.equal k alg_key then 527 match v with Cbort.Cbor.Int z -> Some (Z.to_int z) | _ -> None 528 else None) 529 pairs 530 in 531 let algorithm = 532 Option.bind alg_int (fun n -> 533 match Algorithm.of_cose_int n with 534 | Ok alg -> Some alg 535 | Error _ -> None) 536 in 537 let kid = extract_kid_from_header pairs in 538 (algorithm, kid) 539 | Ok _ -> (None, None) 540 541(** Extract kid from unprotected header if present *) 542let decode_unprotected_header cbor = 543 match cbor with 544 | Cbort.Cbor.Map pairs -> extract_kid_from_header pairs 545 | _ -> None 546 547let parse bytes = 548 match Cbort.decode_string Cbort.any bytes with 549 | Error e -> Error (cbort_error_to_error e) 550 | Ok cbor -> ( 551 (* Handle optional COSE tag and extract the array *) 552 let cose_array = 553 match cbor with 554 | Cbort.Cbor.Tag (18, arr) -> Some arr (* COSE_Sign1 *) 555 | Cbort.Cbor.Tag (17, arr) -> Some arr (* COSE_Mac0 *) 556 | Cbort.Cbor.Array _ as arr -> Some arr (* Untagged *) 557 | _ -> None 558 in 559 match cose_array with 560 | None -> 561 Error (Invalid_cose "expected COSE_Sign1 or COSE_Mac0 structure") 562 | Some 563 (Cbort.Cbor.Array 564 [ protected_bstr; unprotected; payload_bstr; sig_bstr ]) -> ( 565 (* Extract byte strings *) 566 let protected_header = 567 match protected_bstr with Cbort.Cbor.Bytes s -> Some s | _ -> None 568 in 569 let signature = 570 match sig_bstr with Cbort.Cbor.Bytes s -> Some s | _ -> None 571 in 572 match (protected_header, signature) with 573 | Some protected_header, Some signature -> ( 574 (* Decode protected header for algorithm and kid *) 575 let algorithm, protected_kid = 576 decode_protected_header protected_header 577 in 578 (* Decode unprotected header for kid - prefer unprotected over protected *) 579 let unprotected_kid = decode_unprotected_header unprotected in 580 let kid = 581 match unprotected_kid with 582 | Some _ -> unprotected_kid 583 | None -> protected_kid 584 in 585 (* Decode claims from payload - handle detached payloads *) 586 match payload_bstr with 587 | Cbort.Cbor.Null -> 588 (* Detached payload: not currently supported *) 589 Error (Invalid_cose "detached payloads are not supported") 590 | Cbort.Cbor.Bytes payload -> ( 591 match Claims.of_cbor payload with 592 | Error e -> Error e 593 | Ok claims -> 594 Ok 595 { 596 claims; 597 algorithm; 598 kid; 599 protected_header; 600 signature; 601 raw = bytes; 602 }) 603 | _ -> 604 Error (Invalid_cose "payload must be a byte string or null")) 605 | _ -> Error (Invalid_cose "invalid COSE structure fields")) 606 | Some (Cbort.Cbor.Array _) -> 607 Error (Invalid_cose "COSE structure must have exactly 4 elements") 608 | Some _ -> Error (Invalid_cose "expected COSE array structure")) 609 610(* Cryptographic operations *) 611 612let hmac_sign alg key payload = 613 let module Hash = Digestif in 614 match alg with 615 | Algorithm.HMAC_256_64 -> 616 let mac = Hash.SHA256.hmac_string ~key payload in 617 Ok (String.sub (Hash.SHA256.to_raw_string mac) 0 8) 618 | Algorithm.HMAC_256 -> 619 let mac = Hash.SHA256.hmac_string ~key payload in 620 Ok (Hash.SHA256.to_raw_string mac) 621 | Algorithm.HMAC_384 -> 622 let mac = Hash.SHA384.hmac_string ~key payload in 623 Ok (Hash.SHA384.to_raw_string mac) 624 | Algorithm.HMAC_512 -> 625 let mac = Hash.SHA512.hmac_string ~key payload in 626 Ok (Hash.SHA512.to_raw_string mac) 627 | _ -> Error (Key_type_mismatch "Not an HMAC algorithm") 628 629let hmac_verify alg key payload expected_mac = 630 match hmac_sign alg key payload with 631 | Error _ -> false 632 | Ok computed -> Eqaf.equal computed expected_mac 633 634let p256_sign ~priv payload = 635 match Mirage_crypto_ec.P256.Dsa.priv_of_octets priv with 636 | Error _ -> Error (Key_type_mismatch "Invalid P-256 private key") 637 | Ok priv -> 638 let hash = Digestif.SHA256.(digest_string payload |> to_raw_string) in 639 let r, s = Mirage_crypto_ec.P256.Dsa.sign ~key:priv hash in 640 let pad32 s = 641 let len = String.length s in 642 if len >= 32 then String.sub s (len - 32) 32 643 else String.make (32 - len) '\x00' ^ s 644 in 645 Ok (pad32 r ^ pad32 s) 646 647let p384_sign ~priv payload = 648 match Mirage_crypto_ec.P384.Dsa.priv_of_octets priv with 649 | Error _ -> Error (Key_type_mismatch "Invalid P-384 private key") 650 | Ok priv -> 651 let hash = Digestif.SHA384.(digest_string payload |> to_raw_string) in 652 let r, s = Mirage_crypto_ec.P384.Dsa.sign ~key:priv hash in 653 let pad48 s = 654 let len = String.length s in 655 if len >= 48 then String.sub s (len - 48) 48 656 else String.make (48 - len) '\x00' ^ s 657 in 658 Ok (pad48 r ^ pad48 s) 659 660let p521_sign ~priv payload = 661 match Mirage_crypto_ec.P521.Dsa.priv_of_octets priv with 662 | Error _ -> Error (Key_type_mismatch "Invalid P-521 private key") 663 | Ok priv -> 664 let hash = Digestif.SHA512.(digest_string payload |> to_raw_string) in 665 let r, s = Mirage_crypto_ec.P521.Dsa.sign ~key:priv hash in 666 let pad66 s = 667 let len = String.length s in 668 if len >= 66 then String.sub s (len - 66) 66 669 else String.make (66 - len) '\x00' ^ s 670 in 671 Ok (pad66 r ^ pad66 s) 672 673let ed25519_sign ~priv payload = 674 match Mirage_crypto_ec.Ed25519.priv_of_octets priv with 675 | Error _ -> Error (Key_type_mismatch "Invalid Ed25519 private key") 676 | Ok priv -> Ok (Mirage_crypto_ec.Ed25519.sign ~key:priv payload) 677 678(** Build Sig_structure or MAC_structure for COSE operations *) 679let build_sig_structure ~context_string ~protected_header ~payload = 680 let open Cbort.Cbor in 681 Array 682 [ 683 Text context_string; 684 Bytes protected_header; 685 Bytes ""; 686 (* external_aad = empty *) 687 Bytes payload; 688 ] 689 |> Cbort.encode_string Cbort.any 690 691(** Expected signature/MAC length for each algorithm *) 692let expected_sig_length = function 693 | Algorithm.ES256 -> 64 (* 32 + 32 *) 694 | Algorithm.ES384 -> 96 (* 48 + 48 *) 695 | Algorithm.ES512 -> 132 (* 66 + 66 *) 696 | Algorithm.EdDSA -> 64 697 | Algorithm.HMAC_256_64 -> 8 698 | Algorithm.HMAC_256 -> 32 699 | Algorithm.HMAC_384 -> 48 700 | Algorithm.HMAC_512 -> 64 701 702let verify ~key ?allowed_algs t = 703 (* Check algorithm is allowed *) 704 let alg = 705 match t.algorithm with 706 | None -> Error (Invalid_cose "No algorithm in protected header") 707 | Some a -> Ok a 708 in 709 match alg with 710 | Error e -> Error e 711 | Ok alg -> 712 let allowed = 713 match allowed_algs with None -> Algorithm.all | Some l -> l 714 in 715 if not (List.mem alg allowed) then 716 Error (Algorithm_not_allowed (Algorithm.to_string alg)) 717 else 718 (* Validate signature length before attempting to parse it *) 719 let expected_len = expected_sig_length alg in 720 let actual_len = String.length t.signature in 721 if actual_len <> expected_len then 722 Error 723 (Invalid_cose 724 (Printf.sprintf "signature length mismatch: expected %d, got %d" 725 expected_len actual_len)) 726 else 727 (* Build Sig_structure or MAC_structure for verification *) 728 let context_string = 729 match alg with 730 | Algorithm.HMAC_256_64 | Algorithm.HMAC_256 | Algorithm.HMAC_384 731 | Algorithm.HMAC_512 -> 732 "MAC0" 733 | _ -> "Signature1" 734 in 735 let payload = Claims.to_cbor t.claims in 736 let sig_structure = 737 build_sig_structure ~context_string 738 ~protected_header:t.protected_header ~payload 739 in 740 (* Verify based on algorithm - returns Result to distinguish key mismatch from sig failure *) 741 let verify_result = 742 match (alg, key.Cose_key.key_data) with 743 | ( ( Algorithm.HMAC_256_64 | Algorithm.HMAC_256 744 | Algorithm.HMAC_384 | Algorithm.HMAC_512 ), 745 Cose_key.Symmetric_key { k } ) -> 746 if hmac_verify alg k sig_structure t.signature then Ok () 747 else Error Signature_mismatch 748 | ( Algorithm.EdDSA, 749 (Cose_key.Ed25519_pub { x } | Cose_key.Ed25519_priv { x; _ }) ) 750 -> ( 751 match Mirage_crypto_ec.Ed25519.pub_of_octets x with 752 | Ok pub -> 753 if 754 Mirage_crypto_ec.Ed25519.verify ~key:pub t.signature 755 ~msg:sig_structure 756 then Ok () 757 else Error Signature_mismatch 758 | Error _ -> 759 Error (Key_type_mismatch "Invalid Ed25519 public key")) 760 | ( Algorithm.ES256, 761 (Cose_key.P256_pub { x; y } | Cose_key.P256_priv { x; y; _ }) ) 762 -> ( 763 match 764 Mirage_crypto_ec.P256.Dsa.pub_of_octets ("\x04" ^ x ^ y) 765 with 766 | Ok pub -> 767 let hash = 768 Digestif.SHA256.( 769 digest_string sig_structure |> to_raw_string) 770 in 771 let r = String.sub t.signature 0 32 in 772 let s = String.sub t.signature 32 32 in 773 if Mirage_crypto_ec.P256.Dsa.verify ~key:pub (r, s) hash 774 then Ok () 775 else Error Signature_mismatch 776 | Error _ -> 777 Error (Key_type_mismatch "Invalid P-256 public key")) 778 | ( Algorithm.ES384, 779 (Cose_key.P384_pub { x; y } | Cose_key.P384_priv { x; y; _ }) ) 780 -> ( 781 match 782 Mirage_crypto_ec.P384.Dsa.pub_of_octets ("\x04" ^ x ^ y) 783 with 784 | Ok pub -> 785 let hash = 786 Digestif.SHA384.( 787 digest_string sig_structure |> to_raw_string) 788 in 789 let r = String.sub t.signature 0 48 in 790 let s = String.sub t.signature 48 48 in 791 if Mirage_crypto_ec.P384.Dsa.verify ~key:pub (r, s) hash 792 then Ok () 793 else Error Signature_mismatch 794 | Error _ -> 795 Error (Key_type_mismatch "Invalid P-384 public key")) 796 | ( Algorithm.ES512, 797 (Cose_key.P521_pub { x; y } | Cose_key.P521_priv { x; y; _ }) ) 798 -> ( 799 match 800 Mirage_crypto_ec.P521.Dsa.pub_of_octets ("\x04" ^ x ^ y) 801 with 802 | Ok pub -> 803 let hash = 804 Digestif.SHA512.( 805 digest_string sig_structure |> to_raw_string) 806 in 807 let r = String.sub t.signature 0 66 in 808 let s = String.sub t.signature 66 66 in 809 if Mirage_crypto_ec.P521.Dsa.verify ~key:pub (r, s) hash 810 then Ok () 811 else Error Signature_mismatch 812 | Error _ -> 813 Error (Key_type_mismatch "Invalid P-521 public key")) 814 | _ -> 815 Error 816 (Key_type_mismatch 817 (Printf.sprintf "Key type doesn't match algorithm %s" 818 (Algorithm.to_string alg))) 819 in 820 verify_result 821 822let validate ~now ?iss ?aud ?leeway t = 823 let leeway = Option.value leeway ~default:Ptime.Span.zero in 824 (* Check exp *) 825 let check_exp () = 826 match t.claims.exp with 827 | Some exp -> ( 828 match Ptime.add_span exp leeway with 829 | Some exp' when Ptime.is_later now ~than:exp' -> Error Token_expired 830 | _ -> Ok ()) 831 | None -> Ok () 832 in 833 (* Check nbf *) 834 let check_nbf () = 835 match t.claims.nbf with 836 | Some nbf -> ( 837 match Ptime.sub_span nbf leeway with 838 | Some nbf' when Ptime.is_earlier now ~than:nbf' -> 839 Error Token_not_yet_valid 840 | _ -> Ok ()) 841 | None -> Ok () 842 in 843 (* Check iss *) 844 let check_iss () = 845 match iss with 846 | Some expected_iss -> ( 847 match t.claims.iss with 848 | Some actual_iss when actual_iss = expected_iss -> Ok () 849 | _ -> Error Invalid_issuer) 850 | None -> Ok () 851 in 852 (* Check aud *) 853 let check_aud () = 854 match aud with 855 | Some expected_aud -> 856 if List.mem expected_aud t.claims.aud then Ok () 857 else Error Invalid_audience 858 | None -> Ok () 859 in 860 match check_exp () with 861 | Error _ as e -> e 862 | Ok () -> ( 863 match check_nbf () with 864 | Error _ as e -> e 865 | Ok () -> ( 866 match check_iss () with Error _ as e -> e | Ok () -> check_aud ())) 867 868let verify_and_validate ~key ~now ?allowed_algs ?iss ?aud ?leeway t = 869 match verify ~key ?allowed_algs t with 870 | Error _ as e -> e 871 | Ok () -> validate ~now ?iss ?aud ?leeway t 872 873(** Encode protected header as CBOR map *) 874let encode_protected_header algorithm = 875 let open Cbort.Cbor in 876 Map 877 [ 878 ( Int (Z.of_int header_alg), 879 Int (Z.of_int (Algorithm.to_cose_int algorithm)) ); 880 ] 881 |> Cbort.encode_string Cbort.any 882 883(** Encode COSE_Sign1 or COSE_Mac0 structure *) 884let encode_cose_message ~cose_tag ~protected_header ~payload ~signature = 885 Cbort.Cbor.Tag 886 ( cose_tag, 887 Cbort.Cbor.Array 888 [ 889 Cbort.Cbor.Bytes protected_header; 890 Cbort.Cbor.Map []; 891 (* unprotected header - empty *) 892 Cbort.Cbor.Bytes payload; 893 Cbort.Cbor.Bytes signature; 894 ] ) 895 |> Cbort.encode_string Cbort.any 896 897let create ~algorithm ~claims ~key = 898 (* Encode protected header *) 899 let protected_header = encode_protected_header algorithm in 900 901 (* Build Sig_structure or MAC_structure *) 902 let context_string = 903 match algorithm with 904 | Algorithm.HMAC_256_64 | Algorithm.HMAC_256 | Algorithm.HMAC_384 905 | Algorithm.HMAC_512 -> 906 "MAC0" 907 | _ -> "Signature1" 908 in 909 let payload = Claims.to_cbor claims in 910 let to_be_signed = 911 build_sig_structure ~context_string ~protected_header ~payload 912 in 913 914 (* Sign or MAC *) 915 let signature_result = 916 match (algorithm, key.Cose_key.key_data) with 917 | ( ( Algorithm.HMAC_256_64 | Algorithm.HMAC_256 | Algorithm.HMAC_384 918 | Algorithm.HMAC_512 ), 919 Cose_key.Symmetric_key { k } ) -> 920 hmac_sign algorithm k to_be_signed 921 | Algorithm.EdDSA, Cose_key.Ed25519_priv { d; _ } -> 922 ed25519_sign ~priv:d to_be_signed 923 | Algorithm.ES256, Cose_key.P256_priv { d; _ } -> 924 p256_sign ~priv:d to_be_signed 925 | Algorithm.ES384, Cose_key.P384_priv { d; _ } -> 926 p384_sign ~priv:d to_be_signed 927 | Algorithm.ES512, Cose_key.P521_priv { d; _ } -> 928 p521_sign ~priv:d to_be_signed 929 | _ -> Error (Key_type_mismatch "Key type doesn't match algorithm") 930 in 931 932 match signature_result with 933 | Error e -> Error e 934 | Ok signature -> 935 (* Encode COSE_Sign1 or COSE_Mac0 structure *) 936 let cose_tag = 937 match algorithm with 938 | Algorithm.HMAC_256_64 | Algorithm.HMAC_256 | Algorithm.HMAC_384 939 | Algorithm.HMAC_512 -> 940 cose_mac0_tag 941 | _ -> cose_sign1_tag 942 in 943 let raw = 944 encode_cose_message ~cose_tag ~protected_header ~payload ~signature 945 in 946 Ok 947 { 948 claims; 949 algorithm = Some algorithm; 950 kid = key.Cose_key.kid; 951 protected_header; 952 signature; 953 raw; 954 } 955 956let encode t = t.raw 957 958let is_expired ~now ?leeway t = 959 match t.claims.exp with 960 | None -> false 961 | Some exp -> ( 962 let leeway = Option.value leeway ~default:Ptime.Span.zero in 963 match Ptime.add_span exp leeway with 964 | Some exp' -> Ptime.is_later now ~than:exp' 965 | None -> true) 966 967let time_to_expiry ~now t = 968 match t.claims.exp with 969 | None -> None 970 | Some exp -> 971 let diff = Ptime.diff exp now in 972 if Ptime.Span.compare diff Ptime.Span.zero <= 0 then None else Some diff