let ( let* ) = Result.bind

let sha2 = [ `SHA256 ; `SHA384 ; `SHA512 ]
let all_hashes = [ `MD5 ; `SHA1 ; `SHA224 ] @ sha2

let src = Logs.Src.create "x509.validation" ~doc:"X509 validation"
module Log = (val Logs.src_log src : Logs.LOG)

type signature_error = [
  | `Bad_signature of Distinguished_name.t * string
  | `Bad_encoding of Distinguished_name.t * string * string
  | `Hash_not_allowed of Distinguished_name.t * [ `MD5 | `SHA1 | `SHA224 | `SHA256 | `SHA384 | `SHA512 ]
  | `Unsupported_keytype of Distinguished_name.t * Public_key.t
  | `Unsupported_algorithm of Distinguished_name.t * string
  | `Msg of string
]

let pp_signature_error ppf = function
  | `Bad_signature (subj, msg) ->
    Fmt.pf ppf "failed to verify signature of %a: %s"
      Distinguished_name.pp subj msg
  | `Bad_encoding (subj, err, sig_) ->
    Fmt.pf ppf "bad signature encoding of %a, ASN error %s:@.%a"
      Distinguished_name.pp subj err Ohex.pp sig_
  | `Hash_not_allowed (subj, hash) ->
    Fmt.pf ppf "hash algorithm %a is not allowed, but %a is signed using it"
      Certificate.pp_hash hash Distinguished_name.pp subj
  | `Unsupported_keytype (subj, pk) ->
    Fmt.pf ppf "unsupported key used to sign %a: %a" Distinguished_name.pp subj
      Public_key.pp pk
  | `Unsupported_algorithm (subj, alg) ->
    Fmt.pf ppf "unsupported algorithm used to sign %a: %s"
      Distinguished_name.pp subj alg
  | `Msg msg -> Fmt.string ppf msg

let maybe_validate_hostname cert = function
  | None   -> true
  | Some x -> Certificate.supports_hostname cert x

let maybe_validate_ip cert = function
  | None -> true
  | Some ip -> Certificate.supports_ip cert ip

let issuer_matches_subject
    { Certificate.asn = parent ; _ } { Certificate.asn = cert ; _ } =
  Distinguished_name.equal parent.tbs_cert.subject cert.tbs_cert.issuer

let is_self_signed cert = issuer_matches_subject cert cert

