open Core
open State
open Handshake_common
open Config

let state_version state = match state.protocol_version with
  | #tls_before_13 as v -> v
  | _ -> assert false

let default_client_hello config =
  let host = match config.peer_name with
    | None   -> []
    | Some x -> [`Hostname x]
  in
  let version = max_protocol_version config.protocol_versions in
  let ecc_groups = match List.filter Config.elliptic_curve config.groups with
    | [] -> []
    | xs -> [ `ECPointFormats ; `SupportedGroups (List.map group_to_named_group xs) ]
  in
  let extensions, secrets = match version with
    | `TLS_1_0 | `TLS_1_1 -> (ecc_groups, [])
    | `TLS_1_2 ->
      (`SignatureAlgorithms config.signature_algorithms :: ecc_groups, [])
    | `TLS_1_3 ->
      let sig_alg = config.signature_algorithms (* TODO: filter deprecated ones *)
      and groups = List.map group_to_named_group config.groups
      and secrets, keyshares =
        (* OTOH, we could send all the keyshares (but this is pretty substantial size) *)
        (* instead we pick the first two groups and send keyshares *)
        let rec gen c gs acc = match c with
          | 0 -> List.rev acc
          | _ -> match gs with
            | [] -> List.rev acc (* TODO log? complain? *)
            | g::gs' ->
              let priv, share = Handshake_crypto13.dh_gen_key g in
              let acc' = ((g, priv),(group_to_named_group g, share)) :: acc in
              gen (pred c) gs' acc'
        in
        List.split (gen 2 config.groups [])
      in
      let all = all_versions config.protocol_versions in
      let supported_versions = List.map (fun x -> (x :> tls_any_version)) all in
      let point_format =
        if min_protocol_version config.protocol_versions = `TLS_1_3 then
          []
        else
          [ `ECPointFormats ]
      in
      let exts =
        point_format @ [`SignatureAlgorithms sig_alg ; `SupportedGroups groups ; `KeyShare keyshares ; `SupportedVersions supported_versions ]
      in
      (exts, secrets)
  in
  let alpn = match config.alpn_protocols with
    | [] -> []
    | protocols -> [`ALPN protocols]
  in
  let sessionid =
    match config.use_reneg, config.cached_session with
    | _, Some { session_id ; extended_ms ; _ } when extended_ms && not (String.length session_id = 0) -> Some session_id
    | false, Some { session_id ; _ } when not (String.length session_id = 0) -> Some session_id
    | _ -> None
  in
  let ch = {
    client_version = (version :> tls_any_version) ;
    client_random  = Mirage_crypto_rng.generate 32 ;
    sessionid      = sessionid ;
    ciphersuites   = List.map Ciphersuite.ciphersuite_to_any_ciphersuite config.ciphers ;
    extensions     = `ExtendedMasterSecret :: host @ extensions @ alpn
  }
  in
  (ch, version, secrets)

let common_server_hello_validation config reneg (sh : server_hello) (ch : client_hello) =
  let validate_reneg data =
    let err = `Fatal (`Handshake (`Message "invalid renegotiation")) in
    match reneg, data with
    | Some (cvd, svd), Some x -> guard (String.equal (cvd ^ svd) x) err
    | Some _, None -> Error err
    | None, Some x -> guard (String.length x = 0) err
    | None, None -> Ok ()
  in
  let* () =
    guard (List.mem sh.ciphersuite config.ciphers)
      (`Error (`NoConfiguredCiphersuite [sh.ciphersuite]))
  in
  let* () =
    guard (server_hello_valid sh &&
           server_exts_subset_of_client sh.extensions ch.extensions)
      (`Fatal `Unsupported_extension)
  in
  let* () =
    match get_alpn_protocol sh with
    | None -> Ok ()
    | Some x ->
      guard (List.mem x config.alpn_protocols) (`Fatal `Unsupported_extension)
  in
  validate_reneg (get_secure_renegotiation sh.extensions)

let common_server_hello_machina state (sh : server_hello) (ch : client_hello) raw log =
  let cipher = sh.ciphersuite in
  let session_id = Option.value ~default:"" sh.sessionid in
  let extended_ms =
    List.mem `ExtendedMasterSecret ch.extensions &&
    List.mem `ExtendedMasterSecret sh.extensions
  in
  let alpn_protocol = get_alpn_protocol sh in
  let session =
    let session = empty_session in
    let common_session_data = {
      session.common_session_data with
      client_random    = ch.client_random ;
      server_random    = sh.server_random ;
      alpn_protocol ;
    } in {
      session with
      common_session_data ;
      ciphersuite      = cipher ;
      session_id ;
      extended_ms ;
      client_version   = ch.client_version ;
    }
  in
  let state = { state with protocol_version = sh.server_version } in
  match Ciphersuite.ciphersuite_kex cipher with
  | #Ciphersuite.key_exchange_algorithm_dhe ->
    let machina = Client (AwaitCertificate_DHE (session, log @ [raw])) in
    Ok ({ state with machina }, [])
  | `RSA     ->
    let machina = Client (AwaitCertificate_RSA (session, log @ [raw])) in
    Ok ({ state with machina }, [])

let answer_server_hello state (ch : client_hello) sh secrets raw log =
  let validate_version requested (lo, _) server_version =
    guard (version_ge requested server_version && server_version >= lo)
      (`Error (`NoConfiguredVersions [ server_version ]))
  in

  let cfg = state.config in
  let* () = common_server_hello_validation cfg None sh ch in
  let* () = validate_version ch.client_version state.config.protocol_versions sh.server_version in

  let* () =
    if max_protocol_version state.config.protocol_versions = `TLS_1_3 then
      let* () =
        guard (not (Utils.sub_equal ~off:24 ~len:8 Packet.downgrade12 sh.server_random))
          (`Fatal (`Handshake (`Message "random contains downgrade TLS 1.2")))
      in
      guard (not (Utils.sub_equal ~off:24 ~len:8 Packet.downgrade11 sh.server_random))
          (`Fatal (`Handshake (`Message "random contains downgrade TLS 1.1")))
    else
      Ok ()
  in

  let epoch_matches (epoch : epoch_data) =
    epoch.ciphersuite = sh.ciphersuite &&
      epoch.protocol_version = sh.server_version &&
        Option.fold ~none:false ~some:(SessionID.equal epoch.session_id) sh.sessionid &&
          (not cfg.use_reneg ||
             (List.mem `ExtendedMasterSecret sh.extensions && epoch.extended_ms))
  in

  Tracing.debug (fun m -> m "version %a" pp_tls_version sh.server_version) ;
  trace_cipher sh.ciphersuite ;

  let state = { state with protocol_version = sh.server_version } in
  match sh.server_version with
  | #tls13 ->
    Handshake_client13.answer_server_hello state ch sh secrets raw (String.concat "" log)
  | #tls_before_13 as v ->
    match state.config.cached_session with
    | Some epoch when epoch_matches epoch ->
      let session =
        let session = session_of_epoch epoch in
        let common_session_data = {
          session.common_session_data with
          client_random = ch.client_random ;
          server_random = sh.server_random ;
          client_auth = match epoch.own_certificate with [] -> false | _ -> true ;
        } in
        { session with
          common_session_data ;
          client_version = ch.client_version ;
        }
      in
      let client_ctx, server_ctx =
        Handshake_crypto.initialise_crypto_ctx v session
      in
      let machina = AwaitServerChangeCipherSpecResume (session, client_ctx, server_ctx, log @ [raw]) in
      Ok ({ state with machina = Client machina }, [])
    | _ -> common_server_hello_machina state sh ch raw log

