package jose

  1. Overview
  2. Docs

Source file Jwe.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
open Utils
(** {{:https://tools.ietf.org/html/rfc7516} Link to RFC} *)

type t = {
  header : Header.t;
  cek : string;
  iv : string;
  payload : string;
  aad : string option;
}

module RSA_OAEP = Mirage_crypto_pk.Rsa.OAEP (Digestif.SHA1)

(*
Steps to create a JWE

* Generate a random Content Encryption Key (CEK)
* Encrypt the CEK with the recipient's public key using the RSAES OAEP algorithm to produce the JWE Encrypted Key.
* Base64url encode the JWE Encrypted Key to produce the Encoded JWE Encrypted Key.
* Generate a random JWE Initialization Vector.
* Base64url encode the JWE Initialization Vector to produce the Encoded JWE Initialization Vector.
* Let the Additional Authenticated Data encryption parameter be the octets of the ASCII representation of the Encoded JWE Header value.
* Encrypt the Plaintext with AES GCM using the CEK as the encryption key, the JWE Initialization Vector, and the Additional Authenticated Data value, requesting a 128 bit Authentication Tag output.
* Base64url encode the Ciphertext to create the Encoded JWE Ciphertext.
* Base64url encode the Authentication Tag to create the Encoded JWE Authentication Tag.
* Assemble the final representation: The Compact Serialization of this result is the concatenation of the Encoded JWE Header, the Encoded JWE Encrypted Key, the Encoded JWE Initialization Vector, the Encoded JWE Ciphertext, and the Encoded JWE Authentication Tag in that order, with the five strings being separated by four period ('.') characters.
*)

let make_cek (header : Header.t) =
  match header.enc with
  | Some enc ->
      let key_length = Jwa.enc_to_length enc in
      Mirage_crypto_rng.generate (key_length / 8) |> Result.ok
  | None -> Error `Missing_enc

let make_iv (header : Header.t) =
  match header.alg with
  | `RSA_OAEP ->
      Ok (Mirage_crypto_rng.generate Mirage_crypto.AES.GCM.block_size)
  | `RSA1_5 -> Ok (Mirage_crypto_rng.generate Mirage_crypto.AES.CBC.block_size)
  | _ -> Error `Unsupported_alg

let make ~header payload =
  let cek = make_cek header in
  Result.bind cek (fun cek ->
      let iv = make_iv header in
      Result.bind iv (fun iv ->
          let aad = None in
          Ok { header; cek; iv; aad; payload }))

let encrypt_payload ?enc ~cek ~iv ~aad payload =
  match enc with
  | Some `A128CBC_HS256 ->
      (* RFC 7516 appendix B.1: first 128 bit hmac, last 128 bit aes *)
      let hmac_key, aes_key =
        U_String.split cek Mirage_crypto.AES.CBC.block_size
      in
      let key = Mirage_crypto.AES.CBC.of_secret aes_key in
      (* B.2 encryption in CBC mode *)
      Mirage_crypto.AES.CBC.encrypt ~key ~iv
        (Pkcs7.pad payload Mirage_crypto.AES.CBC.block_size)
      |> fun data ->
      (* B.5 input to HMAC computation *)
      let hmac_input =
        (* B.3 64 bit big-endian AAD length (in bits!) *)
        let aal = Bytes.create 8 in
        Bytes.set_int64_be aal 0 Int64.(mul 8L (of_int (String.length aad)));
        String.concat "" [ aad; iv; data; Bytes.unsafe_to_string aal ]
      in
      let computed_auth_tag =
        let full =
          Digestif.SHA256.hmac_string ~key:hmac_key hmac_input
          |> Digestif.SHA256.to_raw_string
        in
        (* B.7 truncate to 128 bit *)
        String.sub full 0 16
      in
      Ok (data, computed_auth_tag)
  | Some `A256GCM ->
      let module GCM = Mirage_crypto.AES.GCM in
      let key = GCM.of_secret cek in
      let adata = aad in
      GCM.authenticate_encrypt ~key ~nonce:iv ~adata payload |> fun cdata ->
      let cipher, tag_data =
        U_String.split cdata (String.length cdata - GCM.tag_size)
      in
      Ok (cipher, tag_data)
  | None -> Error `Missing_enc
  | _ -> Error `Unsupported_enc

let encrypt_cek (type a) alg (cek : string) ~(jwk : a Jwk.t) =
  let key =
    match jwk with
    | Rsa_priv rsa -> Ok (Mirage_crypto_pk.Rsa.pub_of_priv rsa.key)
    | Rsa_pub rsa -> Ok rsa.key
    | 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
    | Ed25519_priv _ -> Error `Unsupported_kty
    | Ed25519_pub _ -> Error `Unsupported_kty
  in
  Result.bind key (fun key ->
      match alg with
      | `RSA1_5 ->
          let ecek = Mirage_crypto_pk.Rsa.PKCS1.encrypt ~key cek in
          Ok ecek
      | `RSA_OAEP ->
          let jek = RSA_OAEP.encrypt ~key cek in
          Ok jek
      | _ -> Error `Invalid_alg)

let encrypt (type a) ~(jwk : a Jwk.t) t =
  let header_string = Header.to_string t.header in
  let ecek =
    encrypt_cek t.header.alg t.cek ~jwk |> Result.map U_Base64.url_encode_string
  in
  Result.bind ecek (fun ecek ->
      let eiv = U_Base64.url_encode_string t.iv in
      let ciphertext =
        encrypt_payload ?enc:t.header.enc ~cek:t.cek ~iv:t.iv ~aad:header_string
          t.payload
      in
      Result.bind ciphertext (fun (ciphertext, auth_tag) ->
          Ok
            (String.concat "."
               [
                 header_string;
                 ecek;
                 eiv;
                 U_Base64.url_encode_string ciphertext;
                 U_Base64.url_encode_string auth_tag;
               ])))

let decrypt_cek alg str ~(jwk : Jwk.priv Jwk.t) =
  let of_opt_string = function
    | Some c -> Ok c
    | None -> Error `Decrypt_cek_failed
  in
  match (alg, jwk) with
  | `RSA1_5, Jwk.Rsa_priv rsa ->
      let decoded =
        Utils.U_Base64.url_decode str
        |> Result.map (Mirage_crypto_pk.Rsa.PKCS1.decrypt ~key:rsa.key)
      in
      Result.bind decoded of_opt_string
  | `RSA_OAEP, Jwk.Rsa_priv rsa ->
      let decoded =
        Utils.U_Base64.url_decode str
        |> Result.map (RSA_OAEP.decrypt ~key:rsa.key)
      in
      Result.bind decoded of_opt_string
  | _ -> Error `Invalid_JWK

(* Move to Jwa? *)
let decrypt_ciphertext enc ~cek ~iv ~auth_tag ~aad ciphertext =
  let encrypted = U_Base64.url_decode ciphertext in
  Result.bind encrypted (fun encrypted ->
      match enc with
      | Some `A128CBC_HS256 ->
          (* RFC 7516 appendix B.1: first 128 bit hmac, last 128 bit aes *)
          let hmac_key, aes_key = U_String.split cek 16 in
          let key = Mirage_crypto.AES.CBC.of_secret aes_key in

          (* B.5 input to HMAC computation *)
          let hmac_input =
            (* B.3 64 bit big-endian AAD length (in bits!) *)
            let aal = Bytes.create 8 in
            Bytes.set_int64_be aal 0 Int64.(mul 8L (of_int (String.length aad)));
            String.concat "" [ aad; iv; encrypted; Bytes.unsafe_to_string aal ]
          in
          let computed_auth_tag =
            let full = Digestif.SHA256.hmac_string ~key:hmac_key hmac_input in
            (* B.7 truncate to 128 bit *)
            String.sub (Digestif.SHA256.to_raw_string full) 0 16
          in
          if not (String.equal computed_auth_tag auth_tag) then
            Error (`Msg "invalid auth tag")
          else
            (* B.2 encryption in CBC mode *)
            Mirage_crypto.AES.CBC.decrypt ~key ~iv encrypted |> Pkcs7.unpad
      | Some `A256GCM ->
          let module GCM = Mirage_crypto.AES.GCM in
          let key = GCM.of_secret cek in
          let adata = aad in
          let encrypted = encrypted ^ auth_tag in
          Mirage_crypto.AES.GCM.authenticate_decrypt ~key ~nonce:iv ~adata
            encrypted
          |> fun message ->
          message
          |> Option.map (fun x -> Ok x)
          |> Option.value ~default:(Error (`Msg "invalid auth tag"))
      | _ -> Error (`Msg "unsupported encryption"))

let decrypt ~(jwk : Jwk.priv Jwk.t) jwe =
  String.split_on_char '.' jwe |> function
  | [ enc_header; enc_cek; enc_iv; ciphertext; auth_tag ] ->
      let header = Header.of_string enc_header in
      Result.bind header (fun header ->
          let cek = decrypt_cek header.Header.alg ~jwk enc_cek in
          Result.bind cek (fun cek ->
              let iv = U_Base64.url_decode enc_iv in
              Result.bind iv (fun iv ->
                  let auth_tag = U_Base64.url_decode auth_tag in
                  Result.bind auth_tag (fun auth_tag ->
                      let payload =
                        decrypt_ciphertext header.Header.enc ~cek ~iv ~auth_tag
                          ~aad:enc_header ciphertext
                      in
                      Result.bind payload (fun payload ->
                          Ok { header; cek; iv; payload; aad = None })))))
  | _ -> Error `Invalid_JWE
OCaml

Innovation. Community. Security.