let validate_raw_signature subject allowed_hashes msg sig_alg signature pk =
  match Algorithm.to_signature_algorithm sig_alg with
  | Some (scheme, siga) ->
    (* we check that siga is a member of allowed_hashes, to ensure not
       using a weak one. *)
    if not (List.mem siga allowed_hashes) then
      Error (`Hash_not_allowed (subject, siga))
    else if not (Key_type.supports_signature_scheme (Public_key.key_type pk) scheme) then
      Error (`Unsupported_keytype (subject, pk))
    else
      let* () =
        Result.map_error (function `Msg m -> `Bad_signature (subject, m))
          (Public_key.verify siga ~scheme ~signature pk (`Message msg))
      in
      if not (List.mem siga sha2) then
        Log.warn (fun m -> m "%a signature uses %a, a weak hash algorithm"
                     Distinguished_name.pp subject Certificate.pp_hash siga);
      Ok ()
  | None ->
    Error (`Unsupported_algorithm (subject, Algorithm.to_string sig_alg))

let shift str off =
  String.sub str off (String.length str - off)

(* XXX should return the tbs_cert blob from the parser, this is insane *)
let raw_cert_hack raw =
  (* we only support definite-length *)
  let loff = 1 in
  let snd = String.get_uint8 raw loff in
  let lenl = 2 + if 0x80 land snd = 0 then 0 else 0x7F land snd in
  (* cut away the SEQUENCE and LENGTH from outer sequence (tbs, sigalg, sig) *)
  let cert_buf = shift raw lenl in
  let rec l acc idx last =
    if idx = last then
      acc
    else
      l (acc lsl 8 + String.get_uint8 cert_buf idx) (succ idx) last
  in
  let cert_len_byte = String.get_uint8 cert_buf loff in
  let cert_len =
    (* two cases: *)
    if 0x80 land cert_len_byte = 0 then
      (* length < 127: highest bit is zero and lower 7 bits encode the length *)
      2 + (0x7F land cert_len_byte)
    else
      (* length > 127: highest bit is 1 and lower 7 bits encode the bytes used
         to encode the length *)
      let len_len = 2 + 0x7F land cert_len_byte in
      len_len + (l 0 2 len_len)
  in
  String.sub cert_buf 0 cert_len

let validate_signature allowed_hashes { Certificate.asn = trusted ; _ } { Certificate.asn ; raw } =
  let tbs_raw = raw_cert_hack raw in
  validate_raw_signature asn.tbs_cert.subject allowed_hashes tbs_raw
    asn.signature_algo asn.signature_val trusted.tbs_cert.pk_info

let validate_time time { Certificate.asn = cert ; _ } =
  match time with
  | None     -> true
  | Some now ->
    let (not_before, not_after) = cert.tbs_cert.validity in
    Ptime.(is_later ~than:not_before now && is_earlier ~than:not_after now)

let version_matches_extensions { Certificate.asn = cert ; _ } =
  let tbs = cert.tbs_cert in
  match tbs.version, Extension.is_empty tbs.extensions with
  | (`V1 | `V2), true -> true
  | (`V1 | `V2), _ -> false
  | `V3, _ -> true

let validate_path_len pathlen { Certificate.asn = cert ; _ } =
  (* X509 V1/V2 certificates do not contain X509v3 extensions! *)
  (* thus, we cannot check the path length. this will only ever happen for trust anchors: *)
  (* intermediate CAs are checked by is_cert_valid, which checks that the CA extensions are there *)
  (* whereas trust anchor are ok with getting V1/2 certificates *)
  (* TODO: make it configurable whether to accept V1/2 certificates at all *)
  let exts = cert.tbs_cert.extensions in
  match cert.tbs_cert.version, Extension.(find Basic_constraints exts) with
  | (`V1 | `V2), _ -> true
  | `V3, Some (_ , (true, None)) -> true
  | `V3, Some (_ , (true, Some n)) -> n >= pathlen
  | _ -> false

let validate_ca_extensions { Certificate.asn = cert ; _ } =
  let exts = cert.tbs_cert.extensions in
  (* comments from RFC5280 *)
  (* 4.2.1.9 Basic Constraints *)
  (* Conforming CAs MUST include this extension in all CA certificates used *)
  (* to validate digital signatures on certificates and MUST mark the *)
  (* extension as critical in such certificates *)
  (* unfortunately, there are 8 CA certs (including the one which
     signed google.com) which are _NOT_ marked as critical *)
  ( match Extension.(find Basic_constraints exts) with
    | Some (_ , (true, _)) -> true
    | _ -> false ) &&

  (* 4.2.1.3 Key Usage *)
  (* Conforming CAs MUST include key usage extension *)
  (* CA Cert (cacert.org) does not *)
  ( match Extension.(find Key_usage exts) with
    (* When present, conforming CAs SHOULD mark this extension as critical *)
    (* yeah, you wish... *)
    | Some (_, usage) -> List.mem `Key_cert_sign usage
    | _ -> false ) &&

  (* if we require this, we cannot talk to github.com
     (* 4.2.1.12.  Extended Key Usage
     If a certificate contains both a key usage extension and an extended
     key usage extension, then both extensions MUST be processed
     independently and the certificate MUST only be used for a purpose
     consistent with both extensions.  If there is no purpose consistent
     with both extensions, then the certificate MUST NOT be used for any
     purpose. *)
     ( match extn_ext_key_usage cert with
     | Some (_, Ext_key_usage usages) -> List.mem Any usages
     | _                              -> true ) &&
  *)

  (* Name Constraints - name constraints should match servername *)

  (* check criticality *)
  Extension.for_all (fun (Extension.B (k, v)) ->
      match k with
      | Extension.Key_usage -> true
      | Extension.Basic_constraints -> true
      | _ -> not (Extension.critical k v) )
    exts

