
type key_usage = [
  | `Digital_signature
  | `Content_commitment
  | `Key_encipherment
  | `Data_encipherment
  | `Key_agreement
  | `Key_cert_sign
  | `CRL_sign
  | `Encipher_only
  | `Decipher_only
]

let pp_key_usage ppf ku =
  Fmt.string ppf
    (match ku with
     | `Digital_signature ->  "digital signature"
     | `Content_commitment -> "content commitment"
     | `Key_encipherment -> "key encipherment"
     | `Data_encipherment -> "data encipherment"
     | `Key_agreement -> "key agreement"
     | `Key_cert_sign -> "key cert sign"
     | `CRL_sign -> "CRL sign"
     | `Encipher_only -> "encipher only"
     | `Decipher_only -> "decipher only")

type extended_key_usage = [
  | `Any
  | `Server_auth
  | `Client_auth
  | `Code_signing
  | `Email_protection
  | `Ipsec_end
  | `Ipsec_tunnel
  | `Ipsec_user
  | `Time_stamping
  | `Ocsp_signing
  | `Other of Asn.oid
]

let pp_extended_key_usage ppf = function
  | `Any -> Fmt.string ppf "any"
  | `Server_auth -> Fmt.string ppf "server authentication"
  | `Client_auth -> Fmt.string ppf "client authentication"
  | `Code_signing -> Fmt.string ppf "code signing"
  | `Email_protection -> Fmt.string ppf "email protection"
  | `Ipsec_end -> Fmt.string ppf "ipsec end"
  | `Ipsec_tunnel -> Fmt.string ppf "ipsec tunnel"
  | `Ipsec_user -> Fmt.string ppf "ipsec user"
  | `Time_stamping -> Fmt.string ppf "time stamping"
  | `Ocsp_signing -> Fmt.string ppf "ocsp signing"
  | `Other oid -> Asn.OID.pp ppf oid

type authority_key_id = string option * General_name.t * string option

let pp_authority_key_id ppf (id, issuer, serial) =
  Fmt.pf ppf "identifier %a@ issuer %a@ serial %a@ "
    Fmt.(option ~none:(any "none") Ohex.pp) id
    General_name.pp issuer
    Fmt.(option ~none:(any "none") Ohex.pp) serial

type priv_key_usage_period = [
  | `Interval   of Ptime.t * Ptime.t
  | `Not_after  of Ptime.t
  | `Not_before of Ptime.t
]

let pp_priv_key_usage_period ppf =
  let pp_ptime = Ptime.pp_human ~tz_offset_s:0 () in
  function
  | `Interval (start, stop) ->
    Fmt.pf ppf "from %a till %a" pp_ptime start pp_ptime stop
  | `Not_after after -> Fmt.pf ppf "not after %a" pp_ptime after
  | `Not_before before -> Fmt.pf ppf "not before %a" pp_ptime before

type name_constraint = (General_name.b * int * int option) list

let pp_name_constraints ppf (permitted, excluded) =
  let pp_one ppf (General_name.B (k, base), min, max) =
    Fmt.pf ppf "base %a min %u max %a"
      (General_name.pp_k k) base min Fmt.(option ~none:(any "none") int) max
  in
  Fmt.pf ppf "permitted %a@ excluded %a"
    Fmt.(list ~sep:(any ", ") pp_one) permitted
    Fmt.(list ~sep:(any ", ") pp_one) excluded

type policy = [ `Any | `Something of Asn.oid ]

let pp_policy ppf = function
  | `Any -> Fmt.string ppf "any"
  | `Something oid -> Fmt.pf ppf "some oid %a" Asn.OID.pp oid

type reason = [
  | `Unspecified
  | `Key_compromise
  | `CA_compromise
  | `Affiliation_changed
  | `Superseded
  | `Cessation_of_operation
  | `Certificate_hold
  | `Remove_from_CRL
  | `Privilege_withdrawn
  | `AA_compromise
]

let reason_to_int = function
  | `Unspecified -> 0
  | `Key_compromise -> 1
  | `CA_compromise -> 2
  | `Affiliation_changed -> 3
  | `Superseded -> 4
  | `Cessation_of_operation -> 5
  | `Certificate_hold -> 6
  (* 7 is not used *)
  | `Remove_from_CRL -> 8
  | `Privilege_withdrawn -> 9
  | `AA_compromise -> 10

let reason_of_int = function
  |  0 -> `Unspecified
  |  1 -> `Key_compromise
  |  2 -> `CA_compromise
  |  3 -> `Affiliation_changed
  |  4 -> `Superseded
  |  5 -> `Cessation_of_operation
  |  6 -> `Certificate_hold
  (* 7 is not used *)
  |  8 -> `Remove_from_CRL
  |  9 -> `Privilege_withdrawn
  |  10 -> `AA_compromise
  | x -> Asn.S.parse_error "Unknown reason %d" x

let pp_reason ppf r =
  Fmt.string ppf (match r with
      | `Unspecified -> "unspecified"
      | `Key_compromise -> "key compromise"
      | `CA_compromise -> "CA compromise"
      | `Affiliation_changed -> "affiliation changed"
      | `Superseded -> "superseded"
      | `Cessation_of_operation -> "cessation of operation"
      | `Certificate_hold -> "certificate hold"
      | `Remove_from_CRL -> "remove from CRL"
      | `Privilege_withdrawn -> "privilege withdrawn"
      | `AA_compromise -> "AA compromise")

type distribution_point_name =
  [ `Full of General_name.t
  | `Relative of Distinguished_name.t ]

let pp_distribution_point_name ppf = function
  | `Full name -> Fmt.pf ppf "full %a" General_name.pp name
  | `Relative name -> Fmt.pf ppf "relative %a" Distinguished_name.pp name

type distribution_point =
  distribution_point_name option *
  reason list option *
  General_name.t option

let pp_distribution_point ppf (name, reasons, issuer) =
  Fmt.pf ppf "name %a reason %a issuer %a"
    Fmt.(option ~none:(any "none") pp_distribution_point_name) name
    Fmt.(option ~none:(any "none") (list ~sep:(any ", ") pp_reason)) reasons
    Fmt.(option ~none:(any "none") General_name.pp) issuer

let pp_issuing_distribution_point ppf (name, onlyuser, onlyca, onlysome, indirectcrl, onlyattributes) =
  Fmt.pf ppf "name %a only user certs %B only CA certs %B only reasons %a indirectcrl %B only attribute certs %B"
    Fmt.(option ~none:(any "none") pp_distribution_point_name) name
    onlyuser onlyca
    Fmt.(option ~none:(any "no") (list ~sep:(any ", ") pp_reason)) onlysome
    indirectcrl onlyattributes

type 'a extension = bool * 'a

type _ k =
  | Unsupported : Asn.oid -> string extension k
  | Subject_alt_name : General_name.t extension k
  | Authority_key_id : authority_key_id extension k
  | Subject_key_id : string extension k
  | Issuer_alt_name : General_name.t extension k
  | Key_usage : key_usage list extension k
  | Ext_key_usage : extended_key_usage list extension k
  | Basic_constraints : (bool * int option) extension k
  | CRL_number : int extension k
  | Delta_CRL_indicator : int extension k
  | Priv_key_period : priv_key_usage_period extension k
  | Name_constraints : (name_constraint * name_constraint) extension k
  | CRL_distribution_points : distribution_point list extension k
  | Issuing_distribution_point : (distribution_point_name option * bool * bool * reason list option * bool * bool) extension k
  | Freshest_CRL : distribution_point list extension k
  | Reason : reason extension k
  | Invalidity_date : Ptime.t extension k
  | Certificate_issuer : General_name.t extension k
  | Policies : policy list extension k

let pp_one' : type a. (Format.formatter -> Asn.oid * string -> unit) -> a k -> Format.formatter -> a -> unit = fun custom k ppf v ->
  let c_to_str b = if b then "critical " else "" in
  match k, v with
  | Subject_alt_name, (crit, alt) ->
    Fmt.pf ppf "%ssubjectAlternativeName %a" (c_to_str crit)
      General_name.pp alt
  | Authority_key_id, (crit, kid) ->
    Fmt.pf ppf "%sauthorityKeyIdentifier %a" (c_to_str crit)
      pp_authority_key_id kid
  | Subject_key_id, (crit, kid) ->
    Fmt.pf ppf "%ssubjectKeyIdentifier %a" (c_to_str crit)
      Ohex.pp kid
  | Issuer_alt_name, (crit, alt) ->
    Fmt.pf ppf "%sissuerAlternativeNames %a" (c_to_str crit)
      General_name.pp alt
  | Key_usage, (crit, ku) ->
    Fmt.pf ppf "%skeyUsage %a" (c_to_str crit)
      Fmt.(list ~sep:(any ", ") pp_key_usage) ku
  | Ext_key_usage, (crit, eku) ->
    Fmt.pf ppf "%sextendedKeyUsage %a" (c_to_str crit)
      Fmt.(list ~sep:(any ", ") pp_extended_key_usage) eku
  | Basic_constraints, (crit, (ca, depth)) ->
    Fmt.pf ppf "%sbasicConstraints CA %B depth %a" (c_to_str crit) ca
      Fmt.(option ~none:(any "none") int) depth
  | CRL_number, (crit, i) ->
    Fmt.pf ppf "%scRLNumber %u" (c_to_str crit) i
  | Delta_CRL_indicator, (crit, indicator) ->
    Fmt.pf ppf "%sdeltaCRLIndicator %u" (c_to_str crit) indicator
  | Priv_key_period, (crit, period) ->
    Fmt.pf ppf "%sprivateKeyUsagePeriod %a" (c_to_str crit)
      pp_priv_key_usage_period period
  | Name_constraints, (crit, ncs) ->
    Fmt.pf ppf "%snameConstraints %a" (c_to_str crit) pp_name_constraints ncs
  | CRL_distribution_points, (crit, points) ->
    Fmt.pf ppf "%scRLDistributionPoints %a" (c_to_str crit)
      Fmt.(list ~sep:(any "; ") pp_distribution_point) points
  | Issuing_distribution_point, (crit, point) ->
    Fmt.pf ppf "%sissuingDistributionPoint %a" (c_to_str crit)
      pp_issuing_distribution_point point
  | Freshest_CRL, (crit, points) ->
    Fmt.pf ppf "%sfreshestCRL %a" (c_to_str crit)
      Fmt.(list ~sep:(any "; ") pp_distribution_point) points
  | Reason, (crit, reason) ->
    Fmt.pf ppf "%sreason %a" (c_to_str crit) pp_reason reason
  | Invalidity_date, (crit, date) ->
    Fmt.pf ppf "%sinvalidityDate %a" (c_to_str crit)
      (Ptime.pp_human ~tz_offset_s:0 ()) date
  | Certificate_issuer, (crit, name) ->
    Fmt.pf ppf "%scertificateIssuer %a" (c_to_str crit) General_name.pp name
  | Policies, (crit, pols) ->
    Fmt.pf ppf "%spolicies %a" (c_to_str crit)
      Fmt.(list ~sep:(any "; ") pp_policy) pols
  | Unsupported oid, (crit, str) ->
    Fmt.pf ppf "%s%a" (c_to_str crit) custom (oid, str)

let default_pp_custom_extension ppf (oid, str) =
  Fmt.pf ppf "unsupported %a: %a" Asn.OID.pp oid Ohex.pp str

let pp_one k fmt =
  pp_one' default_pp_custom_extension k fmt

module ID = Registry.Cert_extn

let to_oid : type a. a k -> Asn.oid = function
  | Unsupported oid -> oid
  | Subject_alt_name -> ID.subject_alternative_name
  | Authority_key_id -> ID.authority_key_identifier
  | Subject_key_id -> ID.subject_key_identifier
  | Issuer_alt_name -> ID.issuer_alternative_name
  | Key_usage -> ID.key_usage
  | Ext_key_usage -> ID.extended_key_usage
  | Basic_constraints -> ID.basic_constraints
  | CRL_number -> ID.crl_number
  | Delta_CRL_indicator -> ID.delta_crl_indicator
  | Priv_key_period -> ID.private_key_usage_period
  | Name_constraints -> ID.name_constraints
  | CRL_distribution_points -> ID.crl_distribution_points
  | Issuing_distribution_point -> ID.issuing_distribution_point
  | Freshest_CRL -> ID.freshest_crl
  | Reason -> ID.reason_code
  | Invalidity_date -> ID.invalidity_date
  | Certificate_issuer -> ID.certificate_issuer
  | Policies -> ID.certificate_policies_2

let critical : type a. a k -> a -> bool = fun k v ->
  match k, v with
  | Unsupported _, (b, _) -> b
  | Subject_alt_name, (b, _) -> b
  | Authority_key_id, (b, _) -> b
  | Subject_key_id, (b, _) -> b
  | Issuer_alt_name, (b, _) -> b
  | Key_usage, (b, _) -> b
  | Ext_key_usage, (b, _) -> b
  | Basic_constraints, (b, _) -> b
  | CRL_number, (b, _) -> b
  | Delta_CRL_indicator, (b, _) -> b
  | Priv_key_period, (b, _) -> b
  | Name_constraints, (b, _) -> b
  | CRL_distribution_points, (b, _) -> b
  | Issuing_distribution_point, (b, _) -> b
  | Freshest_CRL, (b, _) -> b
  | Reason, (b, _) -> b
  | Invalidity_date, (b, _) -> b
  | Certificate_issuer, (b, _) -> b
  | Policies, (b, _) -> b

module K = struct
  type 'a t = 'a k

  let compare : type a b. a t -> b t -> (a, b) Gmap.Order.t = fun t t' ->
    let open Gmap.Order in
    match t, t' with
    | Subject_alt_name, Subject_alt_name -> Eq
    | Authority_key_id, Authority_key_id -> Eq
    | Subject_key_id, Subject_key_id -> Eq
    | Issuer_alt_name, Issuer_alt_name -> Eq
    | Key_usage, Key_usage -> Eq
    | Ext_key_usage, Ext_key_usage -> Eq
    | Basic_constraints, Basic_constraints -> Eq
    | CRL_number, CRL_number -> Eq
    | Delta_CRL_indicator, Delta_CRL_indicator -> Eq
    | Priv_key_period, Priv_key_period -> Eq
    | Name_constraints, Name_constraints -> Eq
    | CRL_distribution_points, CRL_distribution_points -> Eq
    | Issuing_distribution_point, Issuing_distribution_point -> Eq
    | Freshest_CRL, Freshest_CRL -> Eq
    | Reason, Reason -> Eq
    | Invalidity_date, Invalidity_date -> Eq
    | Certificate_issuer, Certificate_issuer -> Eq
    | Policies, Policies -> Eq
    | Unsupported oid, Unsupported oid' when Asn.OID.equal oid oid' -> Eq
    | a, b ->
      let r = Asn.OID.compare (to_oid a) (to_oid b) in
      if r = 0 then assert false else if r < 0 then Lt else Gt
end

include Gmap.Make(K)

let pp' custom ppf m =
  iter (fun (B (k, v)) -> pp_one' custom k ppf v ; Fmt.sp ppf ()) m

let pp = pp' default_pp_custom_extension

let hostnames exts =
  match find Subject_alt_name exts with
  | None -> None
  | Some (_, names) ->
    match General_name.find DNS names with
    | None -> None
    | Some xs ->
      let names =
        List.fold_left (fun acc s ->
            match Host.host s with
            | Some (typ, hostname) -> Host.Set.add (typ, hostname) acc
            | None -> acc)
          Host.Set.empty xs
      in
      if Host.Set.is_empty names then None else Some names

let ips exts =
  match find Subject_alt_name exts with
  | None -> None
  | Some (_, names) ->
    match General_name.find IP names with
    | None -> None
    | Some xs ->
      let ips =
        List.fold_left (fun acc ip ->
          match
            match String.length ip with
            | 4 -> Result.map (fun ip -> Ipaddr.V4 ip) (Ipaddr.V4.of_octets ip)
            | 16 -> Result.map (fun ip -> Ipaddr.V6 ip) (Ipaddr.V6.of_octets ip)
            | _ -> Error (`Msg "unknown IP address kind")
          with
          | Ok ip -> Ipaddr.Set.add ip acc
          | Error _ -> acc)
        Ipaddr.Set.empty xs
      in
      if Ipaddr.Set.is_empty ips then None else Some ips

module Asn = struct
  open Asn.S
  open Asn_grammars

  let display_text =
    map (function `C1 s -> s | `C2 s -> s | `C3 s -> s | `C4 s -> s)
      (fun s -> `C4 s)
    @@
    choice4 ia5_string visible_string bmp_string utf8_string

  module ID = Registry.Cert_extn

  let key_usage : key_usage list Asn.t = bit_string_flags [
      0, `Digital_signature
    ; 1, `Content_commitment
    ; 2, `Key_encipherment
    ; 3, `Data_encipherment
    ; 4, `Key_agreement
    ; 5, `Key_cert_sign
    ; 6, `CRL_sign
    ; 7, `Encipher_only
    ; 8, `Decipher_only
    ]

  let ext_key_usage =
    let open ID.Extended_usage in
    let f = case_of_oid [
      (any              , `Any             ) ;
      (server_auth      , `Server_auth     ) ;
      (client_auth      , `Client_auth     ) ;
      (code_signing     , `Code_signing    ) ;
      (email_protection , `Email_protection) ;
      (ipsec_end_system , `Ipsec_end       ) ;
      (ipsec_tunnel     , `Ipsec_tunnel    ) ;
      (ipsec_user       , `Ipsec_user      ) ;
      (time_stamping    , `Time_stamping   ) ;
      (ocsp_signing     , `Ocsp_signing    ) ]
      ~default:(fun oid -> `Other oid)
    and g = function
      | `Any              -> any
      | `Server_auth      -> server_auth
      | `Client_auth      -> client_auth
      | `Code_signing     -> code_signing
      | `Email_protection -> email_protection
      | `Ipsec_end        -> ipsec_end_system
      | `Ipsec_tunnel     -> ipsec_tunnel
      | `Ipsec_user       -> ipsec_user
      | `Time_stamping    -> time_stamping
      | `Ocsp_signing     -> ocsp_signing
      | `Other oid        -> oid
    in
    map (List.map f) (List.map g) @@ sequence_of oid

  let basic_constraints =
    map (fun (a, b) -> (Option.value ~default:false a, b))
        (fun (a, b) -> ((if a = false then None else Some a), b))
    @@
    sequence2
      (optional ~label:"cA"      bool)
      (optional ~label:"pathLen" int)

  let authority_key_id =
    map (fun (a, b, c) ->
        (a, Option.value ~default:General_name.empty b, c))
      (fun (a, b, c) ->
         (a, (if General_name.is_empty b then None else Some b), c))
    @@
    sequence3
      (optional ~label:"keyIdentifier"  @@ implicit 0 octet_string)
      (optional ~label:"authCertIssuer" @@ implicit 1 General_name.Asn.gen_names)
      (optional ~label:"authCertSN"     @@ implicit 2 serial)

  let priv_key_usage_period =
    let f = function
      | (Some t1, Some t2) -> `Interval (t1, t2)
      | (Some t1, None   ) -> `Not_before t1
      | (None   , Some t2) -> `Not_after  t2
      | _                  -> parse_error "empty PrivateKeyUsagePeriod"
    and g = function
      | `Interval (t1, t2) -> (Some t1, Some t2)
      | `Not_before t1     -> (Some t1, None   )
      | `Not_after  t2     -> (None   , Some t2) in
    map f g @@
    sequence2
      (optional ~label:"notBefore" @@ implicit 0 generalized_time_no_frac_s)
      (optional ~label:"notAfter"  @@ implicit 1 generalized_time_no_frac_s)

  let name_constraints =
    let subtree =
      map
        (fun (base, min, max) -> (base, Option.value ~default:0 min, max))
        (fun (base, min, max) -> (base, (if min = 0 then None else Some min), max))
      @@
      sequence3
        (required ~label:"base"       General_name.Asn.general_name)
        (optional ~label:"minimum" @@ implicit 0 int)
        (optional ~label:"maximum" @@ implicit 1 int)
    in
    map
      (fun (a, b) -> (Option.value ~default:[] a, Option.value ~default:[] b))
      (fun (a, b) -> ((if a = [] then None else Some a),
                      (if b = [] then None else Some b)))
    @@
    sequence2
      (optional ~label:"permittedSubtrees" @@ implicit 0 (sequence_of subtree))
      (optional ~label:"excludedSubtrees"  @@ implicit 1 (sequence_of subtree))

  let cert_policies =
    let open ID.Cert_policy in
    let qualifier_info =
      map (function | (oid, `C1 s) when oid = cps     -> s
                    | (oid, `C2 s) when oid = unotice -> s
                    | _ -> parse_error "bad policy qualifier")
        (function s -> (cps, `C1 s))
      @@
      sequence2
        (required ~label:"qualifierId" oid)
        (required ~label:"qualifier"
           (choice2
              ia5_string
            @@
            map (function (_, Some s) -> s | _ -> "#(BLAH BLAH)")
              (fun s -> (None, Some s))
              (sequence2
                 (optional ~label:"noticeRef"
                    (sequence2
                       (required ~label:"organization" display_text)
                       (required ~label:"numbers"      (sequence_of integer))))
                 (optional ~label:"explicitText" display_text))))
    in
    (* "Optional qualifiers, which MAY be present, are not expected to change
     * the definition of the policy."
     * Hence, we just drop them.  *)
    sequence_of @@
    map (function | (oid, _) when oid = any_policy -> `Any
                  | (oid, _)                       -> `Something oid)
      (function | `Any           -> (any_policy, None)
                | `Something oid -> (oid, None))
    @@
    sequence2
      (required ~label:"policyIdentifier" oid)
      (optional ~label:"policyQualifiers" (sequence_of qualifier_info))

  let reason : reason list Asn.t = bit_string_flags [
      0, `Unspecified
    ; 1, `Key_compromise
    ; 2, `CA_compromise
    ; 3, `Affiliation_changed
    ; 4, `Superseded
    ; 5, `Cessation_of_operation
    ; 6, `Certificate_hold
    ; 7, `Privilege_withdrawn
    ; 8, `AA_compromise
    ]

  let reason_enumerated : reason Asn.t =
    enumerated reason_of_int reason_to_int

  let distribution_point_name =
    map (function | `C1 s -> `Full s | `C2 s -> `Relative s)
      (function | `Full s -> `C1 s | `Relative s -> `C2 s)
    @@
    choice2
      (implicit 0 General_name.Asn.gen_names)
      (implicit 1 Distinguished_name.Asn.name)

  let distribution_point =
    sequence3
      (optional ~label:"distributionPoint" @@ explicit 0 distribution_point_name)
      (optional ~label:"reasons"           @@ implicit 1 reason)
      (optional ~label:"cRLIssuer"         @@ implicit 2 General_name.Asn.gen_names)

  let crl_distribution_points = sequence_of distribution_point

  let issuing_distribution_point =
    map
      (fun (a, b, c, d, e, f) ->
        (a,
         Option.value ~default:false b,
         Option.value ~default:false c,
         d,
         Option.value ~default:false e,
         Option.value ~default:false f))
      (fun (a, b, c, d, e, f) ->
         (a,
          (if b = false then None else Some b),
          (if c = false then None else Some c),
          d,
          (if e = false then None else Some e),
          (if f = false then None else Some f)))
    @@
    sequence6
      (optional ~label:"distributionPoint"          @@ explicit 0 distribution_point_name)
      (optional ~label:"onlyContainsUserCerts"      @@ implicit 1 bool)
      (optional ~label:"onlyContainsCACerts"        @@ implicit 2 bool)
      (optional ~label:"onlySomeReasons"            @@ implicit 3 reason)
      (optional ~label:"indirectCRL"                @@ implicit 4 bool)
      (optional ~label:"onlyContainsAttributeCerts" @@ implicit 5 bool)

  let crl_reason : reason Asn.t =
    let alist = [
        0, `Unspecified
      ; 1, `Key_compromise
      ; 2, `CA_compromise
      ; 3, `Affiliation_changed
      ; 4, `Superseded
      ; 5, `Cessation_of_operation
      ; 6, `Certificate_hold
      ; 8, `Remove_from_CRL
      ; 9, `Privilege_withdrawn
      ; 10, `AA_compromise
      ]
    in
    let rev = List.map (fun (k, v) -> (v, k)) alist in
    enumerated (fun i -> List.assoc i alist) (fun k -> List.assoc k rev)

  let gen_names_of_str, gen_names_to_str       = project_exn General_name.Asn.gen_names
  and auth_key_id_of_str, auth_key_id_to_str   = project_exn authority_key_id
  and subj_key_id_of_str, subj_key_id_to_str   = project_exn octet_string
  and key_usage_of_str, key_usage_to_str       = project_exn key_usage
  and e_key_usage_of_str, e_key_usage_to_str   = project_exn ext_key_usage
  and basic_constr_of_str, basic_constr_to_str = project_exn basic_constraints
  and pr_key_peri_of_str, pr_key_peri_to_str   = project_exn priv_key_usage_period
  and name_con_of_str, name_con_to_str         = project_exn name_constraints
  and crl_distrib_of_str, crl_distrib_to_str   = project_exn crl_distribution_points
  and cert_pol_of_str, cert_pol_to_str         = project_exn cert_policies
  and int_of_str, int_to_str                   = project_exn int
  and issuing_dp_of_str, issuing_dp_to_str     = project_exn issuing_distribution_point
  and crl_reason_of_str, crl_reason_to_str     = project_exn crl_reason
  and time_of_str, time_to_str                 = project_exn generalized_time_no_frac_s

  (* XXX 4.2.1.4. - cert policies! ( and other x509 extensions ) *)

  let reparse_extension_exn crit = case_of_oid_f [
      (ID.subject_alternative_name,
       fun cs -> B (Subject_alt_name, (crit, gen_names_of_str cs))) ;
      (ID.issuer_alternative_name,
       fun cs -> B (Issuer_alt_name, (crit, gen_names_of_str cs))) ;
      (ID.authority_key_identifier,
       fun cs -> B (Authority_key_id, (crit, auth_key_id_of_str cs))) ;
      (ID.subject_key_identifier,
       fun cs -> B (Subject_key_id, (crit, subj_key_id_of_str cs))) ;
      (ID.key_usage,
       fun cs -> B (Key_usage, (crit, key_usage_of_str cs))) ;
      (ID.basic_constraints,
       fun cs -> B (Basic_constraints, (crit, basic_constr_of_str cs))) ;
      (ID.crl_number,
       fun cs -> B (CRL_number, (crit, int_of_str cs))) ;
      (ID.delta_crl_indicator,
       fun cs -> B (Delta_CRL_indicator, (crit, int_of_str cs))) ;
      (ID.extended_key_usage,
       fun cs -> B (Ext_key_usage, (crit, e_key_usage_of_str cs))) ;
      (ID.private_key_usage_period,
       fun cs -> B (Priv_key_period, (crit, pr_key_peri_of_str cs))) ;
      (ID.name_constraints,
       fun cs -> B (Name_constraints, (crit, name_con_of_str cs))) ;
      (ID.crl_distribution_points,
       fun cs -> B (CRL_distribution_points, (crit, crl_distrib_of_str cs))) ;
      (ID.issuing_distribution_point,
       fun cs -> B (Issuing_distribution_point, (crit, issuing_dp_of_str cs))) ;
      (ID.freshest_crl,
       fun cs -> B (Freshest_CRL, (crit, crl_distrib_of_str cs))) ;
      (ID.reason_code,
       fun cs -> B (Reason, (crit, crl_reason_of_str cs))) ;
      (ID.invalidity_date,
       fun cs -> B (Invalidity_date, (crit, time_of_str cs))) ;
      (ID.certificate_issuer,
       fun cs -> B (Certificate_issuer, (crit, gen_names_of_str cs))) ;
      (ID.certificate_policies_2,
       fun cs -> B (Policies, (crit, cert_pol_of_str cs)))
    ]
      ~default:(fun oid -> fun cs -> B (Unsupported oid, (crit, cs)))

  let unparse_extension (B (k, v)) =
    let v' = match k, v with
      | Subject_alt_name, (_, x) -> gen_names_to_str x
      | Issuer_alt_name, (_, x) -> gen_names_to_str x
      | Authority_key_id, (_, x) -> auth_key_id_to_str x
      | Subject_key_id, (_, x) -> subj_key_id_to_str  x
      | Key_usage, (_, x) -> key_usage_to_str x
      | Basic_constraints, (_, x) -> basic_constr_to_str x
      | CRL_number, (_, x) -> int_to_str x
      | Delta_CRL_indicator, (_, x) -> int_to_str x
      | Ext_key_usage, (_, x) -> e_key_usage_to_str x
      | Priv_key_period, (_, x) -> pr_key_peri_to_str x
      | Name_constraints, (_, x) -> name_con_to_str x
      | CRL_distribution_points, (_, x) -> crl_distrib_to_str x
      | Issuing_distribution_point, (_, x) -> issuing_dp_to_str x
      | Freshest_CRL, (_, x) -> crl_distrib_to_str x
      | Reason, (_, x) -> crl_reason_to_str x
      | Invalidity_date, (_, x) -> time_to_str x
      | Certificate_issuer, (_, x) -> gen_names_to_str x
      | Policies, (_, x) -> cert_pol_to_str x
      | Unsupported _, (_, x) -> x
    in
    to_oid k, critical k v, v'

  let extensions_der =
    let extension =
      let f (oid, crit, cs) =
        reparse_extension_exn (Option.value ~default:false crit) (oid, cs)
      and g b =
        let oid, crit, cs = unparse_extension b in
        (oid, (if crit = false then None else Some crit), cs)
      in
      map f g @@
      sequence3
        (required ~label:"id"       oid)
        (optional ~label:"critical" bool) (* default false *)
        (required ~label:"value"    octet_string)
    in
    let f exts =
      List.fold_left (fun map (B (k, v)) ->
          match add_unless_bound k v map with
          | None -> parse_error "%a already bound" (pp_one k) v
          | Some b -> b)
        empty exts
    and g map = bindings map
    in
    map f g @@ sequence_of extension
end
