mte/unikernel/duniverse/ocaml-x509/lib/certificate.ml
2025-11-11 02:07:51 +01:00

264 lines
8.7 KiB
OCaml

(*
* X509 certs
*)
type tBSCertificate = {
version : [ `V1 | `V2 | `V3 ] ;
serial : string ;
signature : Algorithm.t ;
issuer : Distinguished_name.t ;
validity : Ptime.t * Ptime.t ;
subject : Distinguished_name.t ;
pk_info : Public_key.t ;
issuer_id : string option ;
subject_id : string option ;
extensions : Extension.t
}
type certificate = {
tbs_cert : tBSCertificate ;
signature_algo : Algorithm.t ;
signature_val : string
}
(*
* There are two reasons to carry octets around:
* - we still need to hack on the octets to get bytes to hash
* ( this needs to go )
* - we need a cs to send to the peer
* It's a bit ugly to have two levels, and both are better solved by extending
* the asn parser and writer respectively, but until then there needs to be one
* place that hides the existence of this pair.
*)
type t = {
asn : certificate ;
raw : string
}
module Asn = struct
open Asn.S
open Asn_grammars
let version =
map (function 2 -> `V3 | 1 -> `V2 | 0 -> `V1 | _ -> parse_error "unknown version")
(function `V3 -> 2 | `V2 -> 1 | `V1 -> 0)
int
let time =
let f = function `C1 t -> t | `C2 t -> t
and g t =
let (y, _, _) = Ptime.to_date t in
if y < 2050 then `C1 t else `C2 t in
map f g (choice2 utc_time generalized_time_no_frac_s)
let validity =
sequence2
(required ~label:"not before" time)
(required ~label:"not after" time)
let unique_identifier = bit_string_octets
let tBSCertificate =
let f = fun (a, (b, (c, (d, (e, (f, (g, (h, (i, j))))))))) ->
let extn = match j with None -> Extension.empty | Some xs -> xs in
{ version = Option.value ~default:`V1 a ; serial = b ;
signature = c ; issuer = d ;
validity = e ; subject = f ;
pk_info = g ; issuer_id = h ;
subject_id = i ; extensions = extn }
and g = fun
{ version = a ; serial = b ;
signature = c ; issuer = d ;
validity = e ; subject = f ;
pk_info = g ; issuer_id = h ;
subject_id = i ; extensions = j } ->
let extn = if Extension.is_empty j then None else Some j in
((if a = `V1 then None else Some a),
(b, (c, (d, (e, (f, (g, (h, (i, extn)))))))))
in
map f g @@
sequence @@
(optional ~label:"version" @@ explicit 0 version) (* default v1 *)
@ (required ~label:"serialNumber" @@ serial)
@ (required ~label:"signature" @@ Algorithm.identifier)
@ (required ~label:"issuer" @@ Distinguished_name.Asn.name)
@ (required ~label:"validity" @@ validity)
@ (required ~label:"subject" @@ Distinguished_name.Asn.name)
@ (required ~label:"subjectPKInfo" @@ Public_key.Asn.pk_info_der)
(* if present, version is v2 or v3 *)
@ (optional ~label:"issuerUID" @@ implicit 1 unique_identifier)
(* if present, version is v2 or v3 *)
@ (optional ~label:"subjectUID" @@ implicit 2 unique_identifier)
(* v3 if present *)
-@ (optional ~label:"extensions" @@ explicit 3 Extension.Asn.extensions_der)
let (tbs_certificate_of_octets, tbs_certificate_to_octets) =
projections_of Asn.der tBSCertificate
let certificate =
let f (a, b, c) =
if a.signature <> b then
parse_error "signatureAlgorithm != tbsCertificate.signature"
else
{ tbs_cert = a; signature_algo = b; signature_val = c }
and g { tbs_cert = a; signature_algo = b; signature_val = c } = (a, b, c) in
map f g @@
sequence3
(required ~label:"tbsCertificate" tBSCertificate)
(required ~label:"signatureAlgorithm" Algorithm.identifier)
(required ~label:"signatureValue" bit_string_octets)
let (certificate_of_octets, certificate_to_octets) =
projections_of Asn.der certificate
let pkcs1_digest_info =
let open Algorithm in
let f (algo, cs) =
match to_hash algo with
| Some h -> (h, cs)
| None -> parse_error "pkcs1 digest info: unknown hash"
and g (h, cs) = (of_hash h, cs)
in
map f g @@
sequence2
(required ~label:"digestAlgorithm" Algorithm.identifier)
(required ~label:"digest" octet_string)
let (pkcs1_digest_info_of_octets, pkcs1_digest_info_to_octets) =
projections_of Asn.der pkcs1_digest_info
end
let decode_pkcs1_digest_info cs =
Asn_grammars.err_to_msg (Asn.pkcs1_digest_info_of_octets cs)
let encode_pkcs1_digest_info = Asn.pkcs1_digest_info_to_octets
let ( let* ) = Result.bind
let decode_der cs =
let* asn = Asn_grammars.err_to_msg (Asn.certificate_of_octets cs) in
Ok { asn ; raw = cs }
let encode_der { raw ; _ } = raw
let decode_pem_multiple cs =
let* data = Pem.parse cs in
let certs =
List.filter (fun (t, _) -> String.equal "CERTIFICATE" t) data
in
Pem.foldM (fun (_, cs) -> decode_der cs) certs
let fold_decode_pem_multiple fn acc cs =
List.fold_left
(fun acc data ->
let data = match data with
| Ok ("CERTIFICATE", cs) -> decode_der cs
| Ok (hdr, _) -> Error (`Msg ("ignore non certificate (" ^ hdr ^ ")"))
| Error e -> Error e
in
fn acc data)
acc
(Pem.parse_with_errors cs)
let decode_pem cs =
let* certs = decode_pem_multiple cs in
Pem.exactly_one ~what:"certificate" certs
let encode_pem v =
Pem.unparse ~tag:"CERTIFICATE" (encode_der v)
let encode_pem_multiple cs =
String.concat "" (List.map encode_pem cs)
let pp_version ppf v =
Fmt.string ppf (match v with `V1 -> "1" | `V2 -> "2" | `V3 -> "3")
let pp_hash ppf hash =
Fmt.string ppf (match hash with
| `MD5 -> "MD5" | `SHA1 -> "SHA1" | `SHA224 -> "SHA224"
| `SHA256 -> "SHA256" | `SHA384 -> "SHA384" | `SHA512 -> "SHA512")
let pp_sigalg ppf (asym, hash) =
Fmt.pf ppf "%a-%a" Key_type.pp_signature_scheme asym pp_hash hash
let pp' pp_custom_extensions ppf { asn ; _ } =
let tbs = asn.tbs_cert in
let sigalg = Algorithm.to_signature_algorithm tbs.signature in
Fmt.pf ppf "X.509 certificate@.version %a@.serial %a@.algorithm %a@.issuer %a@.valid from %a until %a@.subject %a@.extensions %a"
pp_version tbs.version Ohex.pp tbs.serial
Fmt.(option ~none:(any "NONE") pp_sigalg) sigalg
Distinguished_name.pp tbs.issuer
(Ptime.pp_human ~tz_offset_s:0 ()) (fst tbs.validity)
(Ptime.pp_human ~tz_offset_s:0 ()) (snd tbs.validity)
Distinguished_name.pp tbs.subject
(Extension.pp' pp_custom_extensions) tbs.extensions
let pp = pp' Extension.default_pp_custom_extension
let fingerprint hash cert =
let module Hash = (val (Digestif.module_of_hash' hash)) in
Hash.(to_raw_string (digest_string cert.raw))
let issuer { asn ; _ } = asn.tbs_cert.issuer
let subject { asn ; _ } = asn.tbs_cert.subject
let serial { asn ; _ } = asn.tbs_cert.serial
let validity { asn ; _ } = asn.tbs_cert.validity
let signature_algorithm { asn ; _ } =
Algorithm.to_signature_algorithm asn.signature_algo
let public_key { asn = cert ; _ } = cert.tbs_cert.pk_info
let supports_keytype c t =
match public_key c, t with
| (`RSA _), `RSA -> true
| _ -> false
let extensions { asn = cert ; _ } = cert.tbs_cert.extensions
(* RFC 6125, 6.4.4:
Therefore, if and only if the presented identifiers do not include a
DNS-ID, SRV-ID, URI-ID, or any application-specific identifier types
supported by the client, then the client MAY as a last resort check
for a string whose form matches that of a fully qualified DNS domain
name in a Common Name field of the subject field (i.e., a CN-ID). If
the client chooses to compare a reference identifier of type CN-ID
against that string, it MUST follow the comparison rules for the DNS
domain name portion of an identifier of type DNS-ID, SRV-ID, or
URI-ID, as described under Section 6.4.1, Section 6.4.2, and
Section 6.4.3. *)
let hostnames { asn = cert ; _ } =
let subj =
match Distinguished_name.common_name cert.tbs_cert.subject with
| None -> Host.Set.empty
| Some x ->
match Host.host x with
| Some (wild, d) -> Host.Set.singleton (wild, d)
| None -> Host.Set.empty
in
match Extension.hostnames cert.tbs_cert.extensions with
| Some names -> names
| None -> subj
let supports_hostname cert name =
let names = hostnames cert in
let wc_name_opt =
match Domain_name.drop_label name with
| Error _ -> None
| Ok name -> match Domain_name.host name with
| Ok hostname -> Some hostname
| Error _ -> None
in
Host.Set.mem (`Strict, name) names
|| (match wc_name_opt with
| None -> false
| Some wc_name -> Host.Set.mem (`Wildcard, wc_name) names)
let ips { asn = cert ; _ } =
match Extension.ips cert.tbs_cert.extensions with
| None -> Ipaddr.Set.empty
| Some ips -> ips
let supports_ip cert ip = Ipaddr.Set.mem ip (ips cert)