let validate_server_extensions cert =
  Extension.for_all (fun (Extension.B (k, v)) ->
      match k, v with
      | Extension.Basic_constraints, (_, (true, _)) ->
        if is_self_signed cert then
          (Log.warn (fun m -> m "allowing self-signed certificate with BasicConstraints CA true");
           true)
        else
          false
      | Extension.Basic_constraints, (_, (false, _)) -> true
      | Extension.Key_usage, _ -> true
      | Extension.Ext_key_usage, _ -> true
      | Extension.Subject_alt_name, _ -> true
      | Extension.Policies, (crit, ps) -> not crit || List.mem `Any ps
      (* we've to deal with _all_ extensions marked critical! *)
      | _, _ -> not (Extension.critical k v))
    cert.Certificate.asn.tbs_cert.extensions

let valid_trust_anchor_extensions cert =
  match cert.Certificate.asn.tbs_cert.version with
  | `V1 | `V2 -> true
  | `V3       -> validate_ca_extensions cert

let ext_authority_matches_subject trusted cert =
  match Extension.(find Authority_key_id (Certificate.extensions cert),
                   find Subject_key_id (Certificate.extensions trusted))
  with
  | (_, None) | (None, _)                       -> true (* not mandatory *)
  | Some (_, (Some auth, _, _)), Some (_, au)   -> String.equal auth au
  (* TODO: check exact rules in RFC5280 *)
  | Some (_, (None, _, _)), _                   -> true (* not mandatory *)

(* t -> t list (* set *) -> t list list *)
let rec build_paths fst rst =
  match
    List.filter
      (fun x -> Distinguished_name.equal (Certificate.issuer fst) (Certificate.subject x))
      rst
  with
  | [] -> [[fst]]
  | xs ->
    let tails =
      List.fold_left
        (fun acc x -> acc @ build_paths x (List.filter (fun y -> x <> y) rst))
        [[]]
        xs
    in
    List.map (fun x -> fst :: x) tails

type ca_error = [
  | signature_error
  | `CAIssuerSubjectMismatch of Certificate.t
  | `CAInvalidVersion of Certificate.t
  | `CACertificateExpired of Certificate.t * Ptime.t option
  | `CAInvalidExtensions of Certificate.t
]

let pp_ca_error ppf = function
  | #signature_error as e -> pp_signature_error ppf e
  | `CAIssuerSubjectMismatch c ->
    Fmt.pf ppf "CA certificate %a: issuer does not match subject" Certificate.pp c
  | `CAInvalidVersion c ->
    Fmt.pf ppf "CA certificate %a: version 3 is required for extensions" Certificate.pp c
  | `CAInvalidExtensions c ->
    Fmt.pf ppf "CA certificate %a: invalid CA extensions" Certificate.pp c
  | `CACertificateExpired (c, now) ->
    let pp_pt = Ptime.pp_human ~tz_offset_s:0 () in
    Fmt.pf ppf "CA certificate %a: expired (now %a)" Certificate.pp c
      Fmt.(option ~none:(any "no timestamp provided") pp_pt) now

type leaf_validation_error = [
  | `LeafCertificateExpired of Certificate.t * Ptime.t option
  | `LeafInvalidIP of Certificate.t * Ipaddr.t option
  | `LeafInvalidName of Certificate.t * [`host] Domain_name.t option
  | `LeafInvalidVersion of Certificate.t
  | `LeafInvalidExtensions of Certificate.t
]

