mte/unikernel/duniverse/ocaml-x509/lib/p12.ml

469 lines
16 KiB
OCaml
Raw Normal View History

2025-11-11 02:07:51 +01:00
(* partial PKCS12 implementation, as defined in RFC 7292
- no public/private key mode, only password privacy and integrity
- algorithmidentifier those I need for openssl interop (looking at the p12 I have on my disk)
- require version being 3
some definitions from PKCS7 (RFC 2315) are implemented as well, as needed
*)
type content_info = Asn.oid * string
type digest_info = Algorithm.t * string
type mac_data = digest_info * string * int
type t = string * mac_data
module Asn = struct
open Asn_grammars
open Asn.S
open Registry
let encrypted_content_info =
let f (oid, algo, content) =
if Asn.OID.equal PKCS7.data oid then
(algo, content)
else
parse_error "expected OID PKCS7 data"
and g (algo, content) =
(PKCS7.data, algo, content)
in
Asn.S.map f g @@
sequence3
(required ~label:"content type" oid) (* here we assume data!? *)
(required ~label:"content encryption algorithm" Algorithm.identifier)
(optional ~label:"encrypted content" (implicit 0 octet_string))
let encrypted_data =
let f (v, eci) =
if v = 0 then eci else parse_error "unknown encrypted data version"
and g eci = 0, eci
in
map f g @@
sequence2
(required ~label:"version" int)
(required ~label:"encrypted content info" encrypted_content_info)
let content_info =
let f (oid, data) =
match data with
| None -> parse_error "found no value for content info"
| Some `C1 data when Asn.OID.equal PKCS7.data oid -> `Data data
| Some `C2 eci when Asn.OID.equal PKCS7.encrypted_data oid -> `Encrypted eci
| _ -> parse_error "couldn't match PKCS7 oid with choice"
and g = function
| `Data data -> PKCS7.data, Some (`C1 data)
| `Encrypted eci -> PKCS7.encrypted_data, Some (`C2 eci)
in
map f g @@
sequence2
(required ~label:"content type" oid)
(optional ~label:"content" (explicit 0
(choice2 octet_string encrypted_data)))
let digest_info =
sequence2
(required ~label:"digest algorithm" Algorithm.identifier)
(required ~label:"digest" octet_string)
let mac_data =
sequence3
(required ~label:"mac" digest_info)
(required ~label:"mac salt" octet_string)
(required ~label:"iterations" int)
let pfx =
let f (version, content_info, mac_data) =
if version = 3 then
match content_info, mac_data with
| `Data data, Some md -> data, md
| _, None -> parse_error "missing mac_data"
| _, _ -> parse_error "unsupported content_info"
else
parse_error "unsupported pfx version"
and g (content, mac_data) =
(3, `Data content, Some mac_data)
in
map f g @@
sequence3
(required ~label:"version" int)
(required ~label:"auth safe" content_info)
(* contentType is signedData in public-key integrity mode and data in
password integrity mode *)
(optional ~label:"mac data" mac_data) (* not present if public keys used *)
let pfx_of_cs, pfx_to_cs = projections_of Asn.der pfx
(* payload is a sequence of content_info *)
let authenticated_safe = sequence_of content_info
let auth_safe_of_cs, auth_safe_to_cs =
projections_of Asn.der authenticated_safe
let pkcs12_attribute =
sequence2
(required ~label:"attribute id" oid)
(required ~label:"attribute value" (set_of octet_string))
(* here:
key_bag = PKCS8 private key
pkcs8_shrouded_key_bag = encrypted private key info ==
sequence2 Algorithm octet_string
cert_bag =
sequence2
cert_id (PKCS9 cert_types <| 1 (X509) or 2 (SDSI))
expl 0 cert_value (DER-encoded certificate)
crl_bag = sequence2 crl_id (PKCS9 crl_types <| 1) (expl 0 crl (DER-encoded))
^^^---^^^ those we plan to support
secret_bag = sequence2 secret_type (expl 0 secret_value)
safe_contents_bag = (any of the above) safe_contents (recursive!)
*)
(* since asn1 does not yet support ANY defined BY, we develop a rather
complex grammar covering all supported bags *)
let safe_bag =
let cert_oid, crl_oid =
Asn.OID.(PKCS9.cert_types <| 1, PKCS9.crl_types <| 1)
in
let f (oid, (a, algo, data), attrs) =
match a, algo, data with
| `C1 v, Some a, `C1 data when Asn.OID.equal oid PKCS12.key_bag ->
let key = Private_key.Asn.reparse_private (v, a, data) in
`Private_key key, attrs
| `C2 id, None, `C2 data ->
if Asn.OID.equal oid PKCS12.cert_bag && Asn.OID.equal id cert_oid then
match Certificate.decode_der data with
| Error (`Msg e) -> error (`Parse e)
| Ok cert -> `Certificate cert, attrs
else if Asn.OID.equal oid PKCS12.crl_bag && Asn.OID.equal id crl_oid then
match Crl.decode_der data with
| Error (`Msg e) -> error (`Parse e)
| Ok crl -> `Crl crl, attrs
else
parse_error "crl bag with non-standard crl"
| `C3 algo, None, `C1 data when Asn.OID.equal oid PKCS12.pkcs8_shrouded_key_bag ->
`Encrypted_private_key (algo, data), attrs
| _ -> parse_error "safe bag OID not supported"
and g (v, attrs) =
let oid, d = match v with
| `Encrypted_private_key (algo, data) ->
PKCS12.pkcs8_shrouded_key_bag, (`C3 algo, None, `C1 data)
| `Private_key pk ->
let v, algo, data = Private_key.Asn.unparse_private pk in
PKCS12.key_bag, (`C1 v, Some algo, `C1 data)
| `Certificate cert -> PKCS12.cert_bag, (`C2 cert_oid, None, `C2 (Certificate.encode_der cert))
| `Crl crl -> PKCS12.crl_bag, (`C2 crl_oid, None, `C2 (Crl.encode_der crl))
in
(oid, d, attrs)
in
map f g @@
sequence3
(required ~label:"bag id" oid)
(required ~label:"bag value"
(explicit 0
(sequence3
(required ~label:"fst" (choice3 int oid Algorithm.identifier))
(optional ~label:"algorithm" Algorithm.identifier)
(required ~label:"data" (choice2 octet_string (explicit 0 octet_string))))))
(* (explicit 0 (* encrypted private key *)
(sequence2
(required ~label:"encryption algorithm" Algorithm.identifier)
(required ~label:"encrypted data" octet_string))) *)
(* (explicit 0 (* private key ] *)
(sequence3
(required ~label:"version" int)
(required ~label:"privateKeyAlgorithm" Algorithm.identifier)
(required ~label:"privateKey" octet_string))) *)
(* (explicit 0 (* cert / crl *)
(sequence2
(required ~label:"oid" oid)
(required ~label:"data" (explicit 0 octet_string)))) *)
(optional ~label:"bag attributes" (set_of pkcs12_attribute))
let safe_contents = sequence_of safe_bag
let safe_contents_of_cs, safe_contents_to_cs =
projections_of Asn.der safe_contents
end
let prepare_pw str =
let l = String.length str in
let cs = Bytes.make ((succ l) * 2) '\000' in
for i = 0 to pred l do
Bytes.set cs (succ (i * 2)) (String.get str i)
done;
Bytes.unsafe_to_string cs
let id len purpose =
let id = match purpose with
| `Encryption -> 1
| `Iv -> 2
| `Hmac -> 3
in
String.make len (Char.unsafe_chr id)
let v = function
| `MD5 | `SHA1 | `SHA224 | `SHA256 -> 512 / 8
| `SHA384 | `SHA512 -> 1024 / 8
let fill ~data ~out =
let len = Bytes.length out
and l = String.length data
in
let rec c off =
if off < len then begin
Bytes.blit_string data 0 out off (min (len - off) l);
c (off + l)
end
in
c 0
let fill_or_empty size data =
let l = String.length data in
if l = 0 then data
else
let len = size * ((l + size - 1) / size) in
let buf = Bytes.make len '\000' in
fill ~data ~out:buf;
Bytes.unsafe_to_string buf
let pbes algorithm purpose password salt iterations n =
let module Hash = (val (Digestif.module_of_hash' (algorithm :> Digestif.hash'))) in
let pw = prepare_pw password
and v = v algorithm
and u = Hash.digest_size
in
let diversifier = id v purpose in
let salt = fill_or_empty v salt in
let pass = fill_or_empty v pw in
let out = Bytes.make n '\000' in
let rec one off i =
let ai = ref Hash.(to_raw_string (digest_string (diversifier ^ i))) in
for _j = 1 to pred iterations do
ai := Hash.(to_raw_string (digest_string !ai));
done;
Bytes.blit_string !ai 0 out off (min (n - off) u);
if u >= n - off then () else
(* 6B *)
let b = Bytes.make v '\000' in
fill ~data:!ai ~out:b;
(* 6C *)
let i' = Bytes.create (String.length i) in
for j = 0 to pred (String.length i / v) do
let c = ref 1 in
for k = pred v downto 0 do
let idx = j * v + k in
c := (!c + String.get_uint8 i idx + Bytes.get_uint8 b k) land 0xFFFF;
Bytes.set_uint8 i' idx (!c land 0xFF);
c := !c lsr 8;
done;
done;
one (off + u) (Bytes.to_string i')
in
let i = salt ^ pass in
one 0 i;
Bytes.unsafe_to_string out
let split str off =
String.sub str 0 off,
String.sub str off (String.length str - off)
(* TODO PKCS5/7 padding is "k - (l mod k)" i.e. always > 0!
(and rc4 being a stream cipher has no padding!) *)
let unpad x =
(* TODO can there be bad padding in this scheme? *)
let l = String.length x in
if l > 0 then
let amount = String.get_uint8 x (pred l) in
let split_point = if l > amount then l - amount else l in
let data, pad = split x split_point in
let good = ref true in
for i = 0 to pred amount do
if String.get_uint8 pad i <> amount then good := false
done;
if !good then data else x
else
x
let pad bs x =
let l = String.length x in
let to_pad = bs - (l mod bs) in
let amount = String.make to_pad (Char.unsafe_chr to_pad) in
x ^ amount
let ( let* ) = Result.bind
(* there are 3 possibilities to encrypt / decrypt things:
- PKCS12 KDF (see above), with RC2/RC4/DES
- PKCS5 v1 (PBES, PBKDF1) -- not (yet?) supported
- PKCS5 v2 (PBES2, PBKDF2)
*)
let pkcs12_decrypt algo password data =
let open Algorithm in
let hash = `SHA1 in
let* salt, count, key_len, iv_len =
match algo with
| SHA_RC4_128 (s, i) -> Ok (s, i, 16, 0)
| SHA_RC4_40 (s, i) -> Ok (s, i, 5, 0)
| SHA_3DES_CBC (s, i) -> Ok (s, i, 24, 8)
| SHA_2DES_CBC (s, i) -> Ok (s, i, 16, 8) (* TODO 2des -> 3des keys (if relevant)*)
| SHA_RC2_128_CBC (s, i) -> Ok (s, i, 16, 8)
| SHA_RC2_40_CBC (s, i) -> Ok (s, i, 5, 8)
| _ -> Error (`Msg "unsupported algorithm")
in
let key = pbes hash `Encryption password salt count key_len
and iv = pbes hash `Iv password salt count iv_len
in
let open Mirage_crypto in
let* data =
match algo with
| SHA_RC2_40_CBC _ | SHA_RC2_128_CBC _ ->
Ok (Rc2.decrypt_cbc ~effective:(key_len * 8) ~key ~iv data)
| SHA_RC4_40 _ | SHA_RC4_128 _ ->
let key = ARC4.of_secret key in
let { ARC4.message ; _ } = ARC4.decrypt ~key data in
Ok message
| SHA_3DES_CBC _ ->
let key = DES.CBC.of_secret key in
Ok (DES.CBC.decrypt ~key ~iv data)
| _ -> Error (`Msg "encryption algorithm not supported")
in
Ok (unpad data)
let pkcs5_2_decrypt kdf enc password data =
let* dk_len, iv =
match enc with
| Algorithm.AES128_CBC iv -> Ok (16l, iv)
| Algorithm.AES192_CBC iv -> Ok (24l, iv)
| Algorithm.AES256_CBC iv -> Ok (32l, iv)
| _ -> Error (`Msg "unsupported encryption algorithm")
in
let* salt, count, prf =
match kdf with
| Algorithm.PBKDF2 (salt, iterations, _ (* todo handle keylength *), prf) ->
let* prf =
match Algorithm.to_hmac prf with
| Some prf -> Ok prf
| None -> Error (`Msg "unsupported PRF")
in
Ok (salt, iterations, prf)
| _ -> Error (`Msg "expected kdf being pbkdf2")
in
let key = Pbkdf.pbkdf2 ~prf ~password ~salt ~count ~dk_len in
let key = Mirage_crypto.AES.CBC.of_secret key in
let msg = Mirage_crypto.AES.CBC.decrypt ~key ~iv data in
Ok (unpad msg)
let pkcs5_2_encrypt (mac : [ `SHA1 | `SHA224 | `SHA256 | `SHA384 | `SHA512 ]) count algo password data =
let module Hash = (val (Digestif.module_of_hash' (mac :> Digestif.hash'))) in
let bs = Mirage_crypto.AES.CBC.block_size in
let iv = Mirage_crypto_rng.generate bs in
let enc, dk_len =
match algo with
| `AES128_CBC -> Algorithm.AES128_CBC iv, 16l
| `AES192_CBC -> Algorithm.AES192_CBC iv, 24l
| `AES256_CBC -> Algorithm.AES256_CBC iv, 32l
in
let salt = Mirage_crypto_rng.generate Hash.digest_size in
let key = Pbkdf.pbkdf2 ~prf:(mac :> Digestif.hash') ~password ~salt ~count ~dk_len in
let key = Mirage_crypto.AES.CBC.of_secret key in
let padded_data = pad bs data in
let enc_data =
Mirage_crypto.AES.CBC.encrypt ~key ~iv padded_data
in
let kdf = Algorithm.PBKDF2 (salt, count, None, Algorithm.of_hmac mac) in
Algorithm.PBES2 (kdf, enc), enc_data
let decrypt algo password data =
let open Algorithm in
match algo with
| SHA_RC4_128 _ | SHA_RC4_40 _
| SHA_3DES_CBC _ | SHA_2DES_CBC _
| SHA_RC2_128_CBC _ | SHA_RC2_40_CBC _ -> pkcs12_decrypt algo password data
| PBES2 (kdf, enc) -> pkcs5_2_decrypt kdf enc password data
| _ -> Error (`Msg "unsupported encryption algorithm")
let password_decrypt password (algo, data) =
match data with
| None -> Error (`Msg "no data to decrypt")
| Some data -> decrypt algo password data
let verify password (data, ((algorithm, digest), salt, iterations)) =
let* hash =
Option.to_result
~none:(`Msg "unsupported hash algorithm")
(Algorithm.to_hash algorithm)
in
let module Hash = (val (Digestif.module_of_hash' (hash :> Digestif.hash'))) in
let key =
pbes hash `Hmac password salt iterations Hash.digest_size
in
let computed = Hash.(to_raw_string (hmac_string ~key data)) in
if String.equal computed digest then begin
let* content = Asn_grammars.err_to_msg (Asn.auth_safe_of_cs data) in
let* safe_contents =
List.fold_left (fun acc c ->
let* acc = acc in
match c with
| `Data data -> Ok (data :: acc)
| `Encrypted data ->
let* data = password_decrypt password data in
Ok (data :: acc))
(Ok []) content
in
List.fold_left (fun acc cs ->
let* acc = acc in
let* bags = Asn_grammars.err_to_msg (Asn.safe_contents_of_cs cs) in
List.fold_left (fun acc bag ->
let* acc = acc in
match bag with
| `Certificate c, _ -> Ok (`Certificate c :: acc)
| `Crl c, _ -> Ok (`Crl c :: acc)
| `Private_key p, _ -> Ok (`Private_key p :: acc)
| `Encrypted_private_key (algo, enc_data), _ ->
let* data = decrypt algo password enc_data in
let* p =
Asn_grammars.err_to_msg (Private_key.Asn.private_of_octets data)
in
Ok (`Decrypted_private_key p :: acc))
(Ok acc) bags)
(Ok []) safe_contents
end else
Error (`Msg "invalid signature")
let create ?(mac = `SHA256) ?(algorithm = `AES256_CBC) ?(iterations = 2048) password certificates private_key =
let key_fp pub = Public_key.fingerprint pub in
let priv_fp = key_fp (Private_key.public private_key) in
let attributes = [ Registry.PKCS9.local_key_id, [ priv_fp ]] in
let maybe_attr c =
if String.equal priv_fp (key_fp (Certificate.public_key c)) then
Some attributes
else
None
in
let cert_sc =
Asn.safe_contents_to_cs (List.map (fun c -> `Certificate c, maybe_attr c) certificates)
and priv_sc =
let data = Private_key.Asn.private_to_octets private_key in
let algo, data = pkcs5_2_encrypt mac iterations algorithm password data in
Asn.safe_contents_to_cs [ `Encrypted_private_key (algo, data), Some attributes ]
in
let cert_sc_enc =
let algo, data = pkcs5_2_encrypt mac iterations algorithm password cert_sc in
algo, Some data
in
let auth_data =
Asn.auth_safe_to_cs [ `Encrypted cert_sc_enc ; `Data priv_sc ]
in
let module Hash = (val (Digestif.module_of_hash' (mac :> Digestif.hash'))) in
let mac_size = Hash.digest_size in
let salt = Mirage_crypto_rng.generate mac_size in
let key = pbes mac `Hmac password salt iterations mac_size in
let digest = Hash.(to_raw_string (hmac_string ~key auth_data)) in
auth_data, ((Algorithm.of_hash mac, digest), salt, iterations)
let decode_der cs = Asn_grammars.err_to_msg (Asn.pfx_of_cs cs)
let encode_der = Asn.pfx_to_cs