open Dns

let src = Logs.Src.create "dns_certify" ~doc:"DNS certify"
module Log = (val Logs.src_log src : Logs.LOG)

let tlsa_is usage sel typ t =
  t.Tlsa.cert_usage = usage &&
  t.Tlsa.selector = sel &&
  t.Tlsa.matching_type = typ

let is_csr t =
  tlsa_is Tlsa.Domain_issued_certificate Tlsa.Private Tlsa.No_hash t

let csr req =
  let data = X509.Signing_request.encode_der req in
  {
    Tlsa.matching_type = Tlsa.No_hash ;
    cert_usage = Tlsa.Domain_issued_certificate ;
    selector = Tlsa.Private ;
    data
  }

let is_certificate t =
  tlsa_is Tlsa.Domain_issued_certificate Tlsa.Full_certificate Tlsa.No_hash t

let certificate cert =
  let data = X509.Certificate.encode_der cert in
  {
    Tlsa.matching_type = Tlsa.No_hash ;
    cert_usage = Tlsa.Domain_issued_certificate ;
    selector = Tlsa.Full_certificate ;
    data
  }

let is_ca_certificate t =
  tlsa_is Tlsa.CA_constraint Tlsa.Full_certificate Tlsa.No_hash t

let ca_certificate data = {
  Tlsa.matching_type = Tlsa.No_hash ;
  cert_usage = Tlsa.CA_constraint ;
  selector = Tlsa.Full_certificate ;
  data
}

let signing_request hostname ?(more_hostnames = []) key =
  let host = Domain_name.to_string hostname in
  let extensions =
    match more_hostnames with
    | [] -> X509.Signing_request.Ext.empty
    | _ ->
      let ext =
        let additional = List.map Domain_name.to_string more_hostnames in
        let gn = X509.General_name.(singleton DNS (host :: additional)) in
        X509.Extension.(singleton Subject_alt_name (false, gn))
      in
      X509.Signing_request.Ext.(singleton Extensions ext)
  in
  X509.(Signing_request.create
          [Distinguished_name.(Relative_distinguished_name.singleton (CN host))]
          ~extensions key)

let dns_header rng =
  let id = Randomconv.int16 rng in
  (id, Packet.Flags.empty)

let le_label = "_letsencrypt"
and p_label = "_tcp"

let is_name name =
  if Domain_name.count_labels name < 2 then
    false
  else
    Domain_name.(equal_label le_label (get_label_exn name 0) &&
                 equal_label p_label (get_label_exn name 1))

let letsencrypt_name name =
  match Domain_name.(prepend_label (raw name) p_label) with
  | Ok name' -> Domain_name.prepend_label name' le_label
  | Error e -> Error e

