open State open Core open Handshake_common open Handshake_crypto13 let answer_client_hello ~hrr state ch raw = let* () = client_hello_valid `TLS_1_3 ch in let* () = guard (not (hrr && List.mem `EarlyDataIndication ch.extensions)) (`Fatal (`Handshake (`Message "has 0RTT after hello retry request"))) in Tracing.debug (fun m -> m "version %a" pp_tls_version `TLS_1_3) ; let ciphers = List.filter_map Ciphersuite.any_ciphersuite_to_ciphersuite13 ch.ciphersuites in let* groups = let* gs = Option.to_result ~none:(`Fatal (`Missing_extension "supported group")) (Utils.map_find ~f:(function `SupportedGroups gs -> Some gs | _ -> None) ch.extensions) in Ok (List.filter_map Core.named_group_to_group gs) in let* keyshares = let* ks = Option.to_result ~none:(`Fatal (`Missing_extension "key share")) (Utils.map_find ~f:(function `KeyShare ks -> Some ks | _ -> None) ch.extensions) in List.fold_left (fun acc (g, ks) -> let* acc = acc in match Core.named_group_to_group g with | None -> Ok acc | Some g -> Ok ((g, ks) :: acc)) (Ok []) ks in let base_server_hello ?epoch cipher extensions = let ciphersuite = (cipher :> Ciphersuite.ciphersuite) in let sh = { server_version = `TLS_1_3 ; server_random = Mirage_crypto_rng.generate 32 ; sessionid = ch.sessionid ; ciphersuite ; extensions } in let session : session_data13 = let base = match epoch with None -> empty_session13 cipher | Some e -> session13_of_epoch cipher e in let common_session_data13 = { base.common_session_data13 with server_random = sh.server_random ; client_random = ch.client_random ; } in let resumed = match epoch with None -> false | Some _ -> true in { base with common_session_data13 ; ciphersuite13 = cipher ; resumed } in (sh, session) in let config = state.config in match Utils.first_match (List.map fst keyshares) config.Config.groups, Utils.first_match ciphers (Config.ciphers13 config) with | _, None -> Error (`Error (`NoConfiguredCiphersuite ciphers)) | None, Some cipher -> if hrr then (* avoid loops CH -> HRR -> CH -> HRR -> ... *) Error (`Fatal (`Handshake (`Message "hello retry request already sent, still no supported group"))) else (* no keyshare, looks whether there's a supported group ++ send back HRR *) begin match Utils.first_match groups config.Config.groups with | None -> Error (`Fatal (`Handshake (`Message "no supported group found"))) | Some group -> let cookie = let module H = (val Digestif.module_of_hash' (Ciphersuite.hash13 cipher)) in H.(to_raw_string (digest_string raw)) in let hrr = { retry_version = `TLS_1_3 ; ciphersuite = cipher ; sessionid = ch.sessionid ; selected_group = group ; extensions = [ `Cookie cookie ] } in let hrr_raw = Writer.assemble_handshake (HelloRetryRequest hrr) in Tracing.hs ~tag:"handshake-out" (HelloRetryRequest hrr) ; (* there is no early data anymore if HRR was sent (see 4.1.2) *) (* but the client wouldn't know until it received the HRR *) let early_data_left = if List.mem `EarlyDataIndication ch.extensions then config.Config.zero_rtt else 0l in let machina = Server13 AwaitClientHelloHRR13 in Ok ({ state with early_data_left ; machina }, `Record (Packet.HANDSHAKE, hrr_raw) :: (match ch.sessionid with | None -> [] | Some _ -> [`Record change_cipher_spec])) end | Some group, Some cipher -> Log.debug (fun m -> m "cipher %a" Ciphersuite.pp_ciphersuite cipher) ; Log.debug (fun m -> m "group %a" pp_group group) ; if not (List.mem group groups) then Error (`Fatal (`Handshake (`Message "keyshare group not in group list"))) else (* we already checked above in keyshares that group is present there *) let keyshare = snd (List.find (fun (g, _) -> g = group) keyshares) in (* DHE - full handshake *) let* log = if hrr then let* c = Option.to_result ~none:(`Fatal (`Missing_extension "cookie")) (Utils.map_find ~f:(function `Cookie c -> Some c | _ -> None) ch.extensions) in (* log is: 254 00 00 length c :: HRR *) let hash_hdr = Writer.assemble_message_hash (String.length c) in let hrr = { retry_version = `TLS_1_3 ; ciphersuite = cipher ; sessionid = ch.sessionid ; selected_group = group ; extensions = [ `Cookie c ]} in let hs_buf = Writer.assemble_handshake (HelloRetryRequest hrr) in Ok (String.concat "" [ hash_hdr ; c ; hs_buf ]) else Ok "" in let hostname = hostname ch in let hlen = let module H = (val Digestif.module_of_hash' (Ciphersuite.hash13 cipher)) in H.digest_size in let early_secret, epoch, exts, can_use_early_data = let secret ?(psk = String.make hlen '\x00') () = Handshake_crypto13.(derive (empty cipher) psk) in let no_resume = secret (), None, [], false in match config.Config.ticket_cache, Utils.map_find ~f:(function `PreSharedKeys ids -> Some ids | _ -> None) ch.extensions, Utils.map_find ~f:(function `PskKeyExchangeModes ms -> Some ms | _ -> None) ch.extensions with | None, _, _ | _, None, _ -> no_resume | Some _, Some _, None -> no_resume (* should this lead to an error instead? *) | Some cache, Some ids, Some ms -> if not (List.mem Packet.PSK_KE_DHE ms) then no_resume else let idx_ids = List.mapi (fun i id -> (i, id)) ids in match List.filter (fun (_, ((id, _), _)) -> match cache.Config.lookup id with None -> false | Some _ -> true) idx_ids with | [] -> Log.info (fun m -> m "found no id in psk cache") ; no_resume | (idx, ((id, obf_age), binder))::_ -> (* need to verify binder, do the obf_age computations + checking, figure out whether the id is in our psk cache, and use the resumption secret as input and Ok the idx *) let psk, old_epoch = match cache.Config.lookup id with | None -> assert false (* see above *) | Some x -> x in match Ciphersuite.(any_ciphersuite_to_ciphersuite13 (ciphersuite_to_any_ciphersuite old_epoch.ciphersuite)) with | None -> no_resume | Some c' -> if c' = cipher && match hostname, old_epoch.own_name with | None, None -> true | Some x, Some y -> Domain_name.equal x y | _ -> false then let now = cache.Config.timestamp () in let server_delta_t = Ptime.diff now psk.issued_at in let client_delta_t = match Ptime.Span.of_float_s Int32.(to_float (sub obf_age psk.obfuscation) /. 1000.) with | None -> Logs.debug (fun m -> m "client_delta is not computable, using 0") ; Ptime.Span.zero | Some x -> x in (* ensure server&client_delta_t are not too far off! *) match Ptime.Span.(to_int_s (abs (sub server_delta_t client_delta_t))) with | None -> Logs.debug (fun m -> m "s_c_delta computation lead nowhere") ; no_resume | Some s_c_delta -> if s_c_delta > 10 then begin Logs.debug (fun m -> m "delta between client and server is %d seconds, ignoring this ticket!" s_c_delta); no_resume end else (* if ticket_creation ts + lifetime > now, continue *) let until = match Ptime.add_span psk.issued_at (Ptime.Span.of_int_s (Int32.to_int cache.Config.lifetime)) with | None -> Ptime.epoch | Some ts -> ts in if Ptime.is_earlier now ~than:until then let early_secret = secret ~psk:psk.secret () in let binder_key = Handshake_crypto13.derive_secret early_secret "res binder" "" in let binders_len = binders_len ids in let ch_part = String.(sub raw 0 (length raw - binders_len)) in let log = log ^ ch_part in let binder' = Handshake_crypto13.finished early_secret.hash binder_key log in if String.equal binder binder' then begin (* from 4.1.2 - earlydata is not allowed after hrr *) let zero = idx = 0 && not hrr && List.mem `EarlyDataIndication ch.extensions in early_secret, Some old_epoch, [ `PreSharedKey idx ], zero end else no_resume else no_resume else no_resume in let _, early_traffic_ctx = Handshake_crypto13.early_traffic early_secret raw in let secret, public = Handshake_crypto13.dh_gen_key group in let* es = Handshake_crypto13.dh_shared secret keyshare in let hs_secret = Handshake_crypto13.derive early_secret es in Tracing.cs ~tag:"hs secret" hs_secret.secret ; let sh, session = base_server_hello ?epoch cipher (`KeyShare (group, public) :: exts) in let sh_raw = Writer.assemble_handshake (ServerHello sh) in Tracing.hs ~tag:"handshake-out" (ServerHello sh) ; let log = log ^ raw ^ sh_raw in let server_hs_secret, server_ctx, client_hs_secret, client_ctx = hs_ctx hs_secret log in let* sigalgs = Option.to_result ~none:(`Fatal (`Missing_extension "signature algorithms")) (Utils.map_find ~f:(function `SignatureAlgorithms sa -> Some sa | _ -> None) ch.extensions) in (* TODO respect certificate_signature_algs if present *) let f = supports_key_usage ~not_present:true `Digital_signature in let* chain, priv = let* r = agreed_cert ~f ~signature_algorithms:sigalgs config.Config.own_certificates hostname in match r with | c::cs, priv -> Ok (c::cs, priv) | _ -> Error (`Fatal (`Handshake (`Message "couldn't find certificate chain"))) in let* alpn_protocol = alpn_protocol config ch in let session = let common_session_data13 = { session.common_session_data13 with own_name = hostname ; own_certificate = chain ; own_private_key = Some priv ; alpn_protocol } in { session with common_session_data13 } in let ee = let hostname_ext = Option.fold ~none:[] ~some:(fun _ -> [`Hostname]) hostname and alpn = Option.fold ~none:[] ~some:(fun proto -> [`ALPN proto]) alpn_protocol and early_data = if can_use_early_data && config.Config.zero_rtt <> 0l then [ `EarlyDataIndication ] else [] in EncryptedExtensions (hostname_ext @ alpn @ early_data) in (* TODO also max_fragment_length ; client_certificate_url ; trusted_ca_keys ; user_mapping ; client_authz ; server_authz ; cert_type ; use_srtp ; heartbeat ; alpn ; status_request_v2 ; signed_cert_timestamp ; client_cert_type ; server_cert_type *) let ee_raw = Writer.assemble_handshake ee in Tracing.hs ~tag:"handshake-out" ee ; let log = log ^ ee_raw in let* c_out, log, session' = if session.resumed then Ok ([], log, session) else let out, log, session = match config.Config.authenticator with | None -> [], log, session | Some _ -> let certreq = let exts = `SignatureAlgorithms config.Config.signature_algorithms :: (match config.Config.acceptable_cas with | [] -> [] | cas -> [ `CertificateAuthorities cas ]) in CertificateRequest (Writer.assemble_certificate_request_1_3 exts) in Tracing.hs ~tag:"handshake-out" certreq ; let raw_cert_req = Writer.assemble_handshake certreq in let common_session_data13 = { session.common_session_data13 with client_auth = true } in [raw_cert_req], log ^ raw_cert_req, { session with common_session_data13 } in let certs = List.map X509.Certificate.encode_der chain in let cert = Certificate (Writer.assemble_certificates_1_3 "" certs) in let cert_raw = Writer.assemble_handshake cert in Tracing.hs ~tag:"handshake-out" cert ; let log = log ^ cert_raw in let tbs = let module H = (val Digestif.module_of_hash' (Ciphersuite.hash13 cipher)) in H.(to_raw_string (digest_string log)) in let* signed = signature `TLS_1_3 ~context_string:"TLS 1.3, server CertificateVerify" tbs (Some sigalgs) config.Config.signature_algorithms priv in let cv = CertificateVerify signed in let cv_raw = Writer.assemble_handshake cv in Tracing.hs ~tag:"handshake-out" cv ; let log = log ^ cv_raw in Ok (out @ [cert_raw; cv_raw], log, session) in let master_secret = Handshake_crypto13.derive hs_secret (String.make hlen '\x00') in Tracing.cs ~tag:"master-secret" master_secret.secret ; let f_data = finished hs_secret.hash server_hs_secret log in let fin = Finished f_data in let fin_raw = Writer.assemble_handshake fin in Tracing.hs ~tag:"handshake-out" fin ; let log = log ^ fin_raw in let server_app_secret, server_app_ctx, client_app_secret, client_app_ctx = app_ctx master_secret log in let exporter_master_secret = Handshake_crypto13.exporter master_secret log in let session' = { session' with server_app_secret ; client_app_secret ; exporter_master_secret } in let* () = guard (String.length state.hs_fragment = 0) (`Fatal (`Handshake `Fragments)) in (* send sessionticket early *) (* TODO track the nonce across handshakes / newsessionticket messages (i.e. after post-handshake auth) - needs to be unique! *) let st, st_raw = match session.resumed, config.Config.ticket_cache with | true, _ | _, None -> None, [] | false, Some cache -> let age_add = let cs = Mirage_crypto_rng.generate 4 in String.get_int32_be cs 0 in let psk_id = Mirage_crypto_rng.generate 32 in let nonce = Mirage_crypto_rng.generate 4 in let extensions = match config.Config.zero_rtt with | 0l -> [] | x -> [ `EarlyDataIndication x ] in let st = { lifetime = cache.Config.lifetime ; age_add ; nonce ; ticket = psk_id ; extensions } in Tracing.hs ~tag:"handshake-out" (SessionTicket st) ; let st_raw = Writer.assemble_handshake (SessionTicket st) in (Some st, [st_raw]) in let session = let common_session_data13 = { session'.common_session_data13 with master_secret = master_secret.secret } in { session' with common_session_data13 ; master_secret } in let st, session = if can_use_early_data then (AwaitEndOfEarlyData13 (client_hs_secret, client_ctx, client_app_ctx, st, log), `TLS13 { session with state = `ZeroRTT } :: state.session) else if session.common_session_data13.client_auth then (AwaitClientCertificate13 (session, client_hs_secret, client_app_ctx, st, log), state.session) else (AwaitClientFinished13 (client_hs_secret, client_app_ctx, st, log), `TLS13 session :: state.session) in let early_data_left = if List.mem `EarlyDataIndication ch.extensions then config.Config.zero_rtt else 0l in Ok ({ state with machina = Server13 st ; session ; early_data_left }, `Record (Packet.HANDSHAKE, sh_raw) :: (match ch.sessionid with | Some _ when not hrr -> [`Record change_cipher_spec] | _ -> []) @ [ `Change_enc server_ctx ; `Change_dec (if can_use_early_data then early_traffic_ctx else client_ctx) ; `Record (Packet.HANDSHAKE, ee_raw) ] @ List.map (fun data -> `Record (Packet.HANDSHAKE, data)) c_out @ [ `Record (Packet.HANDSHAKE, fin_raw) ; `Change_enc server_app_ctx ] @ List.map (fun data -> `Record (Packet.HANDSHAKE, data)) st_raw) let answer_client_certificate state cert (sd : session_data13) client_fini dec_ctx st raw log = let* c = map_reader_error (Reader.parse_certificates_1_3 cert) in match c, state.config.Config.authenticator with | (_, []), None -> Error (`Fatal (`Handshake (`Message "couldn't find authenticator"))) | (_ctx, []), Some auth -> begin match auth ~host:None [] with | Ok anchor -> let trust_anchor = match anchor with | None -> None | Some (_chain, ta) -> Some ta in let common_session_data13 = { sd.common_session_data13 with trust_anchor } in let sd = { sd with common_session_data13 } in let st = AwaitClientFinished13 (client_fini, dec_ctx, st, log ^ raw) in Ok ({ state with machina = Server13 st ; session = `TLS13 sd :: state.session }, []) | Error e -> Error (`Error (`AuthenticationFailure e)) end | (_ctx, cert_exts), auth -> (* TODO what to do with ctx? send through authenticator? *) (* TODO what to do with extensions? *) let certs = List.map fst cert_exts in let* peer_certificate, received_certificates, peer_certificate_chain, trust_anchor = validate_chain auth certs state.config.Config.ip None in let sd' = let common_session_data13 = { sd.common_session_data13 with received_certificates ; peer_certificate ; peer_certificate_chain ; trust_anchor } in { sd with common_session_data13 } in let st = AwaitClientCertificateVerify13 (sd', client_fini, dec_ctx, st, log ^ raw) in Ok ({ state with machina = Server13 st }, []) let answer_client_certificate_verify state cv (sd : session_data13) client_fini dec_ctx st raw log = let tbs = let module H = (val Digestif.module_of_hash' (Ciphersuite.hash13 sd.ciphersuite13)) in H.(to_raw_string (digest_string log)) in let* () = verify_digitally_signed `TLS_1_3 ~context_string:"TLS 1.3, client CertificateVerify" state.config.Config.signature_algorithms cv tbs sd.common_session_data13.peer_certificate in let st = AwaitClientFinished13 (client_fini, dec_ctx, st, log ^ raw) in Ok ({ state with machina = Server13 st ; session = `TLS13 sd :: state.session }, []) let answer_client_finished state fin client_fini dec_ctx st raw log = match state.session with | `TLS13 session :: rest -> let hash = Ciphersuite.hash13 session.ciphersuite13 in let data = finished hash client_fini log in let* () = guard (String.equal data fin) (`Fatal (`Handshake (`Message "couldn't verify finished"))) in let* () = guard (String.length state.hs_fragment = 0) (`Fatal (`Handshake `Fragments)) in let session' = match st, state.config.Config.ticket_cache with | None, _ | _, None -> session | Some st, Some cache -> let resumption_secret = Handshake_crypto13.resumption session.master_secret (log ^ raw) in let session = { session with resumption_secret } in let secret = Handshake_crypto13.res_secret hash resumption_secret st.nonce in let issued_at = cache.Config.timestamp () in let psk = { identifier = st.ticket ; obfuscation = st.age_add ; secret ; lifetime = st.lifetime ; early_data = state.config.Config.zero_rtt ; issued_at } in let epoch = epoch_of_session true None `TLS_1_3 (`TLS13 session) in cache.Config.ticket_granted psk epoch ; session in let state' = { state with machina = Server13 Established13 ; session = `TLS13 session' :: rest } in Ok (state', [ `Change_dec dec_ctx ]) | _ -> Error (`Fatal (`Handshake (`Message "no session found in finished"))) let handle_end_of_early_data state cf hs_ctx cc st buf log = let machina = AwaitClientFinished13 (cf, cc, st, log ^ buf) in match state.session with | `TLS13 s1 :: _ -> let session = `TLS13 { s1 with state = `Established } :: state.session in Ok ({ state with machina = Server13 machina ; session }, [ `Change_dec hs_ctx ]) | _ -> Error (`Fatal (`Handshake (`Message "no session handling end of early data"))) let handle_key_update state req = match state.session with | `TLS13 session :: _ -> let* () = guard (String.length state.hs_fragment = 0) (`Fatal (`Handshake `Fragments)) in let client_app_secret, client_ctx = app_secret_n_1 session.master_secret session.client_app_secret in let session' = { session with client_app_secret } in let session', out = match req with | Packet.UPDATE_NOT_REQUESTED -> session', [] | Packet.UPDATE_REQUESTED -> let server_app_secret, server_ctx = app_secret_n_1 session.master_secret session.server_app_secret in let ku = KeyUpdate Packet.UPDATE_NOT_REQUESTED in Tracing.hs ~tag:"handshake-out" ku ; let ku_raw = Writer.assemble_handshake ku in { session' with server_app_secret }, [ `Record (Packet.HANDSHAKE, ku_raw); `Change_enc server_ctx ] in let session = `TLS13 session' :: state.session in let state' = { state with machina = Server13 Established13 ; session } in Ok (state', `Change_dec client_ctx :: out) | _ -> Error (`Fatal (`Handshake (`Message "no session while handling key update"))) 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 | AwaitClientHelloHRR13, ClientHello ch -> answer_client_hello ~hrr:true hs ch buf | AwaitClientCertificate13 (sd, cf, cc, st, log), Certificate cert -> answer_client_certificate hs cert sd cf cc st buf log | AwaitClientCertificateVerify13 (sd, cf, cc, st, log), CertificateVerify cv -> answer_client_certificate_verify hs cv sd cf cc st buf log | AwaitClientFinished13 (cf, cc, st, log), Finished x -> answer_client_finished hs x cf cc st buf log | AwaitEndOfEarlyData13 (cf, hs_c, cc, st, log), EndOfEarlyData -> handle_end_of_early_data hs cf hs_c cc st buf log | Established13, KeyUpdate req -> handle_key_update hs req | _, hs -> Error (`Fatal (`Unexpected (`Handshake hs)))