let pp_leaf_validation_error ppf = function
  | `LeafCertificateExpired (c, now) ->
    let pp_pt = Ptime.pp_human ~tz_offset_s:0 () in
    Fmt.pf ppf "leaf certificate %a expired (now %a)" Certificate.pp c
      Fmt.(option ~none:(any "no timestamp provided") pp_pt) now
  | `LeafInvalidIP (c, ip) ->
    Fmt.pf ppf "leaf certificate %a does not contain the IP %a (IPs present: %a)String"
      Certificate.pp c Fmt.(option ~none:(any "none") Ipaddr.pp) ip
      Fmt.(list ~sep:(any ", ") Ipaddr.pp) (Certificate.ips c |> Ipaddr.Set.elements)
  | `LeafInvalidName (c, n) ->
    Fmt.pf ppf "leaf certificate %a does not contain the name %a"
      Certificate.pp c Fmt.(option ~none:(any "none") Domain_name.pp) n
  | `LeafInvalidVersion c ->
    Fmt.pf ppf "leaf certificate %a: version 3 is required for extensions" Certificate.pp c
  | `LeafInvalidExtensions c ->
    Fmt.pf ppf "leaf certificate %a: invalid server extensions" Certificate.pp c

type chain_validation_error = [
  | `IntermediateInvalidExtensions of Certificate.t
  | `IntermediateCertificateExpired of Certificate.t * Ptime.t option
  | `IntermediateInvalidVersion of Certificate.t
  | `ChainIssuerSubjectMismatch of Certificate.t * Certificate.t
  | `ChainAuthorityKeyIdSubjectKeyIdMismatch of Certificate.t * Certificate.t
  | `ChainInvalidPathlen of Certificate.t * int
  | `EmptyCertificateChain
  | `NoTrustAnchor of Certificate.t
  | `Revoked of Certificate.t
]

let pp_chain_validation_error ppf = function
  | `IntermediateInvalidExtensions c ->
    Fmt.pf ppf "intermediate certificate %a: invalid extensions" Certificate.pp c
  | `IntermediateCertificateExpired (c, now) ->
    let pp_pt = Ptime.pp_human ~tz_offset_s:0 () in
    Fmt.pf ppf "intermediate certificate %a expired (now %a)" Certificate.pp c
      Fmt.(option ~none:(any "no timestamp provided") pp_pt) now
  | `IntermediateInvalidVersion c ->
    Fmt.pf ppf "intermediate certificate %a: version 3 is required for extensions"
      Certificate.pp c
  | `ChainIssuerSubjectMismatch (c, parent) ->
    Fmt.pf ppf "invalid chain: issuer of %a does not match subject of %a"
      Certificate.pp c Certificate.pp parent
  | `ChainAuthorityKeyIdSubjectKeyIdMismatch (c, parent) ->
    Fmt.pf ppf "invalid chain: authority key id extension of %a does not match subject key id extension of %a"
      Certificate.pp c Certificate.pp parent
  | `ChainInvalidPathlen (c, pathlen) ->
    Fmt.pf ppf "invalid chain: the path length of %a is smaller than the required path length %d"
      Certificate.pp c pathlen
  | `EmptyCertificateChain -> Fmt.string ppf "certificate chain is empty"
  | `NoTrustAnchor c ->
    Fmt.pf ppf "no trust anchor found for %a" Certificate.pp c
  | `Revoked c ->
    Fmt.pf ppf "certificate %a is revoked" Certificate.pp c

type chain_error = [
  | signature_error
  | leaf_validation_error
  | chain_validation_error
]

let pp_chain_error ppf = function
  | #signature_error as e -> pp_signature_error ppf e
  | #leaf_validation_error as l -> pp_leaf_validation_error ppf l
  | #chain_validation_error as c -> pp_chain_validation_error ppf c

type fingerprint_validation_error = [
  | `InvalidFingerprint of Certificate.t * string * string
]

