diff --git a/CHANGES.md b/CHANGES.md index ee05a39..349df4e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,10 @@ +next +-------------- +- Support all serialization formats, previously only the compact serialization was supported, now we support both general and flattened JSON format (by @ulrikstrid) +- Add support for ES384 (P-384 with SHA384) (by @ulrikstrid) +- Allow creating a JWK from X509 keys directly (by @ulrikstrid) +- Support extra headers (by @ulrikstrid) + 0.8.2 -------------- - JWS now properly checks the signature. Reported by @nankeen and fixed by @ulrikstrid. CVE-2023-23928 diff --git a/jose/Header.ml b/jose/Header.ml index 0803303..5d08e41 100644 --- a/jose/Header.ml +++ b/jose/Header.ml @@ -2,7 +2,6 @@ open Utils type t = { alg : Jwa.alg; - jku : string option; jwk : Jwk.public Jwk.t option; kid : string option; x5t : string option; @@ -10,9 +9,19 @@ type t = { typ : string option; cty : string option; enc : Jwa.enc option; + extra : (string * Yojson.Safe.t) list option; } -let make_header ?typ ?alg ?enc (jwk : Jwk.priv Jwk.t) = +(* TODO: This is probably very slow *) +let remove_supported (l : (string * Yojson.Safe.t) list) = + l |> List.remove_assoc "alg" |> List.remove_assoc "jwk" + |> List.remove_assoc "kid" |> List.remove_assoc "x5t" + |> List.remove_assoc "x5t#256" + |> List.remove_assoc "typ" |> List.remove_assoc "cty" + |> List.remove_assoc "enc" + +let make_header ?typ ?alg ?enc ?(extra = []) ?(jwk_header = false) + (jwk : Jwk.priv Jwk.t) = let alg = match alg with | Some alg -> alg @@ -21,28 +30,41 @@ let make_header ?typ ?alg ?enc (jwk : Jwk.priv Jwk.t) = | Jwk.Rsa_priv _ -> `RS256 | Jwk.Oct _ -> `HS256 | Jwk.Es256_priv _ -> `ES256 + | Jwk.Es384_priv _ -> `ES384 | Jwk.Es512_priv _ -> `ES512) in + let kid = + match List.assoc_opt "kid" extra with + | Some kid -> Some (Yojson.Safe.Util.to_string kid) + | None -> Jwk.get_kid jwk + in + let extra = remove_supported extra in { alg; - jku = None; - jwk = None; - kid = Jwk.get_kid jwk; + jwk = (if jwk_header then Some (Jwk.pub_of_priv jwk) else None); + kid; x5t = None; x5t256 = None; typ; cty = None; enc; + extra = (match extra with [] -> None | extra -> Some extra); } module Json = Yojson.Safe.Util +let get_extra_headers (json : Yojson.Safe.t) = + match json with + | `Assoc vals -> ( + let extra = remove_supported vals in + match extra with [] -> None | extra -> Some extra) + | _ -> None (* TODO: raise here? *) + let of_json json = try Ok { alg = json |> Json.member "alg" |> Jwa.alg_of_json; - jku = json |> Json.member "jku" |> Json.to_string_option; jwk = json |> Json.member "jwk" |> Json.to_option (fun jwk_json -> @@ -56,6 +78,7 @@ let of_json json = enc = json |> Json.member "enc" |> Json.to_string_option |> U_Opt.map Jwa.enc_of_string; + extra = get_extra_headers json; } with Json.Type_error (s, _) -> Error (`Msg s) @@ -64,17 +87,18 @@ let to_json t = [ RJson.to_json_string_opt "typ" t.typ; Some ("alg", Jwa.alg_to_json t.alg); - RJson.to_json_string_opt "jku" t.jku; - U_Opt.map Jwk.to_pub_json t.jwk |> U_Opt.map (fun jwk -> ("jwk", jwk)); RJson.to_json_string_opt "kid" t.kid; + U_Opt.map Jwk.to_pub_json t.jwk |> U_Opt.map (fun jwk -> ("jwk", jwk)); RJson.to_json_string_opt "x5t" t.x5t; RJson.to_json_string_opt "x5t#256" t.x5t256; RJson.to_json_string_opt "cty" t.cty; - t.enc |> U_Opt.map Jwa.enc_to_string + t.enc + |> U_Opt.map Jwa.enc_to_string |> U_Opt.map (fun enc -> ("enc", `String enc)); ] in - `Assoc (U_List.filter_map (fun x -> x) values) + let extra = Option.value ~default:[] t.extra in + `Assoc (U_List.filter_map (fun x -> x) values @ extra) let of_string header_str = U_Base64.url_decode header_str diff --git a/jose/Jose.mli b/jose/Jose.mli index 04f93b2..ca12989 100644 --- a/jose/Jose.mli +++ b/jose/Jose.mli @@ -6,6 +6,7 @@ module Jwa : sig [ `RS256 (** HMAC using SHA-256 *) | `HS256 (** RSASSA-PKCS1-v1_5 using SHA-256 *) | `ES256 (** ECDSA using P-256 and SHA-256 *) + | `ES384 (** ECDSA using P-384 and SHA-384 *) | `ES512 (** ECDSA using P-521 and SHA-512 *) | `RSA_OAEP (** RSAES OAEP using default parameters *) | `RSA1_5 (** RSA PKCS 1 *) @@ -79,6 +80,12 @@ module Jwk : sig type pub_es256 = Mirage_crypto_ec.P256.Dsa.pub jwk (** [es256] represents a private JWK with [kty] [`EC] and a [P256.priv] key *) + type priv_es384 = Mirage_crypto_ec.P384.Dsa.priv jwk + (** [es384] represents a public JWK with [kty] [`EC] and a [P384.pub] key *) + + type pub_es384 = Mirage_crypto_ec.P384.Dsa.pub jwk + (** [es384] represents a private JWK with [kty] [`EC] and a [P384.priv] key *) + type priv_es512 = Mirage_crypto_ec.P521.Dsa.priv jwk (** [es512] represents a public JWK with [kty] [`EC] and a [P512.pub] key *) @@ -92,6 +99,8 @@ module Jwk : sig | Rsa_pub : pub_rsa -> public t | Es256_priv : priv_es256 -> priv t | Es256_pub : pub_es256 -> public t + | Es384_priv : priv_es384 -> priv t + | Es384_pub : pub_es384 -> public t | Es512_priv : priv_es512 -> priv t | Es512_pub : pub_es512 -> public t @@ -154,6 +163,16 @@ module Jwk : sig (** [to_priv_pem t] takes a JWK and returns a result PEM string or a message of what went wrong. *) + val of_priv_x509 : + ?use:use -> + X509.Private_key.t -> + (priv t, [> `Msg of string | `Not_rsa ]) result + + val of_pub_x509 : + ?use:use -> + X509.Public_key.t -> + (public t, [> `Msg of string | `Not_rsa ]) result + val of_priv_json : Yojson.Safe.t -> ( priv t, @@ -234,7 +253,6 @@ end module Header : sig type t = { alg : Jwa.alg; - jku : string option; jwk : Jwk.public Jwk.t option; kid : string option; x5t : string option; @@ -242,21 +260,32 @@ module Header : sig typ : string option; cty : string option; enc : Jwa.enc option; + extra : (string * Yojson.Safe.t) list option; } - (** The [header] has the following properties: - [alg] Jwa - RS256 and none is - currently the only supported algs - [jku] JWK Set URL - [jwk] JSON Web Key - - [kid] Key ID - We currently always expect this to be there, this can - change in the future - [x5t] X.509 Certificate SHA-1 Thumbprint - - [x5t#S256] X.509 Certificate SHA-256 Thumbprint - [typ] Type - [cty] - Content Type Not implemented: - [x5u] X.509 URL - [x5c] X.509 Certficate - Chain - [crit] Critical + (** The [header] has the following properties: + + - [alg] {! Jwa.alg } + - [jwk] JSON Web Key + - [kid] Key ID - We currently always expect this to be there, this can change in the future + - [x5t] X.509 Certificate SHA-1 Thumbprint - + - [x5t#S256] X.509 Certificate SHA-256 Thumbprint + - [typ] Type + - [cty] Content Type Not implemented + + {{: https://tools.ietf.org/html/rfc7515#section-4.1 } Link to RFC } - {{: https://tools.ietf.org/html/rfc7515#section-4.1 } Link to RFC } *) + {{: https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-header-parameters } Complete list of registered header parameters} *) val make_header : - ?typ:string -> ?alg:Jwa.alg -> ?enc:Jwa.enc -> Jwk.priv Jwk.t -> t + ?typ:string -> + ?alg:Jwa.alg -> + ?enc:Jwa.enc -> + ?extra:(string * Yojson.Safe.t) list -> + ?jwk_header:bool -> + Jwk.priv Jwk.t -> + t (** [make_header typ alg enc jwk] if [alg] is not provided it will be derived - from [jwk]. *) + from [jwk]. [jwk_header] decides if the jwk should be put in the header. *) val of_string : string -> (t, [> `Msg of string ]) result val to_string : t -> string @@ -277,8 +306,12 @@ module Jws : sig signature : signature; } - val of_string : string -> (t, [> `Msg of string ]) result - val to_string : t -> string + type serialization = [ `Compact | `General | `Flattened ] + + val of_string : + string -> (t, [> `Msg of string | `Not_json | `Not_supported ]) result + + val to_string : ?serialization:serialization -> t -> string val validate : jwk:'a Jwk.t -> t -> (t, [> `Invalid_signature | `Msg of string ]) result @@ -315,15 +348,23 @@ module Jwt : sig val get_yojson_claim : t -> string -> Yojson.Safe.t option val get_string_claim : t -> string -> string option val get_int_claim : t -> string -> int option - val to_string : t -> string + val to_string : ?serialization:Jws.serialization -> t -> string val of_string : jwk:'a Jwk.t -> string -> - (t, [> `Expired | `Invalid_signature | `Msg of string ]) result + ( t, + [> `Expired + | `Invalid_signature + | `Msg of string + | `Not_json + | `Not_supported ] ) + result (** [of_string ~jwk jwt_string] parses and validates the encoded JWT string. *) - val unsafe_of_string : string -> (t, [> `Msg of string ]) result + val unsafe_of_string : + string -> (t, [> `Msg of string | `Not_json | `Not_supported ]) result + val to_jws : t -> Jws.t val of_jws : Jws.t -> t diff --git a/jose/Jwa.ml b/jose/Jwa.ml index 92ce2e2..bc0600f 100644 --- a/jose/Jwa.ml +++ b/jose/Jwa.ml @@ -20,6 +20,7 @@ type alg = [ `RS256 (** HMAC using SHA-256 *) | `HS256 (** RSASSA-PKCS1-v1_5 using SHA-256 *) | `ES256 (** ECDSA using P-256 and SHA-256 *) + | `ES384 (** ECDSA using P-384 and SHA-384 *) | `ES512 (** ECDSA using P-521 and SHA-512 *) | `RSA_OAEP (** RSAES OAEP using default parameters *) | `RSA1_5 (** RSA PKCS 1 *) @@ -30,6 +31,7 @@ let alg_to_string = function | `RS256 -> "RS256" | `HS256 -> "HS256" | `ES256 -> "ES256" + | `ES384 -> "ES384" | `ES512 -> "ES512" | `RSA_OAEP -> "RSA-OAEP" | `RSA1_5 -> "RSA1_5" @@ -40,6 +42,7 @@ let alg_of_string = function | "RS256" -> `RS256 | "HS256" -> `HS256 | "ES256" -> `ES256 + | "ES384" -> `ES384 | "ES512" -> `ES512 | "RSA-OAEP" -> `RSA_OAEP | "RSA1_5" -> `RSA1_5 diff --git a/jose/Jwe.ml b/jose/Jwe.ml index 6b0e0a1..4e5052b 100644 --- a/jose/Jwe.ml +++ b/jose/Jwe.ml @@ -104,6 +104,8 @@ let encrypt_cek (type a) alg (cek : string) ~(jwk : a Jwk.t) = | Oct _ -> Error `Unsupported_kty | Es256_priv _ -> Error `Unsupported_kty | Es256_pub _ -> Error `Unsupported_kty + | Es384_priv _ -> Error `Unsupported_kty + | Es384_pub _ -> Error `Unsupported_kty | Es512_priv _ -> Error `Unsupported_kty | Es512_pub _ -> Error `Unsupported_kty) >>= fun key -> diff --git a/jose/Jwk.ml b/jose/Jwk.ml index 6ec256a..e77e310 100644 --- a/jose/Jwk.ml +++ b/jose/Jwk.ml @@ -37,6 +37,24 @@ module Util = struct let k = Mirage_crypto_ec.P256.Dsa.pub_of_cstruct point in k |> U_Result.get_exn) + let get_ES384_x_y key = + let point = Mirage_crypto_ec.P384.Dsa.pub_to_cstruct key in + let x_cs, y_cs = Cstruct.(split (shift point 1) 48) in + let x = x_cs |> Cstruct.to_string |> U_Base64.url_encode_string in + let y = y_cs |> Cstruct.to_string |> U_Base64.url_encode_string in + (x, y) + + let make_ES384_of_x_y (x, y) = + let x = U_Base64.url_decode x |> U_Result.map Cstruct.of_string in + let y = U_Base64.url_decode y |> U_Result.map Cstruct.of_string in + U_Result.both x y + |> U_Result.map (fun (x, y) -> + let four = Cstruct.create 1 in + Cstruct.set_uint8 four 0 4; + let point = Cstruct.concat [ four; x; y ] in + let k = Mirage_crypto_ec.P384.Dsa.pub_of_cstruct point in + k |> U_Result.get_exn) + let get_ES512_x_y key = let point = Mirage_crypto_ec.P521.Dsa.pub_to_cstruct key in let x_cs, y_cs = Cstruct.(split (shift point 1) 66) in @@ -80,6 +98,7 @@ let use_of_alg (alg : Jwa.alg) = | `HS256 -> `Sig | `RS256 -> `Sig | `ES256 -> `Sig + | `ES384 -> `Sig | `ES512 -> `Sig | `RSA_OAEP -> `Enc | `RSA1_5 -> `Enc @@ -102,6 +121,8 @@ type priv_rsa = Mirage_crypto_pk.Rsa.priv jwk type pub_rsa = Mirage_crypto_pk.Rsa.pub jwk type priv_es256 = Mirage_crypto_ec.P256.Dsa.priv jwk type pub_es256 = Mirage_crypto_ec.P256.Dsa.pub jwk +type priv_es384 = Mirage_crypto_ec.P384.Dsa.priv jwk +type pub_es384 = Mirage_crypto_ec.P384.Dsa.pub jwk type priv_es512 = Mirage_crypto_ec.P521.Dsa.priv jwk type pub_es512 = Mirage_crypto_ec.P521.Dsa.pub jwk @@ -111,6 +132,8 @@ type 'a t = | Rsa_pub : pub_rsa -> public t | Es256_priv : priv_es256 -> priv t | Es256_pub : pub_es256 -> public t + | Es384_priv : priv_es384 -> priv t + | Es384_pub : pub_es384 -> public t | Es512_priv : priv_es512 -> priv t | Es512_pub : pub_es512 -> public t @@ -120,6 +143,8 @@ let get_alg (type a) (t : a t) : Jwa.alg option = | Rsa_pub rsa -> rsa.alg | Es256_priv es -> es.alg | Es256_pub es -> es.alg + | Es384_priv es -> es.alg + | Es384_pub es -> es.alg | Es512_priv es -> es.alg | Es512_pub es -> es.alg | Oct oct -> oct.alg @@ -130,6 +155,8 @@ let get_kty (type a) (t : a t) = | Rsa_pub _ -> `RSA | Es256_priv _ -> `EC | Es256_pub _ -> `EC + | Es384_priv _ -> `EC + | Es384_pub _ -> `EC | Es512_priv _ -> `EC | Es512_pub _ -> `EC | Oct _ -> `oct @@ -140,6 +167,8 @@ let get_kid (type a) (t : a t) = | Rsa_pub rsa -> rsa.kid | Es256_priv es -> es.kid | Es256_pub es -> es.kid + | Es384_priv es -> es.kid + | Es384_pub es -> es.kid | Es512_priv es -> es.kid | Es512_pub es -> es.kid | Oct oct -> oct.kid @@ -179,6 +208,28 @@ let make_kid (type a) (t : a t) = ("y", `String y); ] |> Util.kid_of_json + | Es384_priv es -> + let x, y = + Util.get_ES384_x_y (Mirage_crypto_ec.P384.Dsa.pub_of_priv es.key) + in + `Assoc + [ + ("crv", `String "P-384"); + ("kty", `String "EC"); + ("x", `String x); + ("y", `String y); + ] + |> Util.kid_of_json + | Es384_pub es -> + let x, y = Util.get_ES384_x_y es.key in + `Assoc + [ + ("crv", `String "P-384"); + ("kty", `String "EC"); + ("x", `String x); + ("y", `String y); + ] + |> Util.kid_of_json | Es512_priv es -> let x, y = Util.get_ES512_x_y (Mirage_crypto_ec.P521.Dsa.pub_of_priv es.key) @@ -228,6 +279,13 @@ let make_priv_es256 ?use (es256_priv : Mirage_crypto_ec.P256.Dsa.priv) : priv t let jwk = { alg; kty; use; key = es256_priv; kid = None } in Es256_priv { jwk with kid = make_kid (Es256_priv jwk) } +let make_priv_es384 ?use (es384_priv : Mirage_crypto_ec.P384.Dsa.priv) : priv t + = + let kty : Jwa.kty = `EC in + let alg = Some `ES384 in + let jwk = { alg; kty; use; key = es384_priv; kid = None } in + Es384_priv { jwk with kid = make_kid (Es384_priv jwk) } + let make_priv_es512 ?use (es512_priv : Mirage_crypto_ec.P521.Dsa.priv) : priv t = let kty : Jwa.kty = `EC in @@ -241,6 +299,24 @@ let make_pub_rsa ?use (rsa_pub : Mirage_crypto_pk.Rsa.pub) : public t = let jwk = { alg; kty; use; key = rsa_pub; kid = None } in Rsa_pub { jwk with kid = make_kid (Rsa_pub jwk) } +let make_pub_es256 ?use (es256_pub : Mirage_crypto_ec.P256.Dsa.pub) : public t = + let kty : Jwa.kty = `EC in + let alg = Some `ES256 in + let jwk = { alg; kty; use; key = es256_pub; kid = None } in + Es256_pub { jwk with kid = make_kid (Es256_pub jwk) } + +let make_pub_es384 ?use (es384_pub : Mirage_crypto_ec.P384.Dsa.pub) : public t = + let kty : Jwa.kty = `EC in + let alg = Some `ES384 in + let jwk = { alg; kty; use; key = es384_pub; kid = None } in + Es384_pub { jwk with kid = make_kid (Es384_pub jwk) } + +let make_pub_es512 ?use (es512_pub : Mirage_crypto_ec.P521.Dsa.pub) : public t = + let kty : Jwa.kty = `EC in + let alg = Some `ES512 in + let jwk = { alg; kty; use; key = es512_pub; kid = None } in + Es512_pub { jwk with kid = make_kid (Es512_pub jwk) } + let of_pub_pem ?use pem : (public t, [> `Not_rsa ]) result = Cstruct.of_string pem |> X509.Public_key.decode_pem |> U_Result.flat_map (function @@ -272,6 +348,23 @@ let to_priv_pem (jwk : priv t) = Ok (X509.Private_key.encode_pem (`RSA rsa.key) |> Cstruct.to_string) | _ -> Error `Not_rsa +let of_priv_x509 ?use x509 : (priv t, [> `Not_rsa ]) result = + match x509 with + | `RSA priv_key -> Ok (make_priv_rsa ?use priv_key) + | `P256 priv_key -> Ok (make_priv_es256 ?use priv_key) + | `P384 priv_key -> Ok (make_priv_es384 ?use priv_key) + | `P521 priv_key -> Ok (make_priv_es512 ?use priv_key) + | _ -> Error (`Msg "key type not supported") + +let of_pub_x509 ?use (x509 : X509.Public_key.t) : + (public t, [> `Not_rsa ]) result = + match x509 with + | `RSA public_key -> Ok (make_pub_rsa ?use public_key) + | `P256 public_key -> Ok (make_pub_es256 ?use public_key) + | `P384 public_key -> Ok (make_pub_es384 ?use public_key) + | `P521 public_key -> Ok (make_pub_es512 ?use public_key) + | _ -> Error (`Msg "key type not supported") + let oct_to_json (oct : oct) = let values = [ @@ -310,6 +403,9 @@ let pub_of_priv_rsa (priv_rsa : priv_rsa) : pub_rsa = let pub_of_priv_es256 (priv_es256 : priv_es256) : pub_es256 = { priv_es256 with key = Mirage_crypto_ec.P256.Dsa.pub_of_priv priv_es256.key } +let pub_of_priv_es384 (priv_es384 : priv_es384) : pub_es384 = + { priv_es384 with key = Mirage_crypto_ec.P384.Dsa.pub_of_priv priv_es384.key } + let pub_of_priv_es512 (priv_es512 : priv_es512) : pub_es512 = { priv_es512 with key = Mirage_crypto_ec.P521.Dsa.pub_of_priv priv_es512.key } @@ -387,6 +483,48 @@ let priv_es256_to_priv_json (priv_es256 : priv_es256) : Yojson.Safe.t = in `Assoc (U_List.filter_map (fun x -> x) values) +let pub_es384_to_pub_json (pub_es384 : pub_es384) : Yojson.Safe.t = + let x, y = Util.get_ES384_x_y pub_es384.key in + let values = + [ + Option.map (fun alg -> ("alg", Jwa.alg_to_json alg)) pub_es384.alg; + Some ("crv", `String "P-384"); + Some ("x", `String x); + Some ("y", `String y); + Some ("kty", `String (pub_es384.kty |> Jwa.kty_to_string)); + Option.map (fun use -> ("use", `String (use_to_string use))) pub_es384.use; + RJson.to_json_string_opt "kid" pub_es384.kid; + ] + in + `Assoc (U_List.filter_map (fun x -> x) values) + +let priv_es384_to_pub_json (priv_es384 : priv_es384) : Yojson.Safe.t = + pub_of_priv_es384 priv_es384 |> pub_es384_to_pub_json + +let priv_es384_to_priv_json (priv_es384 : priv_es384) : Yojson.Safe.t = + let x, y = + Util.get_ES384_x_y (Mirage_crypto_ec.P384.Dsa.pub_of_priv priv_es384.key) + in + let d = + Mirage_crypto_ec.P384.Dsa.priv_to_cstruct priv_es384.key + |> Cstruct.to_string |> U_Base64.url_encode_string + in + let values = + [ + Option.map (fun alg -> ("alg", Jwa.alg_to_json alg)) priv_es384.alg; + Some ("crv", `String "P-256"); + Some ("x", `String x); + Some ("y", `String y); + Some ("d", `String d); + Some ("kty", `String (priv_es384.kty |> Jwa.kty_to_string)); + Option.map + (fun use -> ("use", `String (use_to_string use))) + priv_es384.use; + RJson.to_json_string_opt "kid" priv_es384.kid; + ] + in + `Assoc (U_List.filter_map (fun x -> x) values) + let pub_es512_to_pub_json (pub_es512 : pub_es512) : Yojson.Safe.t = let x, y = Util.get_ES512_x_y pub_es512.key in let values = @@ -436,6 +574,8 @@ let to_pub_json (type a) (jwk : a t) : Yojson.Safe.t = | Rsa_pub rsa -> pub_rsa_to_json rsa | Es256_priv ec -> priv_es256_to_pub_json ec | Es256_pub ec -> pub_es256_to_pub_json ec + | Es384_priv ec -> priv_es384_to_pub_json ec + | Es384_pub ec -> pub_es384_to_pub_json ec | Es512_priv ec -> priv_es512_to_pub_json ec | Es512_pub ec -> pub_es512_to_pub_json ec @@ -447,6 +587,7 @@ let to_priv_json (jwk : priv t) : Yojson.Safe.t = | Oct oct -> oct_to_json oct | Rsa_priv rsa -> priv_rsa_to_priv_json rsa | Es256_priv ec -> priv_es256_to_priv_json ec + | Es384_priv ec -> priv_es384_to_priv_json ec | Es512_priv ec -> priv_es512_to_priv_json ec let to_priv_json_string (jwk : priv t) : string = @@ -548,7 +689,10 @@ let priv_rsa_of_json json : (priv t, 'error) result = let oct_of_json json = let module Json = Yojson.Safe.Util in try - let alg = Some (json |> Json.member "alg" |> Jwa.alg_of_json) in + let alg = + json |> Json.member "alg" |> Json.to_string_option + |> U_Opt.map Jwa.alg_of_string + in Ok (Oct { @@ -588,6 +732,20 @@ let pub_ec_of_json json = key; kid = json |> Json.member "kid" |> Json.to_string_option; }) + | "P-384" -> + Util.make_ES384_of_x_y (x, y) + |> U_Result.map (fun key -> + Es384_pub + { + alg; + kty = `EC; + (* Shortcut since that is the only thing we handle *) + use = + json |> Json.member "use" |> Json.to_string_option + |> U_Opt.map use_of_string; + key; + kid = json |> Json.member "kid" |> Json.to_string_option; + }) | "P-521" -> Util.make_ES512_of_x_y (x, y) |> U_Result.map (fun key -> @@ -633,6 +791,21 @@ let priv_ec_of_json json = key; kid = json |> Json.member "kid" |> Json.to_string_option; }) + | "P-384", Ok d -> + Mirage_crypto_ec.P384.Dsa.priv_of_cstruct d + |> U_Result.map_error (fun _ -> `Msg "Could not create key") + |> U_Result.map (fun key -> + Es384_priv + { + alg; + kty = `EC; + (* Shortcut since that is the only thing we handle *) + use = + json |> Json.member "use" |> Json.to_string_option + |> U_Opt.map use_of_string; + key; + kid = json |> Json.member "kid" |> Json.to_string_option; + }) | "P-521", Ok d -> Mirage_crypto_ec.P521.Dsa.priv_of_cstruct d |> U_Result.map_error (fun _ -> `Msg "Could not create key") @@ -661,7 +834,8 @@ let of_pub_json (json : Yojson.Safe.t) : (public t, 'error) result = | _ -> Error `Unsupported_kty let of_pub_json_string str : (public t, 'error) result = - Yojson.Safe.from_string str |> of_pub_json + try Yojson.Safe.from_string str |> of_pub_json + with Yojson.Json_error s -> Error (`Json_parse_failed s) let of_priv_json json : (priv t, 'error) result = let module Json = Yojson.Safe.Util in @@ -673,13 +847,15 @@ let of_priv_json json : (priv t, 'error) result = | _ -> Error `Unsupported_kty let of_priv_json_string str : (priv t, 'error) result = - Yojson.Safe.from_string str |> of_priv_json + try Yojson.Safe.from_string str |> of_priv_json + with Yojson.Json_error s -> Error (`Json_parse_failed s) let pub_of_priv (jwk : priv t) : public t = match jwk with | Oct oct -> Oct oct | Rsa_priv rsa -> Rsa_pub (pub_of_priv_rsa rsa) | Es256_priv es -> Es256_pub (pub_of_priv_es256 es) + | Es384_priv es -> Es384_pub (pub_of_priv_es384 es) | Es512_priv es -> Es512_pub (pub_of_priv_es512 es) let oct_to_sign_key (oct : oct) : (Cstruct.t, [> `Msg of string ]) result = @@ -721,6 +897,23 @@ let pub_es256_to_thumbprint hash (pub_es256 : pub_es256) = let priv_es256_to_thumbprint hash (priv_es256 : priv_es256) = pub_of_priv_es256 priv_es256 |> pub_es256_to_thumbprint hash +let pub_es384_to_thumbprint hash (pub_es384 : pub_es384) = + let crv = "P-384" in + let kty = Jwa.kty_to_string pub_es384.kty in + let x, y = Util.get_ES384_x_y pub_es384.key in + let values = + [ + Some ("crv", `String crv); + Some ("kty", `String kty); + Some ("x", `String x); + Some ("y", `String y); + ] + in + hash_values hash values + +let priv_es384_to_thumbprint hash (priv_es384 : priv_es384) = + pub_of_priv_es384 priv_es384 |> pub_es384_to_thumbprint hash + let pub_es512_to_thumbprint hash (pub_es512 : pub_es512) = let crv = "P-521" in let kty = Jwa.kty_to_string pub_es512.kty in @@ -744,6 +937,8 @@ let get_thumbprint (type a) (hash : Mirage_crypto.Hash.hash) (jwk : a t) = | Rsa_priv rsa -> Ok (priv_rsa_to_thumbprint hash rsa) | Es256_pub ec -> Ok (pub_es256_to_thumbprint hash ec) | Es256_priv ec -> Ok (priv_es256_to_thumbprint hash ec) + | Es384_pub ec -> Ok (pub_es384_to_thumbprint hash ec) + | Es384_priv ec -> Ok (priv_es384_to_thumbprint hash ec) | Es512_pub ec -> Ok (pub_es512_to_thumbprint hash ec) | Es512_priv ec -> Ok (priv_es512_to_thumbprint hash ec) | Oct oct -> oct_to_thumbprint hash oct diff --git a/jose/Jws.ml b/jose/Jws.ml index 9df1ef6..24f5072 100644 --- a/jose/Jws.ml +++ b/jose/Jws.ml @@ -3,13 +3,15 @@ open Utils type signature = string type t = { - header : Header.t; + header : Header.t; (* TODO: This is always treated as protected headers*) raw_header : string; payload : string; signature : signature; } -let of_string token = +type serialization = [ `Compact | `General | `Flattened ] + +let of_compact_string token = String.split_on_char '.' token |> function | [ header_str; payload_str; signature ] -> let header = Header.of_string header_str in @@ -19,10 +21,72 @@ let of_string token = { header; raw_header = header_str; payload; signature }) | _ -> Error (`Msg "token didn't include header, payload or signature") -let to_string t = +let of_json_string token = + try + let module Json = Yojson.Safe.Util in + let json = Yojson.Safe.from_string token in + let payload = + Json.member "payload" json |> Json.to_string |> U_Base64.url_decode + in + + match (payload, Json.member "signature" json |> Json.to_string_option) with + | Ok payload, Some signature -> + let protected = Json.member "protected" json |> Json.to_string in + Ok + { + header = Header.of_string protected |> U_Result.get_exn; + raw_header = protected; + payload; + signature; + } + | Error e, _ -> Error e + | _, None -> Error `Not_supported + with _ -> Error `Not_json + +let of_string token = + match of_json_string token with + | Ok t -> Ok t + | Error `Not_json -> of_compact_string token + | e -> e + +let to_flattened_json t = + let payload_str = t.payload |> U_Base64.url_encode_string in + `Assoc + [ + ("payload", `String payload_str); + ("protected", `String t.raw_header); + (* TODO: add "header" for public header parameters *) + ("signature", `String t.signature); + ] + +let to_compact_string t = let payload_str = t.payload |> U_Base64.url_encode_string in Printf.sprintf "%s.%s.%s" t.raw_header payload_str t.signature +let to_general_string t = + let payload_str = t.payload |> U_Base64.url_encode_string in + (* TODO: Support multiple signatures *) + let signatures = + [ + `Assoc + [ + ("protected", `String t.raw_header); + (* TODO: add "header" for public header parameters *) + ("signature", `String t.signature); + ]; + ] + in + `Assoc [ ("payload", `String payload_str); ("signatures", `List signatures) ] + |> Yojson.Safe.to_string + +let to_flattened_string t = to_flattened_json t |> Yojson.Safe.to_string + +let to_string ?(serialization = `Compact) t = + match serialization with + | `Compact -> to_compact_string t + | `General -> to_general_string t + | `Flattened -> to_flattened_string t + let verify_jwk (type a) ~(jwk : a Jwk.t) ~input_str str = match jwk with | Jwk.Rsa_priv jwk -> ( @@ -64,6 +128,23 @@ let verify_jwk (type a) ~(jwk : a Jwk.t) ~input_str str = if Mirage_crypto_ec.P256.Dsa.verify ~key:pub_jwk.key (r, s) message then Ok str else Error `Invalid_signature + | Jwk.Es384_pub pub_jwk -> + let r, s = Cstruct.split str 48 in + let message = + Mirage_crypto.Hash.SHA384.digest (Cstruct.of_string input_str) + in + if Mirage_crypto_ec.P384.Dsa.verify ~key:pub_jwk.key (r, s) message then + Ok str + else Error `Invalid_signature + | Jwk.Es384_priv jwk -> + let r, s = Cstruct.split str 48 in + let message = + Mirage_crypto.Hash.SHA384.digest (Cstruct.of_string input_str) + in + let pub_jwk = Jwk.pub_of_priv_es384 jwk in + if Mirage_crypto_ec.P384.Dsa.verify ~key:pub_jwk.key (r, s) message then + Ok str + else Error `Invalid_signature | Jwk.Es512_pub pub_jwk -> let r, s = Cstruct.split str 66 in let message = @@ -95,6 +176,7 @@ let validate (type a) ~(jwk : a Jwk.t) t = | `RS256 -> Ok header.alg | `HS256 -> Ok header.alg | `ES256 -> Ok header.alg + | `ES384 -> Ok header.alg | `ES512 -> Ok header.alg | `Unsupported _ | `RSA_OAEP | `RSA1_5 | `None -> Error (`Msg "alg not supported for signing")) @@ -122,6 +204,14 @@ let sign ?header ~payload (jwk : Jwk.priv Jwk.t) = let r, s = Mirage_crypto_ec.P256.Dsa.sign ~key message in Cstruct.append r s | `Digest _ -> raise (Invalid_argument "Digest")) + | Jwk.Es384_priv { key; _ } -> + Ok + (function + | `Message x -> + let message = Mirage_crypto.Hash.SHA384.digest x in + let r, s = Mirage_crypto_ec.P384.Dsa.sign ~key message in + Cstruct.append r s + | `Digest _ -> raise (Invalid_argument "Digest")) | Jwk.Es512_priv { key; _ } -> Ok (function diff --git a/jose/Jwt.ml b/jose/Jwt.ml index ee206cf..37f1330 100644 --- a/jose/Jwt.ml +++ b/jose/Jwt.ml @@ -44,30 +44,6 @@ let get_string_claim (jwt : t) (claim_name : string) = let get_int_claim (jwt : t) (claim_name : string) = Option.bind (get_yojson_claim jwt claim_name) Yojson.Safe.Util.to_int_option -let to_string t = - let payload = U_Base64.url_encode_string t.raw_payload in - Printf.sprintf "%s.%s.%s" t.raw_header payload t.signature - -let unsafe_of_string token = - String.split_on_char '.' token |> function - | [ header_str; payload_str; signature ] -> - let header = Header.of_string header_str in - let payload = payload_of_string payload_str in - U_Result.both header payload - |> U_Result.flat_map (fun (header, payload) -> - Ok - { - header; - raw_header = header_str; - payload; - raw_payload = - U_Base64.url_decode payload_str |> U_Result.get_exn; - (* The string is already decoded so this is fine but - redundant *) - signature; - }) - | _ -> Error (`Msg "token didn't include header, payload or signature") - let to_jws (t : t) = Jws. { @@ -87,6 +63,12 @@ let of_jws (jws : Jws.t) = raw_payload = jws.payload; } +let to_string ?serialization t = + let jws = to_jws t in + Jws.to_string ?serialization jws + +let unsafe_of_string token = Jws.of_string token |> U_Result.map of_jws + let check_expiration t = let module Json = Yojson.Safe.Util in match Json.member "exp" t.payload |> Json.to_int_option with diff --git a/test/Fixtures.ml b/test/Fixtures.ml index b7baf85..751e910 100644 --- a/test/Fixtures.ml +++ b/test/Fixtures.ml @@ -153,4 +153,4 @@ let jwk_without_use_and_alg = {|{"e": "AQAB", "kid": "2aff6e30eb11dc76a38ed5d0c1d50fe8d347ffa0cc654edc4a15803f7ae3a784", "kty": "RSA", -"n": "rUSAReHBQZQlb_bvTm8gkhKZhNpOyrTa0VAke2aDoNtDxImGB2cUNXxNDOJDNwzTmcuHCRe5Kx8aTx7crA1j87n6Jt9ygR5pPk_vRPk5uIbXXhVqrrSKAo9lsSGK0s2U8f2eSGVz8xg8UynLBJUJkZ-bs3sMShQW8sq7agbaWyJbZf1l_7BjNmcI_jaXuZh1AtjwvnfOOB8jZYMdZSB3RcVnvkP44KJNtsv1jwCp1nSgMwqFXHmpJTTqxlQf42dGBWbr0MfFQeTNriVPXdGnNReBIy2-1ycNLegvZ3KHE8_lMw9s2woDu1ohe0ims9WuhwBga-WnRdaROAsD3QV66w"}|} \ No newline at end of file +"n": "rUSAReHBQZQlb_bvTm8gkhKZhNpOyrTa0VAke2aDoNtDxImGB2cUNXxNDOJDNwzTmcuHCRe5Kx8aTx7crA1j87n6Jt9ygR5pPk_vRPk5uIbXXhVqrrSKAo9lsSGK0s2U8f2eSGVz8xg8UynLBJUJkZ-bs3sMShQW8sq7agbaWyJbZf1l_7BjNmcI_jaXuZh1AtjwvnfOOB8jZYMdZSB3RcVnvkP44KJNtsv1jwCp1nSgMwqFXHmpJTTqxlQf42dGBWbr0MfFQeTNriVPXdGnNReBIy2-1ycNLegvZ3KHE8_lMw9s2woDu1ohe0ims9WuhwBga-WnRdaROAsD3QV66w"}|} diff --git a/test/Helpers.ml b/test/Helpers.ml index b7ead61..231ce8b 100644 --- a/test/Helpers.ml +++ b/test/Helpers.ml @@ -8,7 +8,9 @@ type 'a error_t = | `Invalid_JWE | `Invalid_JWK | `Decrypt_cek_failed - | `Unsafe ] + | `Unsafe + | `Not_json + | `Not_supported ] as 'a @@ -24,6 +26,8 @@ let result_t : _ error_t Alcotest.testable = | `Invalid_JWK -> Fmt.string ppf "Invalid JWK" | `Decrypt_cek_failed -> Fmt.string ppf "Failed to decrypt cek" | `Unsafe -> Fmt.string ppf "Unsafe" + | `Not_json -> Fmt.string ppf "Not_json" + | `Not_supported -> Fmt.string ppf "Not_supported" in Alcotest.testable pp ( = ) diff --git a/test/JWKTest.ml b/test/JWKTest.ml index 20d1db0..763c31d 100644 --- a/test/JWKTest.ml +++ b/test/JWKTest.ml @@ -171,6 +171,20 @@ let jwk_suite, _ = (Ok "ZrBaai73Hi8Fg4MElvDGzIne2NsbI75RHubOViHYE5Q") @@ Result.map url_encode_cstruct @@ Jose.Jwk.get_thumbprint `SHA256 pub_jwk); + Alcotest.test_case "P384 - thumbprint" `Quick (fun () -> + let pub_string = + {|{ + "crv":"P-384", + "kty":"EC", + "x":"FqTN7UHEy4MLUQvaB31WtfPcBhmzRS2Xl7jVtM3ELvHBQ6l_WrJqryK2gAoDImRl", + "y":"5wlJyPkB7PE2MVdIMoqwclRpnCX3l5w7kIPwE69GGJVMLBxd758jhcptkKVhRjTg" + }|} + in + let pub_jwk = Jose.Jwk.of_pub_json_string pub_string in + check_result_string "Creates the correct thumbprint" + (Ok "CZv-vJviuyEXKGIeW2fYpEjRXSxUTHUdoQ58asby1Rg") + @@ Result.map url_encode_cstruct + @@ CCResult.flat_map (Jose.Jwk.get_thumbprint `SHA256) pub_jwk); Alcotest.test_case "P256 - thumbprint" `Quick (fun () -> let pub_string = {|{ diff --git a/test/JWSTest.ml b/test/JWSTest.ml new file mode 100644 index 0000000..e3761b2 --- /dev/null +++ b/test/JWSTest.ml @@ -0,0 +1,149 @@ +let () = Mirage_crypto_rng_unix.initialize () + +open Helpers + +(* These values are borrowed from the `ocaml-letsencrypt` test suite + https://github.com/mmaker/ocaml-letsencrypt *) +let testkey_pem = + "\n\ + -----BEGIN PRIVATE KEY-----\n\ + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDjGBnd5E+TChG/\n\ + Lup5FRuYwN7RX7Ef15Yt27SKF54uWuYPaapZd8/0h5IoCluffiDxO4BL2DnOGrwQ\n\ + tDeSaOv4pXzoYAyAjpUBwaWrdCvOlMJ//fQBvv7NrCt9FoU41rUfATM9jUoecZWT\n\ + ElzzmA2TBgj1JjZEde2+WffOznAhM2t2iyoRd5oiRVgESFuE27nimneTGjpO5YuL\n\ + 17qld5Z60TCaUHC1ZmU+iJvaPdPEsGSwpl+jIXJ6TfzSYeAkC6ZD8jZ+OP3z/3ua\n\ + TeKE5jgCBV0IOPXP8YKhmQblGrudsIbKizIpbINfTRmz6c2pWGgt4i9cLiedZ0kx\n\ + nquiDHJBAgMBAAECggEABaFh98xKtEe0QbAXOGYPc3m5tIl5teNFmhC30NIt1fKj\n\ + QFfTdUkpDuQjGarLE4DgLnb2EvtTEJL9XXEobRD8o8Mvnf/Oo4vVcjATzFTSprot\n\ + udhpKbdrcBxADkeGCU8aecCw/WpQv4E7rwQuKYx4LrBgPbrDLu6ZFMZ8hEQ+R7Zn\n\ + j0jWswOZEwM5xNHZ8RlwP4xsyFChvBR43lymHwDwQegd7ukbY0OcwXZ+2sxcKltr\n\ + LBZKKFPzMugKnMbZtwm3TRIUTDGjB+IZGU7dPXgF8cK4KR4yDRZ5HKIZWbqxCPCP\n\ + 6TphI+Jz83OxpXU9R8rfPgUhnBgqwTdDpc5pGfmyiQKBgQD+I1TKDW5tF0fXWnza\n\ + Xwoe0ULUM8TRXWBJmxfb1OkzmNLiq/jor6zxibXOas5EzzH5zKd8/HVVBlDfgRh4\n\ + IwhfbXavIn7MMBOXg0TQjia4y9KIf2/HpdzsWaE2dpjM+wEvlOb2ea1C4/T1gSfy\n\ + miI4kWIOz/iiWcPmiADk7hMcaQKBgQDkwgupZgFS6psRYtG0yu5S2kBJyWsGo02w\n\ + kSwwZt6oEmagzF0d5JlyRss6uqbsaUzI1Ek17/m5ZEZLNoxi4abCw+kRHOoS9gWd\n\ + KumNbli1dn4m3EVc1V+b1nWAsuC8ak5QIhRFumgNyQN7W+BS6TfLn4ONmKGz6uog\n\ + njlfNdPMGQKBgFa5/ex6Cu4lnLmsQqFO/6gmp5S9GfSM1hgoWksF7JNUGtuJ7oaR\n\ + tQY0hZusrTmkL5zcr2eiy/O5FQ5BAvW0lt3iADeiIP1ThswU2v4FFMfJns5AFwhd\n\ + 3Pe3WqG4dUq2eeAgA3Wnbm4+VtEVQ2myGe2OB5WgeWwGEClyzkNRz6nJAoGAPN4c\n\ + +D/6DjP6es/OeMqeS1FjVb7QSX3eSCL4nRBiIlpzEEoQZMnUwoFvxfqwO6txEObb\n\ + bAykZ930jkK/a/gaxSwXscP9zHnF2KH4bvdzhyU2P+TQV/k2bWLM9SejgL7Qg6Xt\n\ + uvf0g+Z+lK5HrAf+HqIdAOoh7JuPHIq9PUY3StECgYEAoYP7hkj8TUygnkJcHxwM\n\ + MwdqBsTdyr8O2ZjMTa/UMWlBi7kjg8KblzsRB4g/p1m2/wgyC0Yhv3VBf2le8/Rr\n\ + OfNArBggDydmCgQ0I9+IxM+IQNP17/SU5s71daxeltJOxE+PSy/WsH5TMEnQ+CMr\n\ + irbM4XSw2jtvX7qeUzcFY/E=\n\ + -----END PRIVATE KEY-----\n" + +let testkey_jwk = Jose.Jwk.of_priv_pem testkey_pem |> Result.get_ok + +let expected_protected = + "eyJhbGciOiJSUzI1NiIsImtpZCI6IjZuaWN4emg2V0VUUWxydmRj" + ^ "aGt6LVUzZTNET1FaNGhlSktVNjNyZnFNcVEiLCJqd2siOnsiZSI6" + ^ "IkFRQUIiLCJuIjoiNHhnWjNlUlBrd29Sdnk3cWVSVWJtTURlMFYt" + ^ "eEg5ZVdMZHUwaWhlZUxscm1EMm1xV1hmUDlJZVNLQXBibjM0ZzhU" + ^ "dUFTOWc1emhxOEVMUTNrbWpyLUtWODZHQU1nSTZWQWNHbHEzUXJ6" + ^ "cFRDZl8zMEFiNy16YXdyZlJhRk9OYTFId0V6UFkxS0huR1ZreEpj" + ^ "ODVnTmt3WUk5U1kyUkhYdHZsbjN6czV3SVROcmRvc3FFWGVhSWtW" + ^ "WUJFaGJoTnU1NHBwM2t4bzZUdVdMaTllNnBYZVdldEV3bWxCd3RX" + ^ "WmxQb2liMmozVHhMQmtzS1pmb3lGeWVrMzgwbUhnSkF1bVFfSTJm" + ^ "amo5OF85N21rM2loT1k0QWdWZENEajF6X0dDb1prRzVScTduYkNH" + ^ "eW9zeUtXeURYMDBacy1uTnFWaG9MZUl2WEM0bm5XZEpNWjZyb2d4" + ^ "eVFRIiwia3R5IjoiUlNBIiwia2lkIjoiNm5pY3h6aDZXRVRRbHJ2" + ^ "ZGNoa3otVTNlM0RPUVo0aGVKS1U2M3JmcU1xUSIsIng1dCI6Ijk4" + ^ "WEZNbUZxRWtrb0RudTdHSjhjRFdGaTJJWSJ9LCJub25jZSI6Im5v" ^ "bmNlIn0" + +let expected_payload = "eyJNc2ciOiJIZWxsbyBKV1MifQ" +let expected_decoded_payload = {|{"Msg":"Hello JWS"}|} + +let expected_signature = + "qv79C1SFoz_7EWt7WVIhg5kVBPbCK__Xa1kFtodtS7hD78KvRQrU" + ^ "Cx4Usa5T6PrFKmutXumyArjW3RxwRa1ATKo7g8k-F0TeUELXsZic" + ^ "fLs_5jHu8vj3g47_mlhjMg9oJ6YNDVdhg3Gm19ZXgm6W_WlnM8wC" + ^ "2dUVVSVYLxP7Hk2b6urM_tXJ3HtWRHbmQtD8hxQaMCNzz99usPvA" + ^ "I1SW5b-I1rK0dxIOZ205Kce4VtLgEVs9hz45b4t93-g0bP1clHCU" + ^ "iNKf-vzOs_45H1EKkxEpGDO5fQkeNfoQxTsE03AnB9SZXiF-ApDW" + ^ "QMz_4f3YJ9YhRVB1iXx9vgAMkqhTaQ" + +let jws_suite, _ = + Junit_alcotest.run_and_report ~package:"jose" "JWS" + [ + ( "JWS", + [ + Alcotest.test_case "fail to parse {}" `Quick (fun () -> + let jws_string = "{}" in + let jws_result = Jose.Jws.of_string jws_string in + check_result_string "failing to parse" + (Result.map (fun (jws : Jose.Jws.t) -> jws.payload) jws_result) + (Error + (`Msg "token didn't include header, payload or signature"))); + Alcotest.test_case "parses a flattened json representation correctly" + `Quick (fun () -> + let jws_string = + Printf.sprintf + {|{"protected": "%s", "payload": "%s", "signature": "%s"}|} + expected_protected expected_payload expected_signature + in + let validated = + Jose.Jws.of_string jws_string + |> CCResult.flat_map (Jose.Jws.validate ~jwk:testkey_jwk) + in + check_result_string "Correct signature" + (Result.map (fun (jws : Jose.Jws.t) -> jws.signature) validated) + (Ok expected_signature); + check_result_string "Correct payload" + (Result.map (fun (jws : Jose.Jws.t) -> jws.payload) validated) + (Ok expected_decoded_payload)); + Alcotest.test_case "Produces the same output" `Quick (fun () -> + let header = + Jose.Header.make_header + ~extra:[ ("nonce", `String "nonce") ] + ~jwk_header:true testkey_jwk + in + let jws = + Jose.Jws.sign ~header ~payload:expected_decoded_payload + testkey_jwk + in + let jws_string = + Result.map (Jose.Jws.to_string ~serialization:`Flattened) jws + in + let expected_jws_string = + Printf.sprintf + {|{"payload":"%s","protected":"%s","signature":"%s"}|} + expected_payload expected_protected expected_signature + in + check_result_string "matches original jws" + (Ok expected_jws_string) jws_string); + Alcotest.test_case "Roundtrip with ES384" `Quick (fun () -> + let priv_string = + {|{ + "alg":"ES384", + "crv":"P-384", + "x":"rxz9m2FeRvvTE7_lSSSLve2c_ZkXxAasRId4jLqzIlsud19DtF52LOn91mQTRP9Y", + "y":"3_G1QTpidcws41ep1nLoc--6fHQjPXgu-oVuZhXB7VSihC3nLrF4irfhlB8cmTsa", + "d":"9eZFD1YrsUj5yQKj5u3Rju-Wx4JPL1TGXDWS1zE8AvYAmz_1Hp62R_oTtk1H7ARH", + "kty":"EC", + "kid":"W1X4opFJerkT7BFhQaf1-A5fRZTBJBmuJwerrUEcU4c" + }|} + in + let jwk = + Jose.Jwk.of_priv_json_string priv_string |> Result.get_ok + in + let jws = Jose.Jws.sign ~payload:"hello" jwk in + let jws_string = Result.map Jose.Jws.to_string jws in + let validated = + jws_string + |> CCResult.flat_map Jose.Jws.of_string + |> CCResult.flat_map (Jose.Jws.validate ~jwk) + in + let pub_jwk = Jose.Jwk.pub_of_priv jwk in + let _validated = + CCResult.flat_map (Jose.Jws.validate ~jwk:pub_jwk) jws + in + + check_result_string "Correct payload" (Ok "hello") + (Result.map (fun (jws : Jose.Jws.t) -> jws.payload) validated)); + ] ); + ] + +let jws_suite = jws_suite diff --git a/test/JWTTest.ml b/test/JWTTest.ml index d4d1fca..1297200 100644 --- a/test/JWTTest.ml +++ b/test/JWTTest.ml @@ -205,35 +205,6 @@ let jwt_suite, _ = in check_string "JWT was parsed correctly without kid" "RS256" (jwt.header.alg |> Jose.Jwa.alg_to_string)); - Alcotest.test_case "rfc7515 A.3" `Quick (fun () -> - let jwk_str = - {|{"kty":"EC", -"crv":"P-256", -"x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", -"y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0", -"d":"jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI" -}|} - in - let payload_str = - {|{"iss":"joe", -"exp":1300819380, -"http://example.com/is_root":true -}|} - in - let expected_str = - {|eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q|} - in - let jwk = - Jose.Jwk.of_priv_json_string jwk_str |> CCResult.get_exn - in - Jose.Jwt.unsafe_of_string expected_str - |> CCResult.flat_map (Jose.Jwt.validate_signature ~jwk) - |> CCResult.map (fun (jwt : Jose.Jwt.t) -> - Yojson.Safe.to_string jwt.payload) - |> check_result_string "Validated payload is correct" - (Ok - (payload_str |> Yojson.Safe.from_string - |> Yojson.Safe.to_string))); Alcotest.test_case "Checks for expiration when calling `of_string`" `Quick (fun () -> let open Jose in diff --git a/test/RFC7515.ml b/test/RFC7515.ml new file mode 100644 index 0000000..602a134 --- /dev/null +++ b/test/RFC7515.ml @@ -0,0 +1,144 @@ +(* These tests are based on rfc7515, https://tools.ietf.org/html/rfc7515 *) + +let oct_priv_json = + {|{"kty":"oct", +"k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow" +}|} + +let rsa_priv_json = + {|{"kty":"RSA", +"n":"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddxHmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMsD1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSHSXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ", +"e":"AQAB", +"d":"Eq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyWPEAeP88vLNO97IjlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYTCBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLhBOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ", +"p":"4BzEEOtIpmVdVEZNCqS7baC4crd0pqnRH_5IB3jw3bcxGn6QLvnEtfdUdiYrqBdss1l58BQ3KhooKeQTa9AB0Hw_Py5PJdTJNPY8cQn7ouZ2KKDcmnPGBY5t7yLc1QlQ5xHdwW1VhvKn-nXqhJTBgIPgtldC-KDV5z-y2XDwGUc", +"q":"uQPEfgmVtjL0Uyyx88GZFF1fOunH3-7cepKmtH4pxhtCoHqpWmT8YAmZxaewHgHAjLYsp1ZSe7zFYHj7C6ul7TjeLQeZD_YwD66t62wDmpe_HlB-TnBA-njbglfIsRLtXlnDzQkv5dTltRJ11BKBBypeeF6689rjcJIDEz9RWdc", +"dp":"BwKfV3Akq5_MFZDFZCnW-wzl-CCo83WoZvnLQwCTeDv8uzluRSnm71I3QCLdhrqE2e9YkxvuxdBfpT_PI7Yz-FOKnu1R6HsJeDCjn12Sk3vmAktV2zb34MCdy7cpdTh_YVr7tss2u6vneTwrA86rZtu5Mbr1C1XsmvkxHQAdYo0", +"dq":"h_96-mK1R_7glhsum81dZxjTnYynPbZpHziZjeeHcXYsXaaMwkOlODsWa7I9xXDoRwbKgB719rrmI2oKr6N3Do9U0ajaHF-NKJnwgjMd2w9cjz3_-kyNlxAr2v4IKhGNpmM5iIgOS1VZnOZ68m6_pbLBSp3nssTdlqvd0tIiTHU", +"qi":"IYd7DHOhrWvxkwPQsRM2tOgrjbcrfvtQJipd-DlcxyVuuM9sQLdgjVk2oy26F0EmpScGLq2MowX7fhd_QJQ3ydy5cY7YIBi87w93IKLEdfnbJtoOPLUW0ITrJReOgo1cq9SbsxYawBgfp_gh6A5603k2-ZQwVK0JKSHuLFkuQ3U" + }|} + +let ec_priv_json_es256 = + {|{"kty":"EC", +"kid":"e9bc097a-ce51-4036-9562-d2ade882db0d", +"crv":"P-256", +"x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", +"y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0", +"d":"jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI" +}|} + +let ec_priv_json_es512 = + {|{"kty":"EC", +"crv":"P-521", +"x":"AekpBQ8ST8a8VcfVOTNl353vSrDCLLJXmPk06wTjxrrjcBpXp5EOnYG_NjFZ6OvLFV1jSfS9tsz4qUxcWceqwQGk", +"y":"ADSmRA43Z1DSNx_RvcLI87cdL07l6jQyyBXMoxVg_l2Th-x3S1WDhjDly79ajL4Kkd0AZMaZmh9ubmf63e3kyMj2", +"d":"AY5pb7A0UFiB3RELSD64fTLOSV_jazdF7fLYyuTw8lOfRhWg6Y6rUrPAxerEzgdRhajnu0ferB0d53vM9mE15j2C" +}|} + +(* The real value has \r\n and I can't make that happen *) +let payload_to_same payload = + Yojson.Safe.from_string payload |> Yojson.Safe.to_string + +let payload_str = + {|{"iss":"joe", +"exp":1300819380, +"http://example.com/is_root":true}|} + |> payload_to_same + +open Helpers + +let jws_tests = + ( "RFC7515", + [ + Alcotest.test_case "A.1" `Quick (fun () -> + let expected_str = + {|eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk|} + in + let jwk = + Jose.Jwk.of_priv_json_string oct_priv_json |> CCResult.get_exn + in + Jose.Jws.of_string expected_str + |> CCResult.flat_map (Jose.Jws.validate ~jwk) + |> CCResult.map (fun (jws : Jose.Jws.t) -> + payload_to_same jws.payload) + |> check_result_string "Validated payload is correct" (Ok payload_str)); + Alcotest.test_case "A.2" `Quick (fun () -> + let expected_str = + {|eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw|} + in + let jwk = + Jose.Jwk.of_priv_json_string rsa_priv_json |> CCResult.get_exn + in + Jose.Jws.of_string expected_str + |> CCResult.flat_map (Jose.Jws.validate ~jwk) + |> CCResult.map (fun (jws : Jose.Jws.t) -> + payload_to_same jws.payload) + |> check_result_string "Validated payload is correct" (Ok payload_str)); + Alcotest.test_case "A.3" `Quick (fun () -> + let expected_str = + {|eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q|} + in + let jwk = + Jose.Jwk.of_priv_json_string ec_priv_json_es256 |> CCResult.get_exn + in + Jose.Jws.of_string expected_str + |> CCResult.flat_map (Jose.Jws.validate ~jwk) + |> CCResult.map (fun (jws : Jose.Jws.t) -> + payload_to_same jws.payload) + |> check_result_string "Validated payload is correct" (Ok payload_str)); + Alcotest.test_case "A.4" `Quick (fun () -> + let expected_str = + {|eyJhbGciOiJFUzUxMiJ9.UGF5bG9hZA.AdwMgeerwtHoh-l192l60hp9wAHZFVJbLfD_UxMi70cwnZOYaRI1bKPWROc-mZZqwqT2SI-KGDKB34XO0aw_7XdtAG8GaSwFKdCAPZgoXD2YBJZCPEX3xKpRwcdOO8KpEHwJjyqOgzDO7iKvU8vcnwNrmxYbSW9ERBXukOXolLzeO_Jn|} + in + let jwk = + Jose.Jwk.of_priv_json_string ec_priv_json_es512 |> CCResult.get_exn + in + Jose.Jws.of_string expected_str + |> CCResult.flat_map (Jose.Jws.validate ~jwk) + |> CCResult.map (fun (jws : Jose.Jws.t) -> jws.payload) + |> check_result_string "Validated payload is correct" (Ok "Payload")); + (* We currently do not support `none` *) + Alcotest.test_case "A.5" `Quick (fun () -> + let expected_str = + {|eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.|} + in + let jwk = + Jose.Jwk.of_priv_json_string ec_priv_json_es256 |> CCResult.get_exn + in + Jose.Jws.of_string expected_str + |> CCResult.flat_map (Jose.Jws.validate ~jwk) + |> CCResult.map (fun (jws : Jose.Jws.t) -> + payload_to_same jws.payload) + |> check_result_string "Validated payload is correct" + (Error (`Msg "alg not supported for signing"))); + (* A.6 uses multiple signatures which we don't support yet *) + Alcotest.test_case "A.7" `Quick (fun () -> + let header = + Jose.Header. + { + alg = `ES256; + jwk = None; + kid = None; + x5t = None; + x5t256 = None; + typ = None; + cty = None; + enc = None; + extra = None; + } + in + let jwk = + Jose.Jwk.of_priv_json_string ec_priv_json_es256 |> CCResult.get_exn + in + Jose.Jws.sign ~header ~payload:payload_str jwk + |> CCResult.map (Jose.Jws.to_string ~serialization:`Flattened) + |> check_result_string "Validated payload is correct" + (* We currently don't have a notion of Unprotected Headers Values so this is not exactly correct*) + (Ok + {|{"payload":"eyJpc3MiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ","protected":"eyJhbGciOiJFUzI1NiJ9","signature":"4XSeJsUDLhZisF7Vhx7iYI_q9x7a3Mk8-wsj-jpf39DRe-bDEt-w7UlN1xwfpiouuoGssgJKAT9GwEeORjzuIg"}|})); + ] ) + +(* Begin tests *) +let rfc_suite, _ = + Junit_alcotest.run_and_report ~package:"jose" "RFC7515" [ jws_tests ] + +let suite = rfc_suite diff --git a/test/test.ml b/test/test.ml index 8a83878..b38829c 100644 --- a/test/test.ml +++ b/test/test.ml @@ -5,8 +5,10 @@ let () = [ JWKsTest.jwks_suite; JWKTest.jwk_suite; + JWSTest.jws_suite; JWTTest.jwt_suite; JWETest.jwe_suite; + RFC7515.suite; RFC7520.suite; RFC7638.suite; ]