let answer_server_hello_renegotiate state session (ch : client_hello) sh raw log =
  let* () = common_server_hello_validation state.config (Some session.renegotiation) sh ch in
  let* () =
    guard (state.protocol_version = sh.server_version)
      (`Fatal (`Handshake (`Message "invalid renegotiation version")))
  in
  common_server_hello_machina state sh ch raw log

let validate_keyusage certificate kex =
  let usage = Ciphersuite.required_usage kex in
  let* cert =
    Option.to_result ~none:(`Fatal (`Bad_certificate "none received")) certificate
  in
  let* () =
    guard (supports_key_usage ~not_present:true usage cert)
      (`Fatal (`Bad_certificate "key usage"))
  in
  guard
    (supports_extended_key_usage `Server_auth cert ||
     supports_extended_key_usage ~not_present:true `Any cert)
    (`Fatal (`Bad_certificate "extended key usage"))

let answer_certificate_RSA state (session : session_data) cs raw log =
  let cfg = state.config in
  let* peer_certificate, received_certificates, peer_certificate_chain, trust_anchor =
    validate_chain cfg.authenticator cs cfg.ip cfg.peer_name
  in
  let* () = validate_keyusage peer_certificate `RSA in
  let session =
    let common_session_data = { session.common_session_data with received_certificates ; peer_certificate ; peer_certificate_chain ; trust_anchor } in
    { session with common_session_data }
  in
  let* version =
    match session.client_version with
    | `TLS_1_3 -> Ok `TLS_1_2
    | #tls_before_13 as v -> Ok v
    | _ -> assert false
  in
  let buf = Bytes.create (2 + 46) in
  let _ver = Writer.assemble_protocol_version ~buf version in
  Mirage_crypto_rng.generate_into buf ~off:2 46;
  let premaster = Bytes.unsafe_to_string buf in
  let* k = peer_key peer_certificate in
  match k with
  | `RSA key ->
    let kex = Mirage_crypto_pk.Rsa.PKCS1.encrypt ~key premaster in
    let kex = Writer.assemble_client_dh_key_exchange kex in
    let machina =
      AwaitCertificateRequestOrServerHelloDone
        (session, kex, premaster, log @ [raw])
    in
    Ok ({ state with machina = Client machina }, [])
  | _ -> Error (`Fatal (`Bad_certificate "not an RSA certificate"))

let answer_certificate_DHE state (session : session_data) cs raw log =
  let cfg = state.config in
  let* peer_certificate, received_certificates, peer_certificate_chain, trust_anchor =
    validate_chain cfg.authenticator cs cfg.ip cfg.peer_name
  in
  let* () = validate_keyusage peer_certificate `FFDHE in
  let session =
    let common_session_data = { session.common_session_data with received_certificates ; peer_certificate ; peer_certificate_chain ; trust_anchor } in
    { session with common_session_data }
  in
  let machina = AwaitServerKeyExchange_DHE (session, log @ [raw]) in
  Ok ({ state with machina = Client machina }, [])

let answer_server_key_exchange_DHE state (session : session_data) kex raw log =
  let* group, shared, raw_dh_params, leftover =
    if Ciphersuite.ecdhe session.ciphersuite then
      let* g, share, raw, left =
        map_reader_error (Reader.parse_ec_parameters kex)
      in
      Ok (`Ec g, share, raw, left)
    else
      let unpack_dh dh_params =
        Result.map_error
          (function `Msg m -> `Fatal (`Decode m))
          (Crypto.dh_params_unpack dh_params)
      in
      let* dh_params, raw_dh_params, leftover =
        map_reader_error (Reader.parse_dh_parameters kex)
      in
      let* group, shared = unpack_dh dh_params in
      let* () =
        guard (Mirage_crypto_pk.Dh.modulus_size group >= Config.min_dh_size)
          (`Fatal (`Handshake (`BadDH "too small")))
      in
      Ok (`Finite_field group, shared, raw_dh_params, leftover)
  in

  let sigdata =
    String.concat "" [
      session.common_session_data.client_random ;
      session.common_session_data.server_random ;
      raw_dh_params
    ]
  in
  let* () =
    verify_digitally_signed state.protocol_version
      state.config.signature_algorithms leftover sigdata
      session.common_session_data.peer_certificate
  in

  let* pms, kex =
    let open Mirage_crypto_ec in
    let map_ecdh_error =
      Result.map_error (fun e -> `Fatal (`Handshake (`BadECDH e)))
    in
    match group with
    | `Finite_field g ->
      let secret, client_share = Mirage_crypto_pk.Dh.gen_key g in
      let* pms =
        Option.to_result
          ~none:(`Fatal (`Handshake (`BadDH "invalid FF")))
          (Mirage_crypto_pk.Dh.shared secret shared)
      in
      Ok (pms, Writer.assemble_client_dh_key_exchange client_share)
   | `Ec `P256 ->
     let secret, client_share = P256.Dh.gen_key () in
     let* pms = map_ecdh_error (P256.Dh.key_exchange secret shared) in
     Ok (pms, Writer.assemble_client_ec_key_exchange client_share)
   | `Ec `P384 ->
     let secret, client_share = P384.Dh.gen_key () in
     let* pms = map_ecdh_error (P384.Dh.key_exchange secret shared) in
     Ok (pms, Writer.assemble_client_ec_key_exchange client_share)
   | `Ec `P521 ->
     let secret, client_share = P521.Dh.gen_key () in
     let* pms = map_ecdh_error (P521.Dh.key_exchange secret shared) in
     Ok (pms, Writer.assemble_client_ec_key_exchange client_share)
   | `Ec `X25519 ->
     let secret, client_share = X25519.gen_key () in
     let* pms = map_ecdh_error (X25519.key_exchange secret shared) in
     Ok (pms, Writer.assemble_client_ec_key_exchange client_share)
  in
  let machina =
    AwaitCertificateRequestOrServerHelloDone
      (session, kex, pms, log @ [raw])
  in
  Ok ({ state with machina = Client machina }, [])

let answer_certificate_request state (session : session_data) cr kex pms raw log =
  let cfg = state.config in
  let* _types, sigalgs, _cas =
    match state_version state with
    | `TLS_1_0 | `TLS_1_1 ->
      let* types, cas =
        map_reader_error (Reader.parse_certificate_request cr)
      in
      Ok (types, None, cas)
    | `TLS_1_2 ->
      let* types, sigalgs, cas =
        map_reader_error (Reader.parse_certificate_request_1_2 cr)
      in
      Ok (types, Some sigalgs, cas)
  in
  (* TODO: respect _types and _cas, multiple client certificates *)
  let own_certificate, own_private_key =
    match cfg.own_certificates with
    | `Single (chain, priv) -> (chain, Some priv)
    | _ -> ([], None)
  in
  let session =
    let common_session_data = {
      session.common_session_data with
      own_certificate ;
      own_private_key ;
      client_auth = true
    } in
    { session with common_session_data }
  in
  let machina = AwaitServerHelloDone (session, sigalgs, kex, pms, log @ [raw]) in
  Ok ({ state with machina = Client machina }, [])

let answer_server_hello_done state (session : session_data) sigalgs kex premaster raw log =
  let kex = ClientKeyExchange kex in
  let ckex = Writer.assemble_handshake kex in

  let* msgs, raw_msgs, raws, cert_verify =
    match session.common_session_data.client_auth, session.common_session_data.own_private_key with
    | true, Some p ->
       let cs = List.map X509.Certificate.encode_der session.common_session_data.own_certificate in
       let cert = Certificate (Writer.assemble_certificates cs) in
       let ccert = Writer.assemble_handshake cert in
       let to_sign = log @ [ raw ; ccert ; ckex ] in
       let data = String.concat "" to_sign in
       let ver = state.protocol_version
       and my_sigalgs = state.config.signature_algorithms in
       let* signature = signature ver data sigalgs my_sigalgs p in
       let cert_verify = CertificateVerify signature in
       let ccert_verify = Writer.assemble_handshake cert_verify in
       Ok ([ cert ; kex ; cert_verify ],
           [ ccert ; ckex ; ccert_verify ],
           to_sign, Some ccert_verify)
    | true, None ->
       let cert = Certificate (Writer.assemble_certificates []) in
       let ccert = Writer.assemble_handshake cert in
       Ok ([cert ; kex], [ccert ; ckex], log @ [ raw ; ccert ; ckex ], None)
    | false, _ ->
       Ok ([kex], [ckex], log @ [ raw ; ckex ], None)
  in

  let to_fin = raws @ Option.to_list cert_verify in

  let master_secret =
    Handshake_crypto.derive_master_secret (state_version state) session premaster raws
  in
  let session =
    let common_session_data = { session.common_session_data with master_secret } in
    { session with common_session_data }
  in
  let client_ctx, server_ctx =
    Handshake_crypto.initialise_crypto_ctx (state_version state) session
  in

  let checksum = Handshake_crypto.finished (state_version state) session.ciphersuite master_secret "client finished" to_fin in
  let fin = Finished checksum in
  let raw_fin = Writer.assemble_handshake fin in
  let session = { session with tls_unique = checksum } in
  let ps = to_fin @ [raw_fin] in

  let session =
    let common_session_data = { session.common_session_data with master_secret } in
    { session with common_session_data }
  in
  let machina = AwaitServerChangeCipherSpec (session, server_ctx, checksum, ps)
  and ccst, ccs = change_cipher_spec in

  List.iter (Tracing.hs ~tag:"handshake-out") msgs;
  Tracing.cs ~tag:"change-cipher-spec-out" ccs ;
  Tracing.cs ~tag:"master-secret" master_secret;
  Tracing.hs ~tag:"handshake-out" fin;

  Ok ({ state with machina = Client machina },
      List.map (fun x -> `Record (Packet.HANDSHAKE, x)) raw_msgs @
      [ `Record (ccst, ccs);
        `Change_enc client_ctx;
        `Record (Packet.HANDSHAKE, raw_fin)])

let answer_server_finished state (session : session_data) client_verify fin log =
  let computed =
    Handshake_crypto.finished (state_version state) session.ciphersuite session.common_session_data.master_secret "server finished" log
  in
  let* () =
    guard (String.equal computed fin)
      (`Fatal (`Handshake (`Message "couldn't verify finished")))
  in
  let* () =
    guard (String.length state.hs_fragment = 0) (`Fatal (`Handshake `Fragments))
  in
  let machina = Established
  and session = { session with renegotiation = (client_verify, computed) } in
  Ok ({ state with machina = Client machina ; session = `TLS session :: state.session }, [])

let answer_server_finished_resume state (session : session_data) fin raw log =
  let client, server =
    let checksum = Handshake_crypto.finished (state_version state) session.ciphersuite session.common_session_data.master_secret in
    (checksum "client finished" (log @ [raw]), checksum "server finished" log)
  in
  let* () =
    guard (String.equal server fin)
      (`Fatal (`Handshake (`Message "couldn't verify finished")))
  in
  let session = { session with tls_unique = server } in
  let* () =
    guard (String.length state.hs_fragment = 0)
      (`Fatal (`Handshake `Fragments))
  in
  let machina = Established
  and session = { session with renegotiation = (client, server) }
  in
  let finished = Finished client in
  let raw_finished = Writer.assemble_handshake finished in
  Tracing.hs ~tag:"handshake-out" finished ;
  Ok ({ state with machina = Client machina ; session = `TLS session :: state.session },
      [`Record (Packet.HANDSHAKE, raw_finished)])

let answer_hello_request state =
  let produce_client_hello session config exts =
     let dch, _, _ = default_client_hello config in
     let ch = { dch with extensions = dch.extensions @ exts ; sessionid = None } in
     let raw = Writer.assemble_handshake (ClientHello ch) in
     let machina = AwaitServerHelloRenegotiate (session, ch, [raw]) in
     Tracing.hs ~tag:"handshake-out" (ClientHello ch) ;
     ({ state with machina = Client machina }, [`Record (Packet.HANDSHAKE, raw)])
  in

  match state.config.use_reneg, state.session with
  | true , `TLS x :: _ ->
    let ext = `SecureRenegotiation (fst x.renegotiation) in
    Ok (produce_client_hello x state.config [ext])
  | true , _      -> Error (`Fatal (`Handshake (`Message "couldn't find session")))
  | false, _      ->
    let no_reneg = Writer.assemble_alert ~level:Packet.WARNING Packet.NO_RENEGOTIATION in
    Tracing.debug (fun m -> m "alert-out (warning, no_renegotiation)") ;
    Ok (state, [`Record (Packet.ALERT, no_reneg)])

let handle_change_cipher_spec cs state packet =
  let* () = map_reader_error (Reader.parse_change_cipher_spec packet) in
  match cs with
  | AwaitServerChangeCipherSpec (session, server_ctx, client_verify, log) ->
    let* () =
      guard (String.length state.hs_fragment = 0)
        (`Fatal (`Handshake `Fragments))
    in
    let machina = AwaitServerFinished (session, client_verify, log) in
    Tracing.cs ~tag:"change-cipher-spec-in" packet ;
    Ok ({ state with machina = Client machina }, [`Change_dec server_ctx])
  | AwaitServerChangeCipherSpecResume (session, client_ctx, server_ctx, log) ->
    let* () =
      guard (String.length state.hs_fragment = 0)
        (`Fatal (`Handshake `Fragments))
    in
    let ccs = change_cipher_spec in
    let machina = AwaitServerFinishedResume (session, log) in
    Tracing.cs ~tag:"change-cipher-spec-in" packet ;
    Tracing.cs ~tag:"change-cipher-spec-out" packet ;
    Ok ({ state with machina = Client machina },
        [`Record ccs ; `Change_enc client_ctx; `Change_dec server_ctx])
  | _ -> Error (`Fatal (`Unexpected (`Message "change cipher spec")))

let handle_handshake cs hs buf =
  let* handshake = map_reader_error (Reader.parse_handshake buf) in
  Tracing.hs ~tag:"handshake-in" handshake ;
  match cs, handshake with
  | AwaitServerHello (ch, secrets, log), ServerHello sh ->
    answer_server_hello hs ch sh secrets buf log
  | AwaitServerHello (ch, secrets, log), HelloRetryRequest hrr ->
    Handshake_client13.answer_hello_retry_request hs ch hrr secrets buf (String.concat "" log)
  | AwaitServerHelloRenegotiate (session, ch, log), ServerHello sh ->
    answer_server_hello_renegotiate hs session ch sh buf log
  | AwaitCertificate_RSA (session, log), Certificate cs ->
    let* cs = map_reader_error (Reader.parse_certificates cs) in
    answer_certificate_RSA hs session cs buf log
  | AwaitCertificate_DHE (session, log), Certificate cs ->
    let* cs = map_reader_error (Reader.parse_certificates cs) in
    answer_certificate_DHE hs session cs buf log
  | AwaitServerKeyExchange_DHE (session, log), ServerKeyExchange kex ->
    answer_server_key_exchange_DHE hs session kex buf log
  | AwaitCertificateRequestOrServerHelloDone (session, kex, pms, log), CertificateRequest cr ->
    answer_certificate_request hs session cr kex pms buf log
  | AwaitCertificateRequestOrServerHelloDone (session, kex, pms, log), ServerHelloDone ->
    answer_server_hello_done hs session None kex pms buf log
  | AwaitServerHelloDone (session, sigalgs, kex, pms, log), ServerHelloDone ->
    answer_server_hello_done hs session sigalgs kex pms buf log
  | AwaitServerFinished (session, client_verify, log), Finished fin ->
    answer_server_finished hs session client_verify fin log
  | AwaitServerFinishedResume (session, log), Finished fin ->
    answer_server_finished_resume hs session fin buf log
  | Established, HelloRequest ->
    answer_hello_request hs
  | _, hs -> Error (`Fatal (`Unexpected (`Handshake hs)))