let pp_fingerprint_validation_error ppf = function
  | `InvalidFingerprint (c, c_fp, fp) ->
    Fmt.pf ppf "fingerprint for %a (computed %a) does not match, expected %a"
      Certificate.pp c Ohex.pp c_fp Ohex.pp fp

type validation_error = [
  | signature_error
  | leaf_validation_error
  | fingerprint_validation_error
  | `EmptyCertificateChain
  | `InvalidChain
]

let pp_validation_error ppf = function
  | #signature_error as e -> pp_signature_error ppf e
  | #leaf_validation_error as l -> pp_leaf_validation_error ppf l
  | #fingerprint_validation_error as f -> pp_fingerprint_validation_error ppf f
  | `EmptyCertificateChain ->
    Fmt.string ppf "provided certificate chain is empty"
  | `InvalidChain -> Fmt.string ppf "invalid certificate chain"

type r = ((Certificate.t list * Certificate.t) option, validation_error) result

(* TODO RFC 5280: A certificate MUST NOT include more than one
   instance of a particular extension. *)

let is_cert_valid now cert =
  match
    validate_time now cert,
    version_matches_extensions cert,
    validate_ca_extensions cert
  with
  | (true, true, true) -> Ok ()
  | (false, _, _)      -> Error (`IntermediateCertificateExpired (cert, now))
  | (_, false, _)      -> Error (`IntermediateInvalidVersion cert)
  | (_, _, false)      -> Error (`IntermediateInvalidExtensions cert)

let is_ca_cert_valid allowed_hashes now cert =
  match
    is_self_signed cert,
    version_matches_extensions cert,
    validate_signature allowed_hashes cert cert,
    validate_time now cert,
    valid_trust_anchor_extensions cert
  with
  | (true, true, Ok (), true, true) -> Ok ()
  | (false, _, _, _, _)            -> Error (`CAIssuerSubjectMismatch cert)
  | (_, false, _, _, _)            -> Error (`CAInvalidVersion cert)
  | (_, _, Error e, _, _)          -> Error e
  | (_, _, _, false, _)            -> Error (`CACertificateExpired (cert, now))
  | (_, _, _, _, false)            -> Error (`CAInvalidExtensions cert)

let valid_ca ?(allowed_hashes = all_hashes) ?time cacert =
  is_ca_cert_valid allowed_hashes time cacert

let is_server_cert_valid ip host now cert =
  match
    validate_time now cert,
    maybe_validate_ip cert ip,
    maybe_validate_hostname cert host,
    version_matches_extensions cert,
    validate_server_extensions cert
  with
  | (true, true, true, true, true) -> Ok ()
  | (false, _, _, _, _)         -> Error (`LeafCertificateExpired (cert, now))
  | (_, false, _, _, _)         -> Error (`LeafInvalidIP (cert, ip))
  | (_, _, false, _, _)         -> Error (`LeafInvalidName (cert, host))
  | (_, _, _, false, _)         -> Error (`LeafInvalidVersion cert)
  | (_, _, _, _, false)         -> Error (`LeafInvalidExtensions cert)

let signs hash pathlen trusted cert =
  match
    issuer_matches_subject trusted cert,
    ext_authority_matches_subject trusted cert,
    validate_signature hash trusted cert,
    validate_path_len pathlen trusted
  with
  | (true, true, Ok (), true) -> Ok ()
  | (false, _, _, _)         -> Error (`ChainIssuerSubjectMismatch (trusted, cert))
  | (_, false, _, _)         -> Error (`ChainAuthorityKeyIdSubjectKeyIdMismatch (trusted, cert))
  | (_, _, Error e, _)       -> Error e
  | (_, _, _, false)         -> Error (`ChainInvalidPathlen (trusted, pathlen))

let issuer trusted cert =
  List.filter (fun p -> issuer_matches_subject p cert) trusted

let rec validate_anchors revoked hash pathlen cert = function
  | []    -> Error (`NoTrustAnchor cert)
  | x::xs -> match signs hash pathlen x cert with
    | Ok _    -> if revoked ~issuer:x ~cert then Error (`Revoked cert) else Ok x
    | Error _ -> validate_anchors revoked hash pathlen cert xs