type u_err = [ `Tsig of Dns_tsig.e | `Bad_reply of Packet.mismatch * Packet.t | `Unexpected_reply of Packet.reply ]

let pp_u_err ppf = function
  | `Tsig e -> Fmt.pf ppf "tsig error %a" Dns_tsig.pp_e e
  | `Bad_reply (e, res) -> Fmt.pf ppf "bad reply %a: %a" Packet.pp_mismatch e Packet.pp res
  | `Unexpected_reply r -> Fmt.pf ppf "unexpected reply %a" Packet.pp_reply r

let nsupdate rng now ~host ~keyname ~zone dnskey request =
  match letsencrypt_name host with
  | Error e -> Error e
  | Ok host ->
    let tlsa = csr request in
    let zone = Packet.Question.create zone Soa
    and update =
      let up =
        Domain_name.Map.singleton host
          [
            Packet.Update.Remove (K Tlsa) ;
            Packet.Update.Add (B (Tlsa, (3600l, Rr_map.Tlsa_set.singleton tlsa)))
          ]
      in
      (Domain_name.Map.empty, up)
    and header = dns_header rng
    in
    let packet = Packet.create header zone (`Update update) in
    let now = now () in
    match Dns_tsig.encode_and_sign ~proto:`Tcp packet now dnskey keyname with
    | Error e -> Error (`Msg (Fmt.to_to_string Dns_tsig.pp_s e))
    | Ok (data, mac) ->
      Ok (data, (fun data ->
          match Dns_tsig.decode_and_verify now dnskey keyname ~mac data with
          | Error e -> Error (`Tsig e)
          | Ok (res, _, _) ->
            match Packet.reply_matches_request ~request:packet res with
            | Ok `Update_ack -> Ok ()
            | Ok r -> Error (`Unexpected_reply r)
            | Error e -> Error (`Bad_reply (e, res))))

type q_err = [
  | `Decode of Packet.err
  | `Bad_reply of Packet.mismatch * Packet.t
  | `Unexpected_reply of Packet.reply
  | `No_tlsa
]

let pp_q_err ppf = function
  | `Decode err -> Fmt.pf ppf "decoding failed %a" Packet.pp_err err
  | `Bad_reply (e, res) -> Fmt.pf ppf "bad reply %a: %a" Packet.pp_mismatch e Packet.pp res
  | `Unexpected_reply r -> Fmt.pf ppf "unexpected reply %a" Packet.pp_reply r
  | `No_tlsa -> Fmt.pf ppf "No TLSA record found"

(* may be better suited in X509? *)
let cert_matches_csr ?until now csr cert =
  let until = match until with None -> now | Some x -> x in
  let csr_key = X509.Signing_request.((info csr).public_key)
  and csr_hostnames = X509.Signing_request.hostnames csr
  and cert_key = X509.Certificate.public_key cert
  and cert_hostnames = X509.Certificate.hostnames cert
  and (st, en) = X509.Certificate.validity cert
  in
  let valid = Ptime.is_later ~than:st now && Ptime.is_later ~than:until en in
  if not (String.equal (X509.Public_key.fingerprint cert_key) (X509.Public_key.fingerprint csr_key)) then begin
    Log.info (fun m -> m "public key of CSR and certificate %a do not match"
                 X509.Certificate.pp cert);
    false
  end else if not (X509.Host.Set.equal cert_hostnames csr_hostnames) then begin
    Log.info (fun m -> m "hostnames of CSR %a and certificate %a do not match"
                 X509.Host.Set.pp csr_hostnames X509.Host.Set.pp cert_hostnames);
    false
  end else if not valid then begin
    let pp_pt = Ptime.pp_rfc3339 () in
    Log.info (fun m -> m "Certificate is not valid now %a (until %a), it is \
                          valid from %a until %a)"
                 pp_pt now pp_pt until pp_pt st pp_pt en);
    false
  end else
    true

let tlsas_to_certchain host now csr tlsas =
  let certificates, ca_certificates =
    Rr_map.Tlsa_set.fold (fun tlsa (certs, cacerts as acc) ->
        if is_certificate tlsa || is_ca_certificate tlsa then
          match X509.Certificate.decode_der tlsa.Tlsa.data with
          | Error (`Msg msg) ->
            Log.warn (fun m -> m "couldn't decode tlsa record %a: %s (%a)"
                         Domain_name.pp host msg
                         Ohex.pp tlsa.Tlsa.data);
            acc
          | Ok cert ->
            match is_certificate tlsa, is_ca_certificate tlsa with
            | true, _ -> (cert :: certs, cacerts)
            | _, true -> (certs, cert :: cacerts)
            | _ -> acc
        else acc)
      tlsas ([], [])
  in
  match List.find_opt (cert_matches_csr now csr) certificates with
  | None -> Error `No_tlsa
  | Some server_cert ->
    match List.rev (X509.Validation.build_paths server_cert ca_certificates) with
    | (_server :: chain) :: _ -> Ok (server_cert, chain)
    | _ -> Ok (server_cert, []) (* build_paths always returns the server_cert *)

let query rng now host csr =
  match letsencrypt_name host with
  | Error e -> Error e
  | Ok host ->
    let header = dns_header rng
    and question = Packet.Question.create host Tlsa
    in
    let request = Packet.create header question `Query in
    let out, _ = Packet.encode `Tcp request
    and react data =
      match Packet.decode data with
      | Error e -> Error (`Decode e)
      | Ok reply ->
        match Packet.reply_matches_request ~request reply with
        | Ok (`Answer (answer, _)) ->
          begin match Name_rr_map.find host Tlsa answer with
            | None -> Error `No_tlsa
            | Some (_, tlsas) -> tlsas_to_certchain host now csr tlsas
          end
        | Ok (`Rcode_error (Rcode.NXDomain, Opcode.Query, _)) -> Error `No_tlsa
        | Ok reply -> Error (`Unexpected_reply reply)
        | Error e -> Error (`Bad_reply (e, reply))
    in
    Ok (out, react)