let verify_single_chain now ?(revoked = fun ~issuer:_ ~cert:_ -> false) hash anchors chain =
  let rec climb pathlen = function
    | cert :: issuer :: certs ->
      let* () = is_cert_valid now issuer in
      let* () = if revoked ~issuer ~cert then Error (`Revoked cert) else Ok () in
      let* () = signs hash pathlen issuer cert in
      climb (succ pathlen) (issuer :: certs)
    | [c] ->
      let anchors = issuer anchors c in
      validate_anchors revoked hash pathlen c anchors
    | [] -> Error `EmptyCertificateChain
  in
  climb 0 chain

let verify_chain ?ip ~host ~time ?revoked ?(allowed_hashes = sha2) ~anchors = function
  | [] -> Error `EmptyCertificateChain
  | server :: certs ->
    let now = time () in
    let anchors = List.filter (validate_time now) anchors in
    let* () = is_server_cert_valid ip host now server in
    verify_single_chain now ?revoked allowed_hashes anchors (server :: certs)

let rec any_m e f = function
  | [] -> Error e
  | c::cs -> match f c with
    | Ok ta -> Ok (Some (c, ta))
    | Error _ -> any_m e f cs

let verify_chain_of_trust ?ip ~host ~time ?revoked ?(allowed_hashes = sha2) ~anchors = function
  | [] -> Error `EmptyCertificateChain
  | server :: certs ->
    let now = time () in
    (* verify server! *)
    let* () = is_server_cert_valid ip host now server in
    (* build all paths *)
    let paths = build_paths server certs
    and anchors = List.filter (validate_time now) anchors
    in
    (* exists there one which is good? *)
    any_m `InvalidChain (verify_single_chain now ?revoked allowed_hashes anchors) paths

let valid_cas ?(allowed_hashes = all_hashes) ?time cas =
  List.filter (fun cert ->
      Result.is_ok (is_ca_cert_valid allowed_hashes time cert))
    cas

let fingerprint_verification ?ip host now fingerprint fp = function
  | [] -> Error `EmptyCertificateChain
  | server::_ ->
    let computed_fingerprint = fp server in
    if String.equal computed_fingerprint fingerprint then
      match
        validate_time now server,
        maybe_validate_hostname server host,
        maybe_validate_ip server ip
      with
      | true , true , true  -> Ok None
      | false, _    , _     -> Error (`LeafCertificateExpired (server, now))
      | _    , false, _     -> Error (`LeafInvalidName (server, host))
      | _    , _    , false -> Error (`LeafInvalidIP (server, ip))
    else
      Error (`InvalidFingerprint (server, computed_fingerprint, fingerprint))

let trust_key_fingerprint ?ip ~host ~time ~hash ~fingerprint =
  let now = time () in
  let fp cert = Public_key.fingerprint ~hash (Certificate.public_key cert) in
  fingerprint_verification ?ip host now fingerprint fp

let trust_cert_fingerprint ?ip ~host ~time ~hash ~fingerprint =
  let now = time () in
  let fp = Certificate.fingerprint hash in
  fingerprint_verification ?ip host now fingerprint fp

(* RFC5246 says 'root certificate authority MAY be omitted' *)

(* TODO: how to deal with
    2.16.840.1.113730.1.1 - Netscape certificate type
    2.16.840.1.113730.1.12 - SSL server name
    2.16.840.1.113730.1.13 - Netscape certificate comment *)

(* stuff from 4366 (TLS extensions):
  - root CAs
  - client cert url *)

(* Future TODO Certificate Revocation Lists and OCSP (RFC6520)
2.16.840.1.113730.1.2 - Base URL
2.16.840.1.113730.1.3 - Revocation URL
2.16.840.1.113730.1.4 - CA Revocation URL
2.16.840.1.113730.1.7 - Renewal URL
2.16.840.1.113730.1.8 - Netscape CA policy URL

2.5.4.38 - id-at-authorityRevocationList
2.5.4.39 - id-at-certificateRevocationList

do not forget about 'authority information access' (private internet extension -- 4.2.2 of 5280) *)

(* Future TODO: Policies
2.5.29.32 - Certificate Policies
2.5.29.33 - Policy Mappings
2.5.29.36 - Policy Constraints
 *)

(* Future TODO: anything with subject_id and issuer_id ? seems to be not used by anybody *)
