diff options
Diffstat (limited to 'lib/ssl')
68 files changed, 4935 insertions, 1217 deletions
diff --git a/lib/ssl/doc/src/ssl.xml b/lib/ssl/doc/src/ssl.xml index 05590666da..0ad2d8da0b 100644 --- a/lib/ssl/doc/src/ssl.xml +++ b/lib/ssl/doc/src/ssl.xml @@ -655,16 +655,18 @@ fun(srp, Username :: string(), UserState :: term()) -> <datatype> <name name="log_alert"/> - <desc><p>If set to <c>false</c>, error reports are not displayed. + <desc><p>If set to <c>false</c>, TLS/DTLS Alert reports are not displayed. Deprecated in OTP 22, use {log_level, <seealso marker="#type-logging_level">logging_level()</seealso>} instead.</p> </desc> </datatype> <datatype> <name name="logging_level"/> - <desc><p>Specifies the log level for TLS/DTLS. At verbosity level <c>notice</c> and above error reports are - displayed in TLS/DTLS. The level <c>debug</c> triggers verbose logging of TLS/DTLS protocol - messages.</p> + <desc><p>Specifies the log level for a TLS/DTLS + connection. Alerts are logged on <c>notice</c> level, which is + the default level. The level <c>debug</c> triggers verbose + logging of TLS/DTLS protocol messages. See also <seealso marker="ssl_app">ssl(6)</seealso> + </p> </desc> </datatype> diff --git a/lib/ssl/doc/src/ssl_app.xml b/lib/ssl/doc/src/ssl_app.xml index b05caf44ea..129fc57862 100644 --- a/lib/ssl/doc/src/ssl_app.xml +++ b/lib/ssl/doc/src/ssl_app.xml @@ -67,14 +67,14 @@ <tag><c>protocol_version = </c><seealso marker="ssl#type-protocol">ssl:ssl_tls_protocol()</seealso><c><![CDATA[<optional>]]></c></tag> <item><p>Protocol supported by started clients and servers. If this option is not set, it defaults to all - TLS protocols currently supported by the SSL application. + TLS protocols currently supported, more might be configurable, by the SSL application. This option can be overridden by the version option to <c>ssl:connect/[2,3]</c> and <c>ssl:listen/2</c>.</p></item> <tag><c>dtls_protocol_version = </c><seealso marker="ssl#type-protocol">ssl:dtls_protocol()</seealso><c><![CDATA[<optional>]]></c></tag> <item><p>Protocol supported by started clients and servers. If this option is not set, it defaults to all - DTLS protocols currently supported by the SSL application. + DTLS protocols currently supported, more might be configurable, by the SSL application. This option can be overridden by the version option to <c>ssl:connect/[2,3]</c> and <c>ssl:listen/2</c>.</p></item> @@ -155,10 +155,17 @@ <section> <title>ERROR LOGGER AND EVENT HANDLERS</title> - <p>The SSL application uses the default <seealso - marker="kernel:error_logger">OTP error logger</seealso> to log - unexpected errors and TLS/DTLS alerts. The logging of TLS/DTLS alerts may be - turned off with the <c>log_alert</c> option. </p> + + <p>The SSL application uses <seealso + marker="kernel:logger">OTP logger</seealso>. + TLS/DTLS alerts are logged on notice level. Unexpected + errors are logged on error level. These log entries + will by default end up in the default Erlang log. + The option <c>log_level</c> may be used to in run-time to set + the log level of a specific TLS connection, which is + handy when you want to use level debug to inspect the + TLS handshake setup. + </p> </section> <section> diff --git a/lib/ssl/doc/src/ssl_crl_cache_api.xml b/lib/ssl/doc/src/ssl_crl_cache_api.xml index 4cba4e1de1..9922cfb0e9 100644 --- a/lib/ssl/doc/src/ssl_crl_cache_api.xml +++ b/lib/ssl/doc/src/ssl_crl_cache_api.xml @@ -56,19 +56,29 @@ </desc> </datatype> - <datatype> <name name="dist_point"/> <desc> <p>For description see <seealso marker="public_key:public_key_records"> X509 certificates records</seealso></p> </desc> - </datatype> + </datatype> + + <datatype> + <name name="logger_info"/> + <desc> + <p>Information for ssl applications use of <seealso + marker="kernel:logger"> Logger(3)</seealso></p> + </desc> + </datatype> + + </datatypes> <funcs> - <func> - <name since="OTP 18.0">fresh_crl(DistributionPoint, CRL) -> FreshCRL</name> + <func> + <name since="@maint@">fresh_crl(DistributionPoint, CRL) -> FreshCRL </name> + <name since="OTP 18.0">fresh_crl(DistributionPoint, CRL) -> FreshCRL | {LoggerInfo, FreshCRL}</name> <fsummary> <c>fun fresh_crl/2 </c> will be used as input option <c>update_crl</c> to public_key:pkix_crls_validate/3 </fsummary> <type> @@ -77,14 +87,21 @@ marker="public_key:public_key#type-der_encoded">public_key:der_encoded()</seealso>] </v> <v> FreshCRL = [<seealso marker="public_key:public_key#type-der_encoded">public_key:der_encoded()</seealso>] </v> + <v> LoggerInfo = {logger, <seealso marker="#type-logger_info"> logger_info() </seealso>}} </v> </type> <desc> <p> <c>fun fresh_crl/2 </c> will be used as input option <c>update_crl</c> to <seealso marker="public_key:public_key#pkix_crls_validate-3">public_key:pkix_crls_validate/3 </seealso> </p> + + <p>It is possible to return logger info that will be used by the TLS connection + to produce log events. + </p> </desc> </func> <func> + <name since="@maint@">lookup(DistributionPoint, Issuer, DbHandle) -> not_available | CRLs | + {LoggerInfo, CRLs} </name> <name since="OTP 19.0">lookup(DistributionPoint, Issuer, DbHandle) -> not_available | CRLs </name> <name since="OTP 18.0">lookup(DistributionPoint, DbHandle) -> not_available | CRLs </name> <fsummary> </fsummary> @@ -94,7 +111,8 @@ marker="public_key:public_key#type-issuer_name">public_key:issuer_name()</seealso> </v> <v> DbHandle = <seealso marker="#type-crl_cache_ref"> crl_cache_ref() </seealso></v> <v> CRLs = [<seealso - marker="public_key:public_key#type-der_encoded">public_key:der_encoded()</seealso>] </v> + marker="public_key:public_key#type-der_encoded">public_key:der_encoded()</seealso>]</v> + <v> LoggerInfo = {logger, <seealso marker="#type-logger_info"> logger_info() </seealso>}} </v> </type> <desc> <p>Lookup the CRLs belonging to the distribution point <c> Distributionpoint</c>. This function may choose to only look in the cache or to follow distribution point @@ -111,19 +129,35 @@ compatibility, this is still supported: if there is no <c>lookup/3</c> function in the callback module, <c>lookup/2</c> is called instead.</p> + + <p>It is possible to return logger info that will be used by the TLS connection + to produce log events. + </p> </desc> </func> <func> + <name since="@maint@">select(Issuer, DbHandle) -> CRLs | {LoggerInfo, CRLs} </name> <name since="OTP 18.0">select(Issuer, DbHandle) -> CRLs </name> <fsummary>Select the CRLs in the cache that are issued by <c>Issuer</c></fsummary> <type> <v> Issuer = <seealso - marker="public_key:public_key#type-issuer_name">public_key:issuer_name()</seealso></v> + marker="public_key:public_key#type-issuer_name">public_key:issuer_name()</seealso> | list() </v> <v> DbHandle = <seealso marker="#type-crl_cache_ref"> cache_ref() </seealso></v> - </type> + <v> LoggerInfo = {logger, <seealso marker="#type-logger_info"> logger_info() </seealso>} </v> + </type> <desc> - <p>Select the CRLs in the cache that are issued by <c>Issuer</c> </p> + <p>Select the CRLs in the cache that are issued by <c>Issuer</c> unless + the value is a list of so called general names, see <seealso + marker="public_key:public_key_records"> X509 certificates records</seealso>, + originating form <c>#'DistributionPoint'.cRLissuer</c> and + representing different mechanism to obtain the CRLs. The cache + callback needs to use the appropriate entry to retrive the CRLs or + return an empty list if it does not exist. + </p> + + <p>It is possible to return logger info that will be used by the TLS connection + to produce log events.</p> </desc> </func> </funcs> diff --git a/lib/ssl/src/Makefile b/lib/ssl/src/Makefile index 8dc76f2638..e961f05b37 100644 --- a/lib/ssl/src/Makefile +++ b/lib/ssl/src/Makefile @@ -50,6 +50,7 @@ MODULES= \ dtls_listener_sup \ dtls_packet_demux \ dtls_record \ + dtls_sup \ dtls_socket \ dtls_v1 \ inet_tls_dist \ @@ -83,6 +84,7 @@ MODULES= \ ssl_srp_primes \ ssl_sup \ ssl_v3 \ + tls_bloom_filter \ tls_connection \ tls_connection_sup \ tls_connection_1_3 \ @@ -90,8 +92,13 @@ MODULES= \ tls_handshake_1_3 \ tls_record \ tls_record_1_3 \ + tls_client_ticket_store \ tls_sender \ + tls_server_session_ticket\ + tls_server_session_ticket_sup\ + tls_server_sup\ tls_socket \ + tls_sup \ tls_v1 diff --git a/lib/ssl/src/dtls_connection.erl b/lib/ssl/src/dtls_connection.erl index f8fd42e36d..a658fe0306 100644 --- a/lib/ssl/src/dtls_connection.erl +++ b/lib/ssl/src/dtls_connection.erl @@ -441,19 +441,19 @@ init({call, From}, {start, Timeout}, handshake_env = #handshake_env{renegotiation = {Renegotiation, _}}, connection_env = CEnv, ssl_options = #{versions := Versions} = SslOpts, - session = #session{own_certificate = Cert} = Session0, + session = #session{own_certificate = Cert} = NewSession, connection_states = ConnectionStates0 } = State0) -> + Session = ssl_session:client_select_session({Host, Port, SslOpts}, Cache, CacheCb, NewSession), Hello = dtls_handshake:client_hello(Host, Port, ConnectionStates0, SslOpts, - Cache, CacheCb, Renegotiation, Cert), + Session#session.session_id, Renegotiation, Cert), Version = Hello#client_hello.client_version, HelloVersion = dtls_record:hello_version(Version, Versions), State1 = prepare_flight(State0#state{connection_env = CEnv#connection_env{negotiated_version = Version}}), {State2, Actions} = send_handshake(Hello, State1#state{connection_env = CEnv#connection_env{negotiated_version = HelloVersion}}), State = State2#state{connection_env = CEnv#connection_env{negotiated_version = Version}, %% RequestedVersion - session = - Session0#session{session_id = Hello#client_hello.session_id}, + session = Session, start_or_recv_from = From}, next_event(hello, no_record, State, [{{timeout, handshake}, Timeout, close} | Actions]); init({call, _} = Type, Event, #state{static_env = #static_env{role = server}, @@ -521,30 +521,24 @@ hello(internal, #client_hello{cookie = <<>>, Actions); hello(internal, #hello_verify_request{cookie = Cookie}, #state{static_env = #static_env{role = client, host = Host, - port = Port, - session_cache = Cache, - session_cache_cb = CacheCb}, + port = Port}, handshake_env = #handshake_env{renegotiation = {Renegotiation, _}} = HsEnv, connection_env = CEnv, ssl_options = SslOpts, - session = #session{own_certificate = OwnCert} - = Session0, + session = #session{own_certificate = Cert, session_id = Id}, connection_states = ConnectionStates0 } = State0) -> Hello = dtls_handshake:client_hello(Host, Port, Cookie, ConnectionStates0, - SslOpts, - Cache, CacheCb, Renegotiation, OwnCert), + SslOpts, Id, Renegotiation, Cert), Version = Hello#client_hello.client_version, State1 = prepare_flight(State0#state{handshake_env = HsEnv#handshake_env{tls_handshake_history = ssl_handshake:init_handshake_history()}}), {State2, Actions} = send_handshake(Hello, State1), - State = State2#state{connection_env = CEnv#connection_env{negotiated_version = Version}, %% Requested version - session = - Session0#session{session_id = - Hello#client_hello.session_id}}, + State = State2#state{connection_env = CEnv#connection_env{negotiated_version = Version} % RequestedVersion + }, next_event(?FUNCTION_NAME, no_record, State, Actions); hello(internal, #client_hello{extensions = Extensions} = Hello, #state{ssl_options = #{handshake := hello}, @@ -702,7 +696,7 @@ connection(internal, #hello_request{}, #state{static_env = #static_env{host = Ho session_cache = Cache, session_cache_cb = CacheCb }, - handshake_env = #handshake_env{ renegotiation = {Renegotiation, _}}, + handshake_env = #handshake_env{renegotiation = {Renegotiation, _}}, connection_env = CEnv, session = #session{own_certificate = Cert} = Session0, ssl_options = #{versions := Versions} = SslOpts, @@ -710,15 +704,15 @@ connection(internal, #hello_request{}, #state{static_env = #static_env{host = Ho protocol_specific = PS } = State0) -> + Session = ssl_session:client_select_session({Host, Port, SslOpts}, Cache, CacheCb, Session0), Hello = dtls_handshake:client_hello(Host, Port, ConnectionStates0, SslOpts, - Cache, CacheCb, Renegotiation, Cert), + Session#session.session_id, Renegotiation, Cert), Version = Hello#client_hello.client_version, HelloVersion = dtls_record:hello_version(Version, Versions), State1 = prepare_flight(State0), {State2, Actions} = send_handshake(Hello, State1#state{connection_env = CEnv#connection_env{negotiated_version = HelloVersion}}), State = State2#state{protocol_specific = PS#{flight_state => initial_flight_state(DataTag)}, - session = Session0#session{session_id - = Hello#client_hello.session_id}}, + session = Session}, next_event(hello, no_record, State, Actions); connection(internal, #client_hello{} = Hello, #state{static_env = #static_env{role = server}, handshake_env = #handshake_env{allow_renegotiate = true} = HsEnv} = State) -> @@ -1209,12 +1203,13 @@ is_ignore_alert(#alert{description = ?ILLEGAL_PARAMETER}) -> is_ignore_alert(_) -> false. -log_ignore_alert(debug, StateName, Alert, Role) -> - Txt = ssl_alert:alert_txt(Alert), - ?LOG_ERROR("DTLS over UDP ~p: In state ~p ignored to send ALERT ~s as DoS-attack mitigation \n", - [Role, StateName, Txt]); -log_ignore_alert(_, _, _, _) -> - ok. +log_ignore_alert(Level, StateName, #alert{where = Location} = Alert, Role) -> + ssl_logger:log(info, + Level, #{alert => Alert, + alerter => ignored, + statename => StateName, + role => Role, + protocol => protocol_name()}, Location). send_application_data(Data, From, _StateName, #state{static_env = #static_env{socket = Socket, diff --git a/lib/ssl/src/dtls_handshake.erl b/lib/ssl/src/dtls_handshake.erl index b2a5fcfa66..cad31b23ee 100644 --- a/lib/ssl/src/dtls_handshake.erl +++ b/lib/ssl/src/dtls_handshake.erl @@ -30,7 +30,7 @@ -include("ssl_alert.hrl"). %% Handshake handling --export([client_hello/8, client_hello/9, cookie/4, hello/4, +-export([client_hello/7, client_hello/8, cookie/4, hello/4, hello_verify_request/2]). %% Handshake encoding @@ -47,29 +47,29 @@ %%==================================================================== %%-------------------------------------------------------------------- -spec client_hello(ssl:host(), inet:port_number(), ssl_record:connection_states(), - ssl_options(), integer(), atom(), boolean(), der_cert()) -> + ssl_options(), binary(), boolean(), der_cert()) -> #client_hello{}. %% %% Description: Creates a client hello message. %%-------------------------------------------------------------------- client_hello(Host, Port, ConnectionStates, SslOpts, - Cache, CacheCb, Renegotiation, OwnCert) -> + Id, Renegotiation, OwnCert) -> %% First client hello (two sent in DTLS ) uses empty Cookie client_hello(Host, Port, <<>>, ConnectionStates, SslOpts, - Cache, CacheCb, Renegotiation, OwnCert). + Id, Renegotiation, OwnCert). %%-------------------------------------------------------------------- -spec client_hello(ssl:host(), inet:port_number(), term(), ssl_record:connection_states(), - ssl_options(), integer(), atom(), boolean(), der_cert()) -> + ssl_options(), binary(),boolean(), der_cert()) -> #client_hello{}. %% %% Description: Creates a client hello message. %%-------------------------------------------------------------------- -client_hello(Host, Port, Cookie, ConnectionStates, +client_hello(_Host, _Port, Cookie, ConnectionStates, #{versions := Versions, ciphers := UserSuites, fallback := Fallback} = SslOpts, - Cache, CacheCb, Renegotiation, OwnCert) -> + Id, Renegotiation, _OwnCert) -> Version = dtls_record:highest_protocol_version(Versions), Pending = ssl_record:pending_connection_state(ConnectionStates, read), SecParams = maps:get(security_parameters, Pending), @@ -78,8 +78,8 @@ client_hello(Host, Port, Cookie, ConnectionStates, Extensions = ssl_handshake:client_hello_extensions(TLSVersion, CipherSuites, SslOpts, ConnectionStates, - Renegotiation, undefined), - Id = ssl_session:client_id({Host, Port, SslOpts}, Cache, CacheCb, OwnCert), + Renegotiation, undefined, + undefined), #client_hello{session_id = Id, client_version = Version, diff --git a/lib/ssl/src/dtls_listener_sup.erl b/lib/ssl/src/dtls_listener_sup.erl index dc30696a2c..2c43c215be 100644 --- a/lib/ssl/src/dtls_listener_sup.erl +++ b/lib/ssl/src/dtls_listener_sup.erl @@ -29,7 +29,9 @@ %% API -export([start_link/0]). --export([start_child/1]). +-export([start_child/1, + lookup_listner/1, + register_listner/2]). %% Supervisor callback -export([init/1]). @@ -43,10 +45,37 @@ start_link() -> start_child(Args) -> supervisor:start_child(?MODULE, Args). +lookup_listner(0) -> + undefined; +lookup_listner(Port) -> + try ets:lookup(dtls_listener_sup, Port) of + [{Port, {Owner, Handler}}] -> + case erlang:is_process_alive(Handler) of + true -> + case erlang:is_process_alive(Owner) of + true -> + {error, already_listening}; + false -> + {ok, Handler} + end; + false -> + ets:delete(dtls_listener_sup, Port), + undefined + end; + [] -> + undefined + catch _:_ -> + undefined + end. + +register_listner(OwnerAndListner, Port) -> + ets:insert(dtls_listener_sup, {Port, OwnerAndListner}). + %%%========================================================================= %%% Supervisor callback %%%========================================================================= init(_O) -> + ets:new(dtls_listener_sup, [named_table, public]), RestartStrategy = simple_one_for_one, MaxR = 0, MaxT = 3600, diff --git a/lib/ssl/src/dtls_packet_demux.erl b/lib/ssl/src/dtls_packet_demux.erl index 94b350eaa5..d3cbc364bf 100644 --- a/lib/ssl/src/dtls_packet_demux.erl +++ b/lib/ssl/src/dtls_packet_demux.erl @@ -28,7 +28,7 @@ %% API -export([start_link/5, active_once/3, accept/2, sockname/1, close/1, - get_all_opts/1, get_sock_opts/2, set_sock_opts/2]). + get_all_opts/1, set_all_opts/2, get_sock_opts/2, set_sock_opts/2]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -72,6 +72,8 @@ get_all_opts(PacketSocket) -> call(PacketSocket, get_all_opts). set_sock_opts(PacketSocket, Opts) -> call(PacketSocket, {set_sock_opts, Opts}). +set_all_opts(PacketSocket, Opts) -> + call(PacketSocket, {set_all_opts, Opts}). %%%=================================================================== %%% gen_server callbacks @@ -141,7 +143,10 @@ handle_call(get_all_opts, _, #state{dtls_options = DTLSOptions, handle_call({set_sock_opts, {SocketOpts, NewEmOpts}}, _, #state{listener = Socket, emulated_options = EmOpts0} = State) -> set_socket_opts(Socket, SocketOpts), EmOpts = do_set_emulated_opts(NewEmOpts, EmOpts0), - {reply, ok, State#state{emulated_options = EmOpts}}. + {reply, ok, State#state{emulated_options = EmOpts}}; +handle_call({set_all_opts, {SocketOpts, NewEmOpts, SslOpts}}, _, #state{listener = Socket} = State) -> + set_socket_opts(Socket, SocketOpts), + {reply, ok, State#state{emulated_options = NewEmOpts, dtls_options = SslOpts}}. handle_cast({active_once, Client, Pid}, State0) -> State = handle_active_once(Client, Pid, State0), diff --git a/lib/ssl/src/dtls_socket.erl b/lib/ssl/src/dtls_socket.erl index b305d08f70..f7b1968064 100644 --- a/lib/ssl/src/dtls_socket.erl +++ b/lib/ssl/src/dtls_socket.erl @@ -31,23 +31,34 @@ send(Transport, {{IP,Port},Socket}, Data) -> listen(Port, #config{transport_info = TransportInfo, ssl = SslOpts, - emulated = EmOpts, + emulated = EmOpts0, inet_user = Options} = Config) -> - - case dtls_listener_sup:start_child([Port, TransportInfo, emulated_socket_options(EmOpts, #socket_options{}), - Options ++ internal_inet_values(), SslOpts]) of - {ok, Pid} -> - Socket = #sslsocket{pid = {dtls, Config#config{dtls_handler = {Pid, Port}}}}, - check_active_n(EmOpts, Socket), + Result = case dtls_listener_sup:lookup_listner(Port) of + undefined -> + Result0 = {ok, Listner0} = dtls_listener_sup:start_child([Port, TransportInfo, emulated_socket_options(EmOpts0, #socket_options{}), + Options ++ internal_inet_values(), SslOpts]), + dtls_listener_sup:register_listner({self(), Listner0}, Port), + Result0; + {ok, Listner0} = Result0 -> + dtls_packet_demux:set_all_opts(Listner0, {Options, emulated_socket_options(EmOpts0, #socket_options{}), SslOpts}), + dtls_listener_sup:register_listner({self(), Listner0}, Port), + Result0; + Result0 -> + Result0 + end, + case Result of + {ok, Listner} -> + Socket = #sslsocket{pid = {dtls, Config#config{dtls_handler = {Listner, Port}}}}, + check_active_n(EmOpts0, Socket), {ok, Socket}; - Err = {error, _} -> - Err + Err -> + Err end. accept(dtls, #config{transport_info = {Transport,_,_,_,_}, - connection_cb = ConnectionCb, - dtls_handler = {Listner, _}}, _Timeout) -> + connection_cb = ConnectionCb, + dtls_handler = {Listner, _}}, _Timeout) -> case dtls_packet_demux:accept(Listner, self()) of {ok, Pid, Socket} -> {ok, socket([Pid], Transport, {Listner, Socket}, ConnectionCb)}; diff --git a/lib/ssl/src/dtls_sup.erl b/lib/ssl/src/dtls_sup.erl new file mode 100644 index 0000000000..2e72c10ba0 --- /dev/null +++ b/lib/ssl/src/dtls_sup.erl @@ -0,0 +1,76 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2019-2019. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +%% + +-module(dtls_sup). + +-behaviour(supervisor). + +%% API +-export([start_link/0]). + +%% Supervisor callback +-export([init/1]). + +%%%========================================================================= +%%% API +%%%========================================================================= + +-spec start_link() -> {ok, pid()} | ignore | {error, term()}. + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%%%========================================================================= +%%% Supervisor callback +%%%========================================================================= + +init([]) -> + DTLSConnetionManager = dtls_connection_manager_child_spec(), + DTLSListeners = dtls_listeners_spec(), + + {ok, {{one_for_one, 10, 3600}, [DTLSConnetionManager, + DTLSListeners + ]}}. + + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- + +dtls_connection_manager_child_spec() -> + Name = dtls_connection, + StartFunc = {dtls_connection_sup, start_link, []}, + Restart = permanent, + + Shutdown = 4000, + Modules = [dtls_connection_sup], + Type = supervisor, + {Name, StartFunc, Restart, Shutdown, Type, Modules}. + +dtls_listeners_spec() -> + Name = dtls_listener, + StartFunc = {dtls_listener_sup, start_link, []}, + Restart = permanent, + Shutdown = 4000, + Modules = [], + Type = supervisor, + {Name, StartFunc, Restart, Shutdown, Type, Modules}. diff --git a/lib/ssl/src/ssl.app.src b/lib/ssl/src/ssl.app.src index e7a4d73ec4..c45e6bcf9a 100644 --- a/lib/ssl/src/ssl.app.src +++ b/lib/ssl/src/ssl.app.src @@ -14,6 +14,10 @@ ssl_v3, tls_connection_sup, tls_sender, + tls_server_sup, + tls_server_session_ticket_sup, + tls_server_session_ticket, + tls_sup, ssl_dh_groups, %% DTLS dtls_connection, @@ -24,6 +28,7 @@ dtls_connection_sup, dtls_packet_demux, dtls_listener_sup, + dtls_sup, %% API ssl, %% Main API ssl_session_cache_api, @@ -37,6 +42,8 @@ ssl_srp_primes, ssl_alert, ssl_listen_tracker_sup, %% may be used by DTLS over SCTP + tls_bloom_filter, + tls_client_ticket_store, %% Erlang Distribution over SSL/TLS inet_tls_dist, inet6_tls_dist, diff --git a/lib/ssl/src/ssl.erl b/lib/ssl/src/ssl.erl index 0d6501b5ee..aae79d7625 100644 --- a/lib/ssl/src/ssl.erl +++ b/lib/ssl/src/ssl.erl @@ -92,13 +92,12 @@ connection_information/1, connection_information/2]). %% Misc --export([handle_options/2, +-export([handle_options/2, + handle_options/3, tls_version/1, - new_ssl_options/3, suite_to_str/1, suite_to_openssl_str/1, - str_to_suite/1, - options_to_map/1]). + str_to_suite/1]). -deprecated({ssl_accept, 1, eventually}). -deprecated({ssl_accept, 2, eventually}). @@ -439,7 +438,6 @@ elliptic_curves => [public_key:oid()], sni => hostname()}. % exported %% ------------------------------------------------------------------------------------------------------- --define(SSL_OPTIONS, record_info(fields, ssl_options)). %%%-------------------------------------------------------------------- %%% API @@ -501,7 +499,8 @@ connect(Socket, SslOptions) when is_port(Socket) -> connect(Socket, SslOptions0, Timeout) when is_port(Socket), (is_integer(Timeout) andalso Timeout >= 0) or (Timeout == infinity) -> - CbInfo = handle_option(cb_info, SslOptions0, default_cb_info(tls)), + CbInfo = handle_option_cb_info(SslOptions0, tls), + Transport = element(1, CbInfo), EmulatedOptions = tls_socket:emulated_options(), {ok, SocketValues} = tls_socket:getopts(Transport, Socket, EmulatedOptions), @@ -687,9 +686,10 @@ handshake(ListenSocket, SslOptions) when is_port(ListenSocket) -> handshake(#sslsocket{} = Socket, [], Timeout) when (is_integer(Timeout) andalso Timeout >= 0) or (Timeout == infinity)-> handshake(Socket, Timeout); -handshake(#sslsocket{fd = {_, _, _, Tracker}} = Socket, SslOpts, Timeout) when +handshake(#sslsocket{fd = {_, _, _, Trackers}} = Socket, SslOpts, Timeout) when (is_integer(Timeout) andalso Timeout >= 0) or (Timeout == infinity)-> try + Tracker = proplists:get_value(option_tracker, Trackers), {ok, EmOpts, _} = tls_socket:get_all_opts(Tracker), ssl_connection:handshake(Socket, {SslOpts, tls_socket:emulated_socket_options(EmOpts, #socket_options{})}, Timeout) @@ -707,7 +707,8 @@ handshake(#sslsocket{pid = [Pid|_], fd = {_, _, _}} = Socket, SslOpts, Timeout) end; handshake(Socket, SslOptions, Timeout) when is_port(Socket), (is_integer(Timeout) andalso Timeout >= 0) or (Timeout == infinity) -> - CbInfo = handle_option(cb_info, SslOptions, default_cb_info(tls)), + CbInfo = handle_option_cb_info(SslOptions, tls), + Transport = element(1, CbInfo), EmulatedOptions = tls_socket:emulated_options(), {ok, SocketValues} = tls_socket:getopts(Transport, Socket, EmulatedOptions), @@ -1461,6 +1462,7 @@ do_listen(Port, Config, dtls_connection) -> dtls_socket:listen(Port, Config). + -spec handle_options([any()], client | server) -> {ok, #config{}}; ([any()], ssl_options()) -> ssl_options(). @@ -1468,255 +1470,360 @@ handle_options(Opts, Role) -> handle_options(Opts, Role, undefined). -%% Handle ssl options at handshake_continue -handle_options(Opts0, #{protocol := Protocol, - cacerts := CaCerts0, - cacertfile := CaCertFile0} = InheritedSslOpts, _) -> - RecordCB = record_cb(Protocol), - CaCerts = handle_option(cacerts, Opts0, CaCerts0), - {Verify, FailIfNoPeerCert, CaCertDefault, VerifyFun, PartialChainHanlder, - VerifyClientOnce} = handle_verify_options(Opts0, CaCerts), - CaCertFile = case proplists:get_value(cacertfile, Opts0, CaCertFile0) of - undefined -> - CaCertDefault; - CAFile -> - CAFile - end, - - NewVerifyOpts = InheritedSslOpts#{cacerts => CaCerts, - cacertfile => CaCertFile, - verify => Verify, - verify_fun => VerifyFun, - partial_chain => PartialChainHanlder, - fail_if_no_peer_cert => FailIfNoPeerCert, - verify_client_once => VerifyClientOnce}, - SslOpts1 = lists:foldl(fun(Key, PropList) -> - proplists:delete(Key, PropList) - end, Opts0, [cacerts, cacertfile, verify, verify_fun, partial_chain, - fail_if_no_peer_cert, verify_client_once]), - case handle_option(versions, SslOpts1, []) of - [] -> - new_ssl_options(SslOpts1, NewVerifyOpts, RecordCB); - Value -> - Versions0 = [RecordCB:protocol_version(Vsn) || Vsn <- Value], - Versions1 = lists:sort(fun RecordCB:is_higher/2, Versions0), - new_ssl_options(proplists:delete(versions, SslOpts1), - NewVerifyOpts#{versions => Versions1}, record_cb(Protocol)) - end; - +%% Handle ssl options at handshake, handshake_continue +handle_options(Opts0, Role, InheritedSslOpts) when is_map(InheritedSslOpts) -> + {SslOpts, _} = expand_options(Opts0, ?RULES), + process_options(SslOpts, InheritedSslOpts, #{role => Role, + rules => ?RULES}); %% Handle all options in listen, connect and handshake handle_options(Opts0, Role, Host) -> - Opts = proplists:expand([{binary, [{mode, binary}]}, - {list, [{mode, list}]}], Opts0), - assert_proplist(Opts), - RecordCb = record_cb(Opts), - CaCerts = handle_option(cacerts, Opts, undefined), - - {Verify, FailIfNoPeerCert, CaCertDefault, VerifyFun, PartialChainHanlder, VerifyClientOnce} = - handle_verify_options(Opts, CaCerts), - - CertFile = handle_option(certfile, Opts, <<>>), - - [HighestVersion|_] = Versions = - case handle_option(versions, Opts, []) of - [] -> - RecordCb:supported_protocol_versions(); - Vsns -> - Versions0 = [RecordCb:protocol_version(Vsn) || Vsn <- Vsns], - lists:sort(fun RecordCb:is_higher/2, Versions0) - end, - - Protocol = handle_option(protocol, Opts, tls), + {SslOpts0, SockOpts} = expand_options(Opts0, ?RULES), + + %% Ensure all options are evaluated at startup + SslOpts1 = add_missing_options(SslOpts0, ?RULES), + SslOpts = #{protocol := Protocol, + versions := Versions} + = process_options(SslOpts1, + #{}, + #{role => Role, + host => Host, + rules => ?RULES}), case Versions of [{3, 0}] -> - reject_alpn_next_prot_options(Opts); + reject_alpn_next_prot_options(SslOpts0); _ -> ok end, - - SSLOptions0 = #ssl_options{ - versions = Versions, - verify = validate_option(verify, Verify), - verify_fun = VerifyFun, - partial_chain = PartialChainHanlder, - fail_if_no_peer_cert = FailIfNoPeerCert, - verify_client_once = VerifyClientOnce, - depth = handle_option(depth, Opts, 1), - cert = handle_option(cert, Opts, undefined), - certfile = CertFile, - key = handle_option(key, Opts, undefined), - keyfile = handle_option(keyfile, Opts, CertFile), - password = handle_option(password, Opts, ""), - cacerts = CaCerts, - cacertfile = handle_option(cacertfile, Opts, CaCertDefault), - dh = handle_option(dh, Opts, undefined), - dhfile = handle_option(dhfile, Opts, undefined), - user_lookup_fun = handle_option(user_lookup_fun, Opts, undefined), - psk_identity = handle_option(psk_identity, Opts, undefined), - srp_identity = handle_option(srp_identity, Opts, undefined), - ciphers = handle_option(ciphers, Opts, undefined, undefined, undefined, HighestVersion), - - eccs = handle_option(eccs, Opts, undefined, undefined, undefined, HighestVersion), - - supported_groups = handle_option(supported_groups, Opts, undefined, undefined, undefined, - HighestVersion), - signature_algs = handle_option(signature_algs, Opts, undefined, Role, undefined, HighestVersion), - signature_algs_cert = handle_option(signature_algs_cert, Opts, undefined, undefined, undefined, - HighestVersion), - reuse_sessions = handle_option(reuse_sessions, Opts, undefined, Role), - - reuse_session = handle_option(reuse_session, Opts, undefined, Role), - - secure_renegotiate = handle_option(secure_renegotiate, Opts, true), - client_renegotiation = handle_option(client_renegotiation, Opts, undefined, Role), - renegotiate_at = handle_option(renegotiate_at, Opts, ?DEFAULT_RENEGOTIATE_AT), - hibernate_after = handle_option(hibernate_after, Opts, infinity), - erl_dist = handle_option(erl_dist, Opts, false), - alpn_advertised_protocols = - handle_option(alpn_advertised_protocols, Opts, undefined), - alpn_preferred_protocols = - handle_option(alpn_preferred_protocols, Opts, undefined), - next_protocols_advertised = - handle_option(next_protocols_advertised, Opts, undefined), - next_protocol_selector = - handle_option(next_protocol_selector, Opts, undefined), - server_name_indication = - handle_option(server_name_indication, Opts, undefined, Role, Host), - sni_hosts = handle_option(sni_hosts, Opts, []), - sni_fun = handle_option(sni_fun, Opts, undefined), - honor_cipher_order = handle_option(honor_cipher_order, Opts, undefined, Role), - honor_ecc_order = handle_option(honor_ecc_order, Opts, undefined, Role), - protocol = Protocol, - padding_check = proplists:get_value(padding_check, Opts, true), - beast_mitigation = handle_option(beast_mitigation, Opts, one_n_minus_one), - fallback = handle_option(fallback, Opts, undefined, Role), - - crl_check = handle_option(crl_check, Opts, false), - crl_cache = handle_option(crl_cache, Opts, {ssl_crl_cache, {internal, []}}), - max_handshake_size = handle_option(max_handshake_size, Opts, ?DEFAULT_MAX_HANDSHAKE_SIZE), - handshake = handle_option(handshake, Opts, full), - customize_hostname_check = handle_option(customize_hostname_check, Opts, []) - }, - LogLevel = handle_option(log_alert, Opts, true), - SSLOptions1 = SSLOptions0#ssl_options{ - log_level = handle_option(log_level, Opts, LogLevel) - }, - - CbInfo = handle_option(cb_info, Opts, default_cb_info(Protocol)), - - SockOpts = lists:foldl(fun(Key, PropList) -> - proplists:delete(Key, PropList) - end, Opts, ?SSL_OPTIONS ++ - [ssl_imp, %% TODO: remove ssl_imp - client_preferred_next_protocols, %% next_protocol_selector - log_alert, - cb_info]), + %% Handle special options {Sock, Emulated} = emulated_options(Protocol, SockOpts), - ConnetionCb = connection_cb(Opts), - SSLOptions = options_to_map(SSLOptions1), - {ok, #config{ssl = SSLOptions, emulated = Emulated, inet_ssl = Sock, - inet_user = Sock, transport_info = CbInfo, connection_cb = ConnetionCb - }}. - -%% handle_option(OptionName, Opts, Default, Role, Role) -> -%% handle_option(OptionName, Opts, Default); -%% handle_option(_, _, undefined = Value, _, _) -> -%% Value. - -handle_option(OptionName, Opts, Default) -> - handle_option(OptionName, Opts, Default, undefined, undefined, undefined). + ConnetionCb = connection_cb(Protocol), + CbInfo = handle_option_cb_info(Opts0, Protocol), + + {ok, #config{ + ssl = SslOpts, + emulated = Emulated, + inet_ssl = Sock, + inet_user = Sock, + transport_info = CbInfo, + connection_cb = ConnetionCb + }}. + + +%% process_options(SSLOptions, OptionsMap, Env) where +%% SSLOptions is the following tuple: +%% {InOptions, SkippedOptions, Counter} %% -handle_option(OptionName, Opts, Default, Role) -> - handle_option(OptionName, Opts, Default, Role, undefined, undefined). -%% -handle_option(OptionName, Opts, Default, Role, Host) -> - handle_option(OptionName, Opts, Default, Role, Host, undefined). -%% -handle_option(sni_fun, Opts, Default, _Role, _Host, _Version) -> - OptFun = validate_option(sni_fun, - proplists:get_value(sni_fun, Opts, Default)), - OptHosts = proplists:get_value(sni_hosts, Opts, undefined), - case {OptFun, OptHosts} of - {Default, _} -> - Default; - {_, undefined} -> - OptFun; +%% The list of options is processed in multiple passes. When +%% processing an option all dependencies must already be resolved. +%% If there are unresolved dependencies the option will be +%% skipped and processed in a subsequent pass. +%% Counter is equal to the number of unprocessed options at +%% the beginning of a pass. Its value must monotonically decrease +%% after each successful pass. +%% If the value of the counter is unchanged at the end of a pass, +%% the processing stops due to faulty input data. +process_options({[], [], _}, OptionsMap, _Env) -> + OptionsMap; +process_options({[], [_|_] = Skipped, Counter}, OptionsMap, Env) + when length(Skipped) < Counter -> + %% Continue handling options if current pass was successful + process_options({Skipped, [], length(Skipped)}, OptionsMap, Env); +process_options({[], [_|_], _Counter}, _OptionsMap, _Env) -> + throw({error, faulty_configuration}); +process_options({[{K0,V} = E|T], S, Counter}, OptionsMap0, Env) -> + K = maybe_map_key_internal(K0), + case check_dependencies(K, OptionsMap0, Env) of + true -> + OptionsMap = handle_option(K, V, OptionsMap0, Env), + process_options({T, S, Counter}, OptionsMap, Env); + false -> + %% Skip option for next pass + process_options({T, [E|S], Counter}, OptionsMap0, Env) + end. + + +handle_option(cacertfile = Option, unbound, #{cacerts := CaCerts, + verify := Verify, + verify_fun := VerifyFun} = OptionsMap, _Env) + when Verify =:= verify_none orelse + Verify =:= 0 -> + Value = validate_option(Option, ca_cert_default(verify_none, VerifyFun, CaCerts)), + OptionsMap#{Option => Value}; +handle_option(cacertfile = Option, unbound, #{cacerts := CaCerts, + verify := Verify, + verify_fun := VerifyFun} = OptionsMap, _Env) + when Verify =:= verify_peer orelse + Verify =:= 1 orelse + Verify =:= 2 -> + Value = validate_option(Option, ca_cert_default(verify_peer, VerifyFun, CaCerts)), + OptionsMap#{Option => Value}; +handle_option(cacertfile = Option, Value0, OptionsMap, _Env) -> + Value = validate_option(Option, Value0), + OptionsMap#{Option => Value}; +handle_option(ciphers = Option, unbound, #{versions := [HighestVersion|_]} = OptionsMap, #{rules := Rules}) -> + Value = handle_cipher_option(default_value(Option, Rules), HighestVersion), + OptionsMap#{Option => Value}; +handle_option(ciphers = Option, Value0, #{versions := [HighestVersion|_]} = OptionsMap, _Env) -> + Value = handle_cipher_option(Value0, HighestVersion), + OptionsMap#{Option => Value}; +handle_option(client_renegotiation = Option, unbound, OptionsMap, #{role := Role}) -> + Value = default_option_role(server, true, Role), + OptionsMap#{Option => Value}; +handle_option(client_renegotiation = Option, Value0, OptionsMap, #{role := Role}) -> + assert_role(server_only, Role, Option, Value0), + Value = validate_option(Option, Value0), + OptionsMap#{Option => Value}; +handle_option(eccs = Option, unbound, #{versions := [HighestVersion|_]} = OptionsMap, #{rules := _Rules}) -> + Value = handle_eccs_option(eccs(), HighestVersion), + OptionsMap#{Option => Value}; +handle_option(eccs = Option, Value0, #{versions := [HighestVersion|_]} = OptionsMap, _Env) -> + Value = handle_eccs_option(Value0, HighestVersion), + OptionsMap#{Option => Value}; +handle_option(fallback = Option, unbound, OptionsMap, #{role := Role}) -> + Value = default_option_role(client, false, Role), + OptionsMap#{Option => Value}; +handle_option(fallback = Option, Value0, OptionsMap, #{role := Role}) -> + assert_role(client_only, Role, Option, Value0), + Value = validate_option(Option, Value0), + OptionsMap#{Option => Value}; +handle_option(honor_cipher_order = Option, unbound, OptionsMap, #{role := Role}) -> + Value = default_option_role(server, false, Role), + OptionsMap#{Option => Value}; +handle_option(honor_cipher_order = Option, Value0, OptionsMap, #{role := Role}) -> + assert_role(server_only, Role, Option, Value0), + Value = validate_option(Option, Value0), + OptionsMap#{Option => Value}; +handle_option(honor_ecc_order = Option, unbound, OptionsMap, #{role := Role}) -> + Value = default_option_role(server, false, Role), + OptionsMap#{Option => Value}; +handle_option(honor_ecc_order = Option, Value0, OptionsMap, #{role := Role}) -> + assert_role(server_only, Role, Option, Value0), + Value = validate_option(Option, Value0), + OptionsMap#{Option => Value}; +handle_option(keyfile = Option, unbound, #{certfile := CertFile} = OptionsMap, _Env) -> + Value = validate_option(Option, CertFile), + OptionsMap#{Option => Value}; +handle_option(next_protocol_selector = Option, unbound, OptionsMap, #{rules := Rules}) -> + Value = default_value(Option, Rules), + OptionsMap#{Option => Value}; +handle_option(next_protocol_selector = Option, Value0, OptionsMap, _Env) -> + Value = make_next_protocol_selector( + validate_option(client_preferred_next_protocols, Value0)), + OptionsMap#{Option => Value}; +handle_option(reuse_session = Option, unbound, OptionsMap, #{role := Role}) -> + Value = + case Role of + client -> + undefined; + server -> + fun(_, _, _, _) -> true end + end, + OptionsMap#{Option => Value}; +handle_option(reuse_session = Option, Value0, OptionsMap, _Env) -> + Value = validate_option(Option, Value0), + OptionsMap#{Option => Value}; +%% TODO: validate based on role +handle_option(reuse_sessions = Option, unbound, OptionsMap, #{rules := Rules}) -> + Value = validate_option(Option, default_value(Option, Rules)), + OptionsMap#{Option => Value}; +handle_option(reuse_sessions = Option, Value0, OptionsMap, _Env) -> + Value = validate_option(Option, Value0), + OptionsMap#{Option => Value}; +handle_option(anti_replay = Option, unbound, OptionsMap, #{rules := Rules}) -> + Value = validate_option(Option, default_value(Option, Rules)), + OptionsMap#{Option => Value}; +handle_option(anti_replay = Option, Value0, + #{session_tickets := SessionTickets} = OptionsMap, #{rules := Rules}) -> + case SessionTickets of + stateless -> + Value = validate_option(Option, Value0), + OptionsMap#{Option => Value}; _ -> - throw({error, {conflict_options, [sni_fun, sni_hosts]}}) - end; -handle_option(cb_info, Opts, Default, _Role, _Host, _Version) -> - CbInfo = proplists:get_value(cb_info, Opts, Default), - true = validate_option(cb_info, CbInfo), - handle_cb_info(CbInfo, Default); -handle_option(ciphers = Key, Opts, _Default, _Role, _Host, HighestVersion) -> - handle_cipher_option(proplists:get_value(Key, Opts, []), - HighestVersion); -handle_option(eccs = Key, Opts, _Default, _Role, _Host, HighestVersion) -> - handle_eccs_option(proplists:get_value(Key, Opts, eccs()), - HighestVersion); -handle_option(supported_groups = Key, Opts, _Default, _Role, _Host, HighestVersion) -> - handle_supported_groups_option(proplists:get_value(Key, Opts, groups(default)), - HighestVersion); -handle_option(signature_algs = Key, Opts, _Default, Role, _Host, HighestVersion) -> - handle_hashsigns_option( - proplists:get_value(Key, - Opts, - default_option_role_sign_algs(server, - tls_v1:default_signature_algs(HighestVersion), - Role, - HighestVersion)), - tls_version(HighestVersion)); -handle_option(signature_algs_cert = Key, Opts, _Default, _Role, _Host, HighestVersion) -> - handle_signature_algorithms_option( - proplists:get_value(Key, - Opts, - undefined), %% Do not send by default - tls_version(HighestVersion)); -handle_option(reuse_sessions = Key, Opts, _Default, client, _Host, _Version) -> - Value = proplists:get_value(Key, Opts, true), - validate_option(Key, Value), - Value; -handle_option(reuse_sessions = Key, Opts0, _Default, server, _Host, _Version) -> - Opts = proplists:delete({Key, save}, Opts0), - Value = proplists:get_value(Key, Opts, true), - validate_option(Key, Value), - Value; -handle_option(reuse_session = Key, Opts, _Default, client, _Host, _Version) -> - Value = proplists:get_value(Key, Opts, undefined), - validate_option(Key, Value), - Value; -handle_option(reuse_session = Key, Opts, _Default, server, _Host, _Version) -> - ReuseSessionFun = fun(_, _, _, _) -> true end, - Value = proplists:get_value(Key, Opts, ReuseSessionFun), - validate_option(Key, Value), - Value; -handle_option(fallback = Key, Opts, _Default, Role, _Host, _Version) -> - Value = proplists:get_value(Key, Opts, default_option_role(client, false, Role)), - assert_role(client_only, Role, Key, Value), - validate_option(Key, Value); -handle_option(client_renegotiation = Key, Opts, _Default, Role, _Host, _Version) -> - Value = proplists:get_value(Key, Opts, default_option_role(server, true, Role)), - assert_role(server_only, Role, Key, Value), - validate_option(Key, Value); -handle_option(honor_cipher_order = Key, Opts, _Default, Role, _Host, _Version) -> - Value = proplists:get_value(Key, Opts, default_option_role(server, false, Role)), - assert_role(server_only, Role, Key, Value), - validate_option(Key, Value); -handle_option(honor_ecc_order = Key, Opts, _Default, Role, _Host, _Version) -> - Value = proplists:get_value(Key, Opts, default_option_role(server, false, Role)), - assert_role(server_only, Role, Key, Value), - validate_option(Key, Value); -handle_option(next_protocol_selector = _Key, Opts, _Default, _Role, _Host, _Version) -> - make_next_protocol_selector( - handle_option(client_preferred_next_protocols, Opts, undefined, undefined, undefined, undefined)); -handle_option(server_name_indication = Key, Opts, _Default, Role, Host, _Version) -> - Default = default_option_role(client, server_name_indication_default(Host), Role), - validate_option(Key, proplists:get_value(Key, Opts, Default)); -handle_option(OptionName, Opts, Default, _Role, _Host, _Version) -> - validate_option(OptionName, - proplists:get_value(OptionName, Opts, Default)). + OptionsMap#{Option => default_value(Option, Rules)} +end; +handle_option(server_name_indication = Option, unbound, OptionsMap, #{host := Host, + role := Role}) -> + Value = default_option_role(client, server_name_indication_default(Host), Role), + OptionsMap#{Option => Value}; +handle_option(server_name_indication = Option, Value0, OptionsMap, _Env) -> + Value = validate_option(Option, Value0), + OptionsMap#{Option => Value}; +handle_option(signature_algs = Option, unbound, #{versions := [HighestVersion|_]} = OptionsMap, #{role := Role}) -> + Value = + handle_hashsigns_option( + default_option_role_sign_algs( + server, + tls_v1:default_signature_algs(HighestVersion), + Role, + HighestVersion), + tls_version(HighestVersion)), + OptionsMap#{Option => Value}; +handle_option(signature_algs = Option, Value0, #{versions := [HighestVersion|_]} = OptionsMap, _Env) -> + Value = handle_hashsigns_option(Value0, tls_version(HighestVersion)), + OptionsMap#{Option => Value}; +handle_option(signature_algs_cert = Option, unbound, #{versions := [HighestVersion|_]} = OptionsMap, _Env) -> + %% Do not send by default + Value = handle_signature_algorithms_option(undefined, tls_version(HighestVersion)), + OptionsMap#{Option => Value}; +handle_option(signature_algs_cert = Option, Value0, #{versions := [HighestVersion|_]} = OptionsMap, _Env) -> + Value = handle_signature_algorithms_option(Value0, tls_version(HighestVersion)), + OptionsMap#{Option => Value}; +handle_option(sni_fun = Option, unbound, OptionsMap, #{rules := Rules}) -> + Value = default_value(Option, Rules), + OptionsMap#{Option => Value}; +handle_option(sni_fun = Option, Value0, OptionsMap, _Env) -> + validate_option(Option, Value0), + OptHosts = maps:get(sni_hosts, OptionsMap, undefined), + Value = + case {Value0, OptHosts} of + {undefined, _} -> + Value0; + {_, []} -> + Value0; + _ -> + throw({error, {conflict_options, [sni_fun, sni_hosts]}}) + end, + OptionsMap#{Option => Value}; +handle_option(supported_groups = Option, unbound, #{versions := [HighestVersion|_]} = OptionsMap, #{rules := _Rules}) -> + Value = handle_supported_groups_option(groups(default), HighestVersion), + OptionsMap#{Option => Value}; +handle_option(supported_groups = Option, Value0, #{versions := [HighestVersion|_]} = OptionsMap, _Env) -> + Value = handle_supported_groups_option(Value0, HighestVersion), + OptionsMap#{Option => Value}; +handle_option(verify = Option, unbound, OptionsMap, #{rules := Rules}) -> + handle_verify_option(default_value(Option, Rules), OptionsMap); +handle_option(verify = _Option, Value, OptionsMap, _Env) -> + handle_verify_option(Value, OptionsMap); + +handle_option(verify_fun = Option, unbound, #{verify := Verify} = OptionsMap, #{rules := Rules}) + when Verify =:= verify_none orelse + Verify =:= 0 -> + OptionsMap#{Option => default_value(Option, Rules)}; +handle_option(verify_fun = Option, unbound, #{verify := Verify} = OptionsMap, _Env) + when Verify =:= verify_peer orelse + Verify =:= 1 orelse + Verify =:= 2 -> + OptionsMap#{Option => undefined}; +handle_option(verify_fun = Option, Value0, OptionsMap, _Env) -> + Value = validate_option(Option, Value0), + OptionsMap#{Option => Value}; +handle_option(versions = Option, unbound, #{protocol := Protocol} = OptionsMap, _Env) -> + RecordCb = record_cb(Protocol), + Vsns0 = RecordCb:supported_protocol_versions(), + Value = lists:sort(fun RecordCb:is_higher/2, Vsns0), + OptionsMap#{Option => Value}; +handle_option(versions = Option, Vsns0, #{protocol := Protocol} = OptionsMap, _Env) -> + validate_option(versions, Vsns0), + RecordCb = record_cb(Protocol), + Vsns1 = [RecordCb:protocol_version(Vsn) || Vsn <- Vsns0], + Value = lists:sort(fun RecordCb:is_higher/2, Vsns1), + OptionsMap#{Option => Value}; +%% Special options +handle_option(cb_info = Option, unbound, #{protocol := Protocol} = OptionsMap, _Env) -> + Default = default_cb_info(Protocol), + validate_option(Option, Default), + Value = handle_cb_info(Default), + OptionsMap#{Option => Value}; +handle_option(cb_info = Option, Value0, OptionsMap, _Env) -> + validate_option(Option, Value0), + Value = handle_cb_info(Value0), + OptionsMap#{Option => Value}; +%% Generic case +handle_option(Option, unbound, OptionsMap, #{rules := Rules}) -> + Value = validate_option(Option, default_value(Option, Rules)), + OptionsMap#{Option => Value}; +handle_option(Option, Value0, OptionsMap, _Env) -> + Value = validate_option(Option, Value0), + OptionsMap#{Option => Value}. + +handle_option_cb_info(Options, Protocol) -> + Value = proplists:get_value(cb_info, Options, default_cb_info(Protocol)), + #{cb_info := CbInfo} = handle_option(cb_info, Value, #{protocol => Protocol}, #{}), + CbInfo. + + +maybe_map_key_internal(client_preferred_next_protocols) -> + next_protocol_selector; +maybe_map_key_internal(K) -> + K. + + +maybe_map_key_external(next_protocol_selector) -> + client_preferred_next_protocols; +maybe_map_key_external(K) -> + K. + + +check_dependencies(K, OptionsMap, Env) -> + Rules = maps:get(rules, Env), + Deps = get_dependencies(K, Rules), + case Deps of + [] -> + true; + L -> + option_already_defined(K,OptionsMap) orelse + dependecies_already_defined(L, OptionsMap) + end. + + +%% Handle options that are not present in the map +get_dependencies(K, _) when K =:= cb_info orelse K =:= log_alert-> + []; +get_dependencies(K, Rules) -> + {_, Deps} = maps:get(K, Rules), + Deps. + + +option_already_defined(K, Map) -> + maps:get(K, Map, unbound) =/= unbound. + + +dependecies_already_defined(L, OptionsMap) -> + Fun = fun (E) -> option_already_defined(E, OptionsMap) end, + lists:all(Fun, L). + + +expand_options(Opts0, Rules) -> + Opts1 = proplists:expand([{binary, [{mode, binary}]}, + {list, [{mode, list}]}], Opts0), + assert_proplist(Opts1), + + %% Remove depricated ssl_imp option + Opts = proplists:delete(ssl_imp, Opts1), + AllOpts = maps:keys(Rules), + SockOpts = lists:foldl(fun(Key, PropList) -> proplists:delete(Key, PropList) end, + Opts, + AllOpts ++ + [ssl_imp, %% TODO: remove ssl_imp + cb_info, + client_preferred_next_protocols, %% next_protocol_selector + log_alert]), %% obsoleted by log_level + + SslOpts = {Opts -- SockOpts, [], length(Opts -- SockOpts)}, + {SslOpts, SockOpts}. + + +add_missing_options({L0, S, _C}, Rules) -> + Fun = fun(K0, Acc) -> + K = maybe_map_key_external(K0), + case proplists:is_defined(K, Acc) of + true -> + Acc; + false -> + Default = unbound, + [{K, Default}|Acc] + end + end, + AllOpts = maps:keys(Rules), + L = lists:foldl(Fun, L0, AllOpts), + {L, S, length(L)}. + + +default_value(Key, Rules) -> + {Default, _} = maps:get(Key, Rules, {undefined, []}), + Default. assert_role(client_only, client, _, _) -> @@ -1961,23 +2068,45 @@ validate_option(handshake, full = Value) -> Value; validate_option(customize_hostname_check, Value) when is_list(Value) -> Value; -validate_option(cb_info, {V1, V2, V3, V4}) when is_atom(V1), - is_atom(V2), - is_atom(V3), - is_atom(V4) +validate_option(cb_info, {V1, V2, V3, V4} = Value) when is_atom(V1), + is_atom(V2), + is_atom(V3), + is_atom(V4) -> - true; -validate_option(cb_info, {V1, V2, V3, V4, V5}) when is_atom(V1), - is_atom(V2), - is_atom(V3), - is_atom(V4), - is_atom(V5) + Value; +validate_option(cb_info, {V1, V2, V3, V4, V5} = Value) when is_atom(V1), + is_atom(V2), + is_atom(V3), + is_atom(V4), + is_atom(V5) -> - true; -validate_option(cb_info, _) -> - false; + Value; +validate_option(use_ticket, Value) when is_list(Value) -> + Value; +validate_option(session_tickets, Value) when Value =:= disabled orelse + Value =:= enabled orelse + Value =:= auto orelse + Value =:= stateless orelse + Value =:= stateful -> + Value; +validate_option(anti_replay, '10k') -> + %% n = 10000 + %% p = 0.030003564 (1 in 33) + %% m = 72985 (8.91KiB) + %% k = 5 + {10, 5, 72985}; +validate_option(anti_replay, '100k') -> + %% n = 100000 + %% p = 0.03000428 (1 in 33) + %% m = 729845 (89.09KiB) + %% k = 5 + {10, 5, 729845}; +validate_option(anti_replay, Value) when (is_tuple(Value) andalso + tuple_size(Value) =:= 3) -> + Value; validate_option(Opt, undefined = Value) -> - case lists:member(Opt, ?SSL_OPTIONS) of + AllOpts = maps:keys(?RULES), + case lists:member(Opt, AllOpts) of true -> Value; false -> @@ -1986,9 +2115,9 @@ validate_option(Opt, undefined = Value) -> validate_option(Opt, Value) -> throw({error, {options, {Opt, Value}}}). -handle_cb_info({V1, V2, V3, V4}, {_,_,_,_,_}) -> +handle_cb_info({V1, V2, V3, V4}) -> {V1,V2,V3,V4, list_to_atom(atom_to_list(V2) ++ "_passive")}; -handle_cb_info(CbInfo, _) -> +handle_cb_info(CbInfo) -> CbInfo. handle_hashsigns_option(Value, Version) when is_list(Value) @@ -2275,159 +2404,22 @@ assert_proplist([inet6 | Rest]) -> assert_proplist([Value | _]) -> throw({option_not_a_key_value_tuple, Value}). -new_ssl_options([], #{} = Opts, _) -> - Opts; -new_ssl_options([{verify_client_once, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{verify_client_once => - validate_option(verify_client_once, Value)}, RecordCB); -new_ssl_options([{depth, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{depth => validate_option(depth, Value)}, RecordCB); -new_ssl_options([{cert, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{cert => validate_option(cert, Value)}, RecordCB); -new_ssl_options([{certfile, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{certfile => validate_option(certfile, Value)}, RecordCB); -new_ssl_options([{key, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{key => validate_option(key, Value)}, RecordCB); -new_ssl_options([{keyfile, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{keyfile => validate_option(keyfile, Value)}, RecordCB); -new_ssl_options([{password, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{password => validate_option(password, Value)}, RecordCB); -new_ssl_options([{dh, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{dh => validate_option(dh, Value)}, RecordCB); -new_ssl_options([{dhfile, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{dhfile => validate_option(dhfile, Value)}, RecordCB); -new_ssl_options([{user_lookup_fun, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{user_lookup_fun => validate_option(user_lookup_fun, Value)}, RecordCB); -new_ssl_options([{psk_identity, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{psk_identity => validate_option(psk_identity, Value)}, RecordCB); -new_ssl_options([{srp_identity, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{srp_identity => validate_option(srp_identity, Value)}, RecordCB); -new_ssl_options([{ciphers, Value} | Rest], #{versions := Versions} = Opts, RecordCB) -> - Ciphers = handle_cipher_option(Value, RecordCB:highest_protocol_version(Versions)), - new_ssl_options(Rest, Opts#{ciphers => Ciphers}, RecordCB); -new_ssl_options([{reuse_session, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{reuse_session => validate_option(reuse_session, Value)}, RecordCB); -new_ssl_options([{reuse_sessions, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{reuse_sessions => validate_option(reuse_sessions, Value)}, RecordCB); -new_ssl_options([{ssl_imp, _Value} | Rest], #{} = Opts, RecordCB) -> %% Not used backwards compatibility - new_ssl_options(Rest, Opts, RecordCB); -new_ssl_options([{renegotiate_at, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{renegotiate_at => validate_option(renegotiate_at, Value)}, RecordCB); -new_ssl_options([{secure_renegotiate, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{secure_renegotiate => validate_option(secure_renegotiate, Value)}, RecordCB); -new_ssl_options([{client_renegotiation, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{client_renegotiation => validate_option(client_renegotiation, Value)}, RecordCB); -new_ssl_options([{hibernate_after, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{hibernate_after => validate_option(hibernate_after, Value)}, RecordCB); -new_ssl_options([{alpn_advertised_protocols, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{alpn_advertised_protocols => validate_option(alpn_advertised_protocols, Value)}, - RecordCB); -new_ssl_options([{alpn_preferred_protocols, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{alpn_preferred_protocols => validate_option(alpn_preferred_protocols, Value)}, - RecordCB); -new_ssl_options([{next_protocols_advertised, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{next_protocols_advertised => validate_option(next_protocols_advertised, Value)}, - RecordCB); -new_ssl_options([{client_preferred_next_protocols, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, - Opts#{next_protocol_selector => - make_next_protocol_selector(validate_option(client_preferred_next_protocols, Value))}, - RecordCB); -new_ssl_options([{log_alert, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{log_level => validate_option(log_alert, Value)}, RecordCB); -new_ssl_options([{log_level, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{log_level => validate_option(log_level, Value)}, RecordCB); -new_ssl_options([{server_name_indication, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{server_name_indication => validate_option(server_name_indication, Value)}, RecordCB); -new_ssl_options([{honor_cipher_order, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{honor_cipher_order => validate_option(honor_cipher_order, Value)}, RecordCB); -new_ssl_options([{honor_ecc_order, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, Opts#{honor_ecc_order => validate_option(honor_ecc_order, Value)}, RecordCB); -new_ssl_options([{eccs, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, - Opts#{eccs => handle_eccs_option(Value, RecordCB:highest_protocol_version()) - }, - RecordCB); -new_ssl_options([{supported_groups, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, - Opts#{supported_groups => - handle_supported_groups_option(Value, RecordCB:highest_protocol_version()) - }, - RecordCB); -new_ssl_options([{signature_algs, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options(Rest, - Opts#{signature_algs => - handle_hashsigns_option(Value, - tls_version(RecordCB:highest_protocol_version()))}, - RecordCB); -new_ssl_options([{signature_algs_cert, Value} | Rest], #{} = Opts, RecordCB) -> - new_ssl_options( - Rest, - Opts#{signature_algs_cert => - handle_signature_algorithms_option( - Value, - tls_version(RecordCB:highest_protocol_version()))}, - RecordCB); -new_ssl_options([{protocol, dtls = Value} | Rest], #{} = Opts, dtls_record = RecordCB) -> - new_ssl_options(Rest, Opts#{protocol => Value}, RecordCB); -new_ssl_options([{protocol, tls = Value} | Rest], #{} = Opts, tls_record = RecordCB) -> - new_ssl_options(Rest, Opts#{protocol => Value}, RecordCB); -new_ssl_options([{Key, Value} | _Rest], #{}, _) -> - throw({error, {options, {Key, Value}}}). - - -handle_verify_options(Opts, CaCerts) -> - DefaultVerifyNoneFun = - {fun(_,{bad_cert, _}, UserState) -> - {valid, UserState}; - (_,{extension, #'Extension'{critical = true}}, UserState) -> - %% This extension is marked as critical, so - %% certificate verification should fail if we don't - %% understand the extension. However, this is - %% `verify_none', so let's accept it anyway. - {valid, UserState}; - (_,{extension, _}, UserState) -> - {unknown, UserState}; - (_, valid, UserState) -> - {valid, UserState}; - (_, valid_peer, UserState) -> - {valid, UserState} - end, []}, - VerifyNoneFun = handle_option(verify_fun, Opts, DefaultVerifyNoneFun), - - UserFailIfNoPeerCert = handle_option(fail_if_no_peer_cert, Opts, false), - UserVerifyFun = handle_option(verify_fun, Opts, undefined), - - PartialChainHanlder = handle_option(partial_chain, Opts, - fun(_) -> unknown_ca end), - - VerifyClientOnce = handle_option(verify_client_once, Opts, false), - - %% Handle 0, 1, 2 for backwards compatibility - case proplists:get_value(verify, Opts, verify_none) of - 0 -> - {verify_none, false, - ca_cert_default(verify_none, VerifyNoneFun, CaCerts), - VerifyNoneFun, PartialChainHanlder, VerifyClientOnce}; - 1 -> - {verify_peer, false, - ca_cert_default(verify_peer, UserVerifyFun, CaCerts), - UserVerifyFun, PartialChainHanlder, VerifyClientOnce}; - 2 -> - {verify_peer, true, - ca_cert_default(verify_peer, UserVerifyFun, CaCerts), - UserVerifyFun, PartialChainHanlder, VerifyClientOnce}; - verify_none -> - {verify_none, false, - ca_cert_default(verify_none, VerifyNoneFun, CaCerts), - VerifyNoneFun, PartialChainHanlder, VerifyClientOnce}; - verify_peer -> - {verify_peer, UserFailIfNoPeerCert, - ca_cert_default(verify_peer, UserVerifyFun, CaCerts), - UserVerifyFun, PartialChainHanlder, VerifyClientOnce}; - Value -> - throw({error, {options, {verify, Value}}}) - end. + +handle_verify_option(verify_none, #{fail_if_no_peer_cert := _FailIfNoPeerCert} = OptionsMap) -> + OptionsMap#{verify => verify_none, + fail_if_no_peer_cert => false}; +handle_verify_option(verify_peer, #{fail_if_no_peer_cert := FailIfNoPeerCert} = OptionsMap) -> + OptionsMap#{verify => verify_peer, + fail_if_no_peer_cert => FailIfNoPeerCert}; +%% Handle 0, 1, 2 for backwards compatibility +handle_verify_option(0, OptionsMap) -> + handle_verify_option(verify_none, OptionsMap); +handle_verify_option(1, OptionsMap) -> + handle_verify_option(verify_peer, OptionsMap#{fail_if_no_peer_cert => false}); +handle_verify_option(2, OptionsMap) -> + handle_verify_option(verify_peer, OptionsMap#{fail_if_no_peer_cert => true}); +handle_verify_option(Value, _) -> + throw({error, {options, {verify, Value}}}). %% Added to handle default values for signature_algs in TLS 1.3 default_option_role_sign_algs(_, Value, _, Version) when Version >= {3,4} -> @@ -2464,7 +2456,7 @@ server_name_indication_default(_) -> undefined. -reject_alpn_next_prot_options(Opts) -> +reject_alpn_next_prot_options({Opts,_,_}) -> AlpnNextOpts = [alpn_advertised_protocols, alpn_preferred_protocols, next_protocols_advertised, @@ -2486,10 +2478,3 @@ add_filter(undefined, Filters) -> Filters; add_filter(Filter, Filters) -> [Filter | Filters]. - -%% Convert the record #ssl_options{} into a map for internal usage -options_to_map(Options) -> - Fields = record_info(fields, ssl_options), - [_Tag| Values] = tuple_to_list(Options), - L = lists:zip(Fields, Values), - maps:from_list(L). diff --git a/lib/ssl/src/ssl_admin_sup.erl b/lib/ssl/src/ssl_admin_sup.erl index 9c96435753..01ddbb7686 100644 --- a/lib/ssl/src/ssl_admin_sup.erl +++ b/lib/ssl/src/ssl_admin_sup.erl @@ -46,7 +46,8 @@ start_link() -> init([]) -> PEMCache = pem_cache_child_spec(), SessionCertManager = session_and_cert_manager_child_spec(), - {ok, {{rest_for_one, 10, 3600}, [PEMCache, SessionCertManager]}}. + TicketStore = ticket_store_spec(), + {ok, {{rest_for_one, 10, 3600}, [PEMCache, SessionCertManager, TicketStore]}}. manager_opts() -> CbOpts = case application:get_env(ssl, session_cb) of @@ -86,6 +87,16 @@ session_and_cert_manager_child_spec() -> Type = worker, {Name, StartFunc, Restart, Shutdown, Type, Modules}. +ticket_store_spec() -> + Name = tls_client_ticket_store, + %% TODO do not hardcode storage size and lifetime + StartFunc = {tls_client_ticket_store, start_link, [20,10]}, + Restart = permanent, + Shutdown = 4000, + Modules = [tls_client_ticket_store], + Type = worker, + {Name, StartFunc, Restart, Shutdown, Type, Modules}. + session_cb_init_args() -> case application:get_env(ssl, session_cb_init_args) of {ok, Args} when is_list(Args) -> diff --git a/lib/ssl/src/ssl_alert.erl b/lib/ssl/src/ssl_alert.erl index 2d57b72f7b..d4b0cb1f14 100644 --- a/lib/ssl/src/ssl_alert.erl +++ b/lib/ssl/src/ssl_alert.erl @@ -33,9 +33,8 @@ -include("ssl_internal.hrl"). -export([decode/1, - own_alert_txt/1, - alert_txt/1, - alert_txt/4, + own_alert_format/4, + alert_format/4, reason_code/4]). %%==================================================================== @@ -62,53 +61,69 @@ decode(Bin) -> reason_code(#alert{description = ?CLOSE_NOTIFY}, _, _, _) -> closed; reason_code(#alert{description = Description, role = Role} = Alert, Role, ProtocolName, StateName) -> - Txt = lists:flatten(alert_txt(ProtocolName, Role, StateName, own_alert_txt(Alert))), + {PrefixFmt, PrefixArgs} = alert_prefix_format(ProtocolName, Role, StateName), + {Fmt, Args} = own_alert_format_depth(Alert), + Txt = lists:flatten(io_lib:format(PrefixFmt ++ Fmt, PrefixArgs ++ Args)), {tls_alert, {description_atom(Description), Txt}}; reason_code(#alert{description = Description} = Alert, Role, ProtocolName, StateName) -> - Txt = lists:flatten(alert_txt(ProtocolName, Role, StateName, alert_txt(Alert))), + {Fmt, Args} = alert_format(ProtocolName, Role, StateName, Alert), + Txt = lists:flatten(io_lib:format(Fmt, Args)), {tls_alert, {description_atom(Description), Txt}}. %%-------------------------------------------------------------------- --spec alert_txt(string(), server | client, StateNam::atom(), string()) -> string(). +-spec own_alert_format(string(), server | client, StateNam::atom(), #alert{}) -> {io:format(), list()}. %% %% Description: Generates alert text for log or string part of error return. %%-------------------------------------------------------------------- -alert_txt(ProtocolName, Role, StateName, Txt) -> - io_lib:format("~s ~p: In state ~p ~s\n", [ProtocolName, Role, StateName, Txt]). +own_alert_format(ProtocolName, Role, StateName, Alert) -> + {PrfixFmt, PrefixArgs} = alert_prefix_format(ProtocolName, Role, StateName), + {Fmt, Args} = own_alert_format(Alert), + {PrfixFmt ++ Fmt, PrefixArgs ++ Args}. %%-------------------------------------------------------------------- --spec own_alert_txt(#alert{}) -> string(). +-spec alert_format(string(), server | client, StateNam::atom(), #alert{}) -> {io:format(), list()}. %% -%% Description: Returns the error string for given alert generated -%% by the erlang implementation. -%%-------------------------------------------------------------------- -own_alert_txt(#alert{level = Level, description = Description, where = {Mod,Line}, reason = undefined, role = Role}) -> - "at " ++ Mod ++ ":" ++ integer_to_list(Line) ++ " generated " ++ string:uppercase(atom_to_list(Role)) ++ " ALERT: " ++ - level_txt(Level) ++ description_txt(Description); -own_alert_txt(#alert{reason = Reason} = Alert) -> - BaseTxt = own_alert_txt(Alert#alert{reason = undefined}), - FormatDepth = 9, % Some limit on printed representation of an error - ReasonTxt = lists:flatten(io_lib:format("~P", [Reason, FormatDepth])), - BaseTxt ++ " - " ++ ReasonTxt. - -%%-------------------------------------------------------------------- --spec alert_txt(#alert{}) -> string(). -%% -%% Description: Returns the error string for given alert received from -%% the peer. +%% Description: Generates alert text for log or string part of error return. %%-------------------------------------------------------------------- -alert_txt(#alert{level = Level, description = Description, reason = undefined, role = Role}) -> - "received " ++ string:uppercase(atom_to_list(Role)) ++ " ALERT: " ++ - level_txt(Level) ++ description_txt(Description); -alert_txt(#alert{reason = Reason} = Alert) -> - BaseTxt = alert_txt(Alert#alert{reason = undefined}), - FormatDepth = 9, % Some limit on printed representation of an error - ReasonTxt = lists:flatten(io_lib:format("~P", [Reason, FormatDepth])), - BaseTxt ++ " - " ++ ReasonTxt. +alert_format(ProtocolName, Role, StateName, Alert) -> + {PrfixFmt, PrefixArgs} = alert_prefix_format(ProtocolName, Role, StateName), + {Fmt, Args} = alert_format(Alert), + {PrfixFmt ++ Fmt, PrefixArgs ++ Args}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- +alert_prefix_format(ProtocolName, Role, StateName) -> + {"~s ~p: In state ~p", [ProtocolName, Role, StateName]}. + +own_alert_format(#alert{reason = Reason} = Alert) -> + Txt = own_alert_txt(Alert), + case Reason of + undefined -> + {" ~s\n", [Txt]}; + Reason -> + {" ~s\n - ~p", [Txt, Reason]} + end. +own_alert_format_depth(#alert{reason = Reason} = Alert) -> + Txt = own_alert_txt(Alert), + case Reason of + undefined -> + {" ~s\n", [Txt]}; + Reason -> + {" ~s\n ~P", [Txt, Reason, ?DEPTH]} + end. + +own_alert_txt(#alert{level = Level, description = Description, where = #{line := Line, file := Mod}, role = Role}) -> + "at " ++ Mod ++ ":" ++ integer_to_list(Line) ++ " generated " ++ string:uppercase(atom_to_list(Role)) ++ " ALERT: " ++ + level_txt(Level) ++ description_txt(Description). + +alert_format(Alert) -> + Txt = alert_txt(Alert), + {" ~s\n ", [Txt]}. + +alert_txt(#alert{level = Level, description = Description, role = Role}) -> + "received " ++ string:uppercase(atom_to_list(Role)) ++ " ALERT: " ++ + level_txt(Level) ++ description_txt(Description). %% It is very unlikely that an correct implementation will send more than one alert at the time %% So it there is more than 10 warning alerts we consider it an error diff --git a/lib/ssl/src/ssl_alert.hrl b/lib/ssl/src/ssl_alert.hrl index 9b2322da17..0edc4bf8e4 100644 --- a/lib/ssl/src/ssl_alert.hrl +++ b/lib/ssl/src/ssl_alert.hrl @@ -26,6 +26,7 @@ -ifndef(ssl_alert). -define(ssl_alert, true). +-include_lib("kernel/include/logger.hrl"). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Alert protocol - RFC 2246 section 7.2 @@ -113,8 +114,8 @@ -define(CERTIFICATE_REQUIRED, 116). -define(NO_APPLICATION_PROTOCOL, 120). --define(ALERT_REC(Level,Desc), #alert{level=Level,description=Desc,where={?FILE, ?LINE}}). --define(ALERT_REC(Level,Desc,Reason), #alert{level=Level,description=Desc,where={?FILE, ?LINE},reason=Reason}). +-define(ALERT_REC(Level,Desc), #alert{level=Level,description=Desc,where= ?LOCATION}). +-define(ALERT_REC(Level,Desc,Reason), #alert{level=Level,description=Desc,where=?LOCATION,reason=Reason}). -define(MAX_ALERTS, 10). @@ -122,7 +123,7 @@ -record(alert, { level, description, - where = {?FILE, ?LINE}, + where, role, reason }). diff --git a/lib/ssl/src/ssl_app.erl b/lib/ssl/src/ssl_app.erl index 9e6d676bef..840a075e84 100644 --- a/lib/ssl/src/ssl_app.erl +++ b/lib/ssl/src/ssl_app.erl @@ -45,7 +45,8 @@ start_logger() -> Filter = {fun logger_filters:domain/2,{log,sub,[otp,ssl]}}, logger:add_handler(ssl_handler, logger_std_h, Config), logger:add_handler_filter(ssl_handler, filter_non_ssl, Filter), - logger:set_application_level(ssl, debug). + logger:set_module_level([ssl_logger], + debug). %% %% Description: Stop SSL logger diff --git a/lib/ssl/src/ssl_certificate.erl b/lib/ssl/src/ssl_certificate.erl index 9997f5e0c8..104bbd65c8 100644 --- a/lib/ssl/src/ssl_certificate.erl +++ b/lib/ssl/src/ssl_certificate.erl @@ -103,7 +103,7 @@ certificate_chain(OwnCert, CertDbHandle, CertsDbRef) -> certificate_chain(OwnCert, DerCert, CertDbHandle, CertsDbRef, [DerCert], []). %%-------------------------------------------------------------------- --spec certificate_chain(undefined | binary() | #'OTPCertificate'{} , db_handle(), certdb_ref(), [der_cert()]) -> +-spec certificate_chain(undefined | binary() | #'OTPCertificate'{} , db_handle(), certdb_ref() | {extracted, list()}, [der_cert()]) -> {error, no_cert} | {ok, #'OTPCertificate'{} | undefined, [der_cert()]}. %% %% Description: Create certificate chain with certs from diff --git a/lib/ssl/src/ssl_cipher.erl b/lib/ssl/src/ssl_cipher.erl index c16e2331ff..c97884ec08 100644 --- a/lib/ssl/src/ssl_cipher.erl +++ b/lib/ssl/src/ssl_cipher.erl @@ -48,7 +48,11 @@ key_material/1, signature_algorithm_to_scheme/1]). %% RFC 8446 TLS 1.3 --export([generate_client_shares/1, generate_server_share/1, add_zero_padding/2]). +-export([generate_client_shares/1, + generate_server_share/1, + add_zero_padding/2, + encrypt_ticket/3, + decrypt_ticket/3]). -compile(inline). @@ -1386,3 +1390,63 @@ add_zero_padding(Bin, PrimeSize) Bin; add_zero_padding(Bin, PrimeSize) -> add_zero_padding(<<0, Bin/binary>>, PrimeSize). + + +%% Functions for handling self-encrypted session tickets (TLS 1.3). +%% +encrypt_ticket(#stateless_ticket{ + hash = Hash, + pre_shared_key = PSK, + ticket_age_add = TicketAgeAdd, + lifetime = Lifetime, + timestamp = Timestamp + }, Shard, IV) -> + Plaintext = <<(ssl_cipher:hash_algorithm(Hash)):8,PSK/binary, + ?UINT64(TicketAgeAdd),?UINT32(Lifetime),?UINT32(Timestamp)>>, + encrypt_ticket_data(Plaintext, Shard, IV). + + +decrypt_ticket(CipherFragment, Shard, IV) -> + case decrypt_ticket_data(CipherFragment, Shard, IV) of + error -> + error; + Plaintext -> + <<?BYTE(HKDF),T/binary>> = Plaintext, + Hash = hash_algorithm(HKDF), + HashSize = hash_size(Hash), + <<PSK:HashSize/binary,?UINT64(TicketAgeAdd),?UINT32(Lifetime),?UINT32(Timestamp),_/binary>> = T, + #stateless_ticket{ + hash = Hash, + pre_shared_key = PSK, + ticket_age_add = TicketAgeAdd, + lifetime = Lifetime, + timestamp = Timestamp + } + end. + + +encrypt_ticket_data(Plaintext, Shard, IV) -> + AAD = additional_data(erlang:iolist_size(Plaintext) + 16), %% TagLen = 16 + {OTP, Key} = make_otp_key(Shard), + {Content, CipherTag} = crypto:crypto_one_time_aead(aes_256_gcm, Key, IV, Plaintext, AAD, 16, true), + <<Content/binary,CipherTag/binary,OTP/binary>>. + + +decrypt_ticket_data(CipherFragment, Shard, IV) -> + Size = byte_size(Shard), + AAD = additional_data(erlang:iolist_size(CipherFragment) - Size), + Len = byte_size(CipherFragment) - Size - 16, + <<Encrypted:Len/binary,CipherTag:16/binary,OTP:Size/binary>> = CipherFragment, + Key = crypto:exor(OTP, Shard), + crypto:crypto_one_time_aead(aes_256_gcm, Key, IV, Encrypted, AAD, CipherTag, false). + + +additional_data(Length) -> + <<"ticket",?UINT16(Length)>>. + + +make_otp_key(Shard) -> + Size = byte_size(Shard), + OTP = crypto:strong_rand_bytes(Size), + Key = crypto:exor(OTP, Shard), + {OTP, Key}. diff --git a/lib/ssl/src/ssl_cipher.hrl b/lib/ssl/src/ssl_cipher.hrl index 0fa5f66c49..150d37ef18 100644 --- a/lib/ssl/src/ssl_cipher.hrl +++ b/lib/ssl/src/ssl_cipher.hrl @@ -27,6 +27,16 @@ -ifndef(ssl_cipher). -define(ssl_cipher, true). +%%% Session tickets %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +-record(stateless_ticket, + { + hash, + pre_shared_key, + ticket_age_add, + lifetime, + timestamp + }). + %%% SSL cipher protocol %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -define(CHANGE_CIPHER_SPEC_PROTO, 1). % _PROTO to not clash with % SSL record protocol diff --git a/lib/ssl/src/ssl_connection.erl b/lib/ssl/src/ssl_connection.erl index 6789c2c23f..9ef66bfb26 100644 --- a/lib/ssl/src/ssl_connection.erl +++ b/lib/ssl/src/ssl_connection.erl @@ -178,24 +178,24 @@ socket_control(Connection, Socket, Pid, Transport) -> socket_control(Connection, Socket, Pid, Transport, undefined). %-------------------------------------------------------------------- --spec socket_control(tls_connection | dtls_connection, port(), [pid()], atom(), pid()| atom()) -> +-spec socket_control(tls_connection | dtls_connection, port(), [pid()], atom(), [pid()] | atom()) -> {ok, #sslsocket{}} | {error, reason()}. %%-------------------------------------------------------------------- socket_control(Connection, Socket, Pids, Transport, udp_listener) -> %% dtls listener process must have the socket control {ok, Connection:socket(Pids, Transport, Socket, undefined)}; -socket_control(tls_connection = Connection, Socket, [Pid|_] = Pids, Transport, ListenTracker) -> +socket_control(tls_connection = Connection, Socket, [Pid|_] = Pids, Transport, Trackers) -> case Transport:controlling_process(Socket, Pid) of ok -> - {ok, Connection:socket(Pids, Transport, Socket, ListenTracker)}; + {ok, Connection:socket(Pids, Transport, Socket, Trackers)}; {error, Reason} -> {error, Reason} end; -socket_control(dtls_connection = Connection, {_, Socket}, [Pid|_] = Pids, Transport, ListenTracker) -> +socket_control(dtls_connection = Connection, {_, Socket}, [Pid|_] = Pids, Transport, Trackers) -> case Transport:controlling_process(Socket, Pid) of ok -> - {ok, Connection:socket(Pids, Transport, Socket, ListenTracker)}; + {ok, Connection:socket(Pids, Transport, Socket, Trackers)}; {error, Reason} -> {error, Reason} end. @@ -350,29 +350,29 @@ handle_normal_shutdown(Alert, StateName, #state{static_env = #static_env{role = socket = Socket, transport_cb = Transport, protocol_cb = Connection, - tracker = Tracker}, + trackers = Trackers}, handshake_env = #handshake_env{renegotiation = {false, first}}, start_or_recv_from = StartFrom} = State) -> Pids = Connection:pids(State), - alert_user(Pids, Transport, Tracker,Socket, StartFrom, Alert, Role, StateName, Connection); + alert_user(Pids, Transport, Trackers, Socket, StartFrom, Alert, Role, StateName, Connection); handle_normal_shutdown(Alert, StateName, #state{static_env = #static_env{role = Role, socket = Socket, transport_cb = Transport, protocol_cb = Connection, - tracker = Tracker}, + trackers = Trackers}, connection_env = #connection_env{user_application = {_Mon, Pid}}, socket_options = Opts, start_or_recv_from = RecvFrom} = State) -> Pids = Connection:pids(State), - alert_user(Pids, Transport, Tracker, Socket, StateName, Opts, Pid, RecvFrom, Alert, Role, StateName, Connection). + alert_user(Pids, Transport, Trackers, Socket, StateName, Opts, Pid, RecvFrom, Alert, Role, StateName, Connection). handle_alert(#alert{level = ?FATAL} = Alert0, StateName, #state{static_env = #static_env{role = Role, socket = Socket, host = Host, port = Port, - tracker = Tracker, + trackers = Trackers, transport_cb = Transport, protocol_cb = Connection}, connection_env = #connection_env{user_application = {_Mon, Pid}}, @@ -385,7 +385,7 @@ handle_alert(#alert{level = ?FATAL} = Alert0, StateName, log_alert(LogLevel, Role, Connection:protocol_name(), StateName, Alert), Pids = Connection:pids(State), - alert_user(Pids, Transport, Tracker, Socket, StateName, Opts, Pid, From, Alert, Role, StateName, Connection), + alert_user(Pids, Transport, Trackers, Socket, StateName, Opts, Pid, From, Alert, Role, StateName, Connection), {stop, {shutdown, normal}, State}; handle_alert(#alert{level = ?WARNING, description = ?CLOSE_NOTIFY} = Alert, @@ -444,19 +444,22 @@ handle_alert(#alert{level = ?WARNING} = Alert, StateName, %%==================================================================== %% Data handling %%==================================================================== -passive_receive(State0 = #state{user_data_buffer = {_,BufferSize,_}}, StateName, Connection, StartTimerAction) -> +passive_receive(#state{user_data_buffer = {Front,BufferSize,Rear}, + %% Assert! Erl distribution uses active sockets + connection_env = #connection_env{erl_dist_handle = undefined}} + = State0, StateName, Connection, StartTimerAction) -> case BufferSize of 0 -> Connection:next_event(StateName, no_record, State0, StartTimerAction); _ -> - case read_application_data(<<>>, State0) of + case read_application_data(State0, Front, BufferSize, Rear) of {stop, _, _} = ShutdownError -> ShutdownError; {Record, State} -> case State#state.start_or_recv_from of undefined -> %% Cancel recv timeout as data has been delivered - Connection:next_event(StateName, Record, State, + Connection:next_event(StateName, Record, State, [{{timeout, recv}, infinity, timeout}]); _ -> Connection:next_event(StateName, Record, State, StartTimerAction) @@ -556,13 +559,13 @@ read_application_data_bin(State, Front0, BufferSize0, Rear0, SocketOpts0, RecvFr socket = Socket, protocol_cb = Connection, transport_cb = Transport, - tracker = Tracker}, + trackers = Trackers}, connection_env = #connection_env{user_application = {_Mon, Pid}}} = State, Buffer = iolist_to_binary([Bin0,Front0|lists:reverse(Rear0)]), deliver_packet_error( Connection:pids(State), Transport, Socket, SocketOpts0, - Buffer, Pid, RecvFrom, Tracker, Connection), + Buffer, Pid, RecvFrom, Trackers, Connection), {stop, {shutdown, normal}, State#state{socket_options = SocketOpts0, bytes_to_read = BytesToRead, start_or_recv_from = RecvFrom, @@ -576,12 +579,12 @@ read_application_data_deliver(State, Front, BufferSize, Rear, SocketOpts0, RecvF socket = Socket, protocol_cb = Connection, transport_cb = Transport, - tracker = Tracker}, + trackers = Trackers}, connection_env = #connection_env{user_application = {_Mon, Pid}}} = State, SocketOpts = deliver_app_data( - Connection:pids(State), Transport, Socket, SocketOpts0, Data, Pid, RecvFrom, Tracker, Connection), + Connection:pids(State), Transport, Socket, SocketOpts0, Data, Pid, RecvFrom, Trackers, Connection), if SocketOpts#socket_options.active =:= false -> %% Passive mode, wait for active once or recv @@ -821,7 +824,7 @@ init({call, From}, {start, {Opts, EmOpts}, Timeout}, ssl_options = OrigSSLOptions, socket_options = SockOpts} = State0, Connection) -> try - SslOpts = ssl:handle_options(Opts, OrigSSLOptions), + SslOpts = ssl:handle_options(Opts, Role, OrigSSLOptions), State = ssl_config(SslOpts, Role, State0), init({call, From}, {start, Timeout}, State#state{ssl_options = SslOpts, @@ -870,7 +873,7 @@ user_hello({call, From}, {handshake_continue, NewOptions, Timeout}, #state{static_env = #static_env{role = Role}, handshake_env = #handshake_env{hello = Hello}, ssl_options = Options0} = State0, _Connection) -> - Options = ssl:handle_options(NewOptions, Options0#{handshake => full}), + Options = ssl:handle_options(NewOptions, Role, Options0#{handshake => full}), State = ssl_config(Options, Role, State0), {next_state, hello, State#state{start_or_recv_from = From}, [{next_event, internal, Hello}, {{timeout, handshake}, Timeout, close}]}; @@ -965,7 +968,7 @@ certify(internal, #certificate{asn1_certificates = []}, ssl_options = #{verify := verify_peer, fail_if_no_peer_cert := true}} = State, _) -> - Alert = ?ALERT_REC(?FATAL,?HANDSHAKE_FAILURE), + Alert = ?ALERT_REC(?FATAL,?HANDSHAKE_FAILURE, no_client_certificate_provided), handle_own_alert(Alert, Version, ?FUNCTION_NAME, State); certify(internal, #certificate{asn1_certificates = []}, #state{static_env = #static_env{role = server}, @@ -1434,7 +1437,7 @@ handle_call({get_opts, OptTags}, From, _, handle_call({set_opts, Opts0}, From, StateName, #state{static_env = #static_env{socket = Socket, transport_cb = Transport, - tracker = Tracker}, + trackers = Trackers}, connection_env = #connection_env{user_application = {_Mon, Pid}}, socket_options = Opts1 @@ -1445,7 +1448,7 @@ handle_call({set_opts, Opts0}, From, StateName, send_user( Pid, format_passive( - Connection:pids(State0), Transport, Socket, Tracker, Connection)); + Connection:pids(State0), Transport, Socket, Trackers, Connection)); _ -> ok end, @@ -1488,19 +1491,20 @@ handle_info({ErrorTag, Socket, econnaborted}, StateName, socket = Socket, transport_cb = Transport, error_tag = ErrorTag, - tracker = Tracker, + trackers = Trackers, protocol_cb = Connection}, start_or_recv_from = StartFrom } = State) when StateName =/= connection -> Pids = Connection:pids(State), - alert_user(Pids, Transport, Tracker,Socket, + alert_user(Pids, Transport, Trackers,Socket, StartFrom, ?ALERT_REC(?FATAL, ?CLOSE_NOTIFY), Role, StateName, Connection), {stop, {shutdown, normal}, State}; handle_info({ErrorTag, Socket, Reason}, StateName, #state{static_env = #static_env{socket = Socket, - error_tag = ErrorTag}} = State) -> - Report = io_lib:format("SSL: Socket error: ~p ~n", [Reason]), - ?LOG_ERROR(Report), + error_tag = ErrorTag}, + ssl_options = #{log_level := Level}} = State) -> + ssl_logger:log(info, Level, #{description => "Socket error", + reason => [{error_tag, ErrorTag}, {description, Reason}]}, ?LOCATION), handle_normal_shutdown(?ALERT_REC(?FATAL, ?CLOSE_NOTIFY), StateName, State), {stop, {shutdown,normal}, State}; @@ -1528,9 +1532,11 @@ handle_info({'EXIT', Socket, Reason}, _StateName, #state{static_env = #static_en handle_info(allow_renegotiate, StateName, #state{handshake_env = HsEnv} = State) -> {next_state, StateName, State#state{handshake_env = HsEnv#handshake_env{allow_renegotiate = true}}}; -handle_info(Msg, StateName, #state{static_env = #static_env{socket = Socket, error_tag = Tag}} = State) -> - Report = io_lib:format("SSL: Got unexpected info: ~p ~n", [{Msg, Tag, Socket}]), - ?LOG_NOTICE(Report), +handle_info(Msg, StateName, #state{static_env = #static_env{socket = Socket, error_tag = ErrorTag}, + ssl_options = #{log_level := Level}} = State) -> + ssl_logger:log(notice, Level, #{description => "Unexpected INFO message", + reason => [{message, Msg}, {socket, Socket}, + {error_tag, ErrorTag}]}, ?LOCATION), {next_state, StateName, State}. %%==================================================================== @@ -1616,7 +1622,8 @@ send_alert(Alert, _, #state{static_env = #static_env{protocol_cb = Connection}} Connection:send_alert(Alert, State). connection_info(#state{static_env = #static_env{protocol_cb = Connection}, - handshake_env = #handshake_env{sni_hostname = SNIHostname}, + handshake_env = #handshake_env{sni_hostname = SNIHostname, + resumption = Resumption}, session = #session{session_id = SessionId, cipher_suite = CipherSuite, ecc = ECCCurve}, connection_env = #connection_env{negotiated_version = {_,_} = Version}, @@ -1633,6 +1640,7 @@ connection_info(#state{static_env = #static_env{protocol_cb = Connection}, end, [{protocol, RecordCB:protocol_version(Version)}, {session_id, SessionId}, + {session_resumption, Resumption}, {cipher_suite, ssl_cipher_format:suite_legacy(CipherSuiteDef)}, {selected_cipher_suite, CipherSuiteDef}, {sni_hostname, SNIHostname} | CurveInfo] ++ ssl_options_list(Opts). @@ -2863,12 +2871,12 @@ decode_packet(Type, Buffer, PacketOpts) -> deliver_app_data( CPids, Transport, Socket, #socket_options{active=Active, packet=Type} = SOpts, - Data, Pid, From, Tracker, Connection) -> + Data, Pid, From, Trackers, Connection) -> %% send_or_reply( Active, Pid, From, format_reply( - CPids, Transport, Socket, SOpts, Data, Tracker, Connection)), + CPids, Transport, Socket, SOpts, Data, Trackers, Connection)), SO = case Data of {P, _, _, _} @@ -2889,7 +2897,7 @@ deliver_app_data( send_user( Pid, format_passive( - CPids, Transport, Socket, Tracker, Connection)), + CPids, Transport, Socket, Trackers, Connection)), SO#socket_options{active=false}; N when is_integer(N) -> SO#socket_options{active=N - 1}; @@ -2901,20 +2909,20 @@ format_reply(_, _, _,#socket_options{active = false, mode = Mode, packet = Packe header = Header}, Data, _, _) -> {ok, do_format_reply(Mode, Packet, Header, Data)}; format_reply(CPids, Transport, Socket, #socket_options{active = _, mode = Mode, packet = Packet, - header = Header}, Data, Tracker, Connection) -> - {ssl, Connection:socket(CPids, Transport, Socket, Tracker), + header = Header}, Data, Trackers, Connection) -> + {ssl, Connection:socket(CPids, Transport, Socket, Trackers), do_format_reply(Mode, Packet, Header, Data)}. deliver_packet_error(CPids, Transport, Socket, - SO= #socket_options{active = Active}, Data, Pid, From, Tracker, Connection) -> + SO= #socket_options{active = Active}, Data, Pid, From, Trackers, Connection) -> send_or_reply(Active, Pid, From, format_packet_error(CPids, - Transport, Socket, SO, Data, Tracker, Connection)). + Transport, Socket, SO, Data, Trackers, Connection)). format_packet_error(_, _, _,#socket_options{active = false, mode = Mode}, Data, _, _) -> {error, {invalid_packet, do_format_reply(Mode, raw, 0, Data)}}; format_packet_error(CPids, Transport, Socket, #socket_options{active = _, mode = Mode}, - Data, Tracker, Connection) -> - {ssl_error, Connection:socket(CPids, Transport, Socket, Tracker), + Data, Trackers, Connection) -> + {ssl_error, Connection:socket(CPids, Transport, Socket, Trackers), {invalid_packet, do_format_reply(Mode, raw, 0, Data)}}. do_format_reply(binary, _, N, Data) when N > 0 -> % Header mode @@ -2929,8 +2937,8 @@ do_format_reply(list, Packet, _, Data) do_format_reply(list, _,_, Data) -> binary_to_list(Data). -format_passive(CPids, Transport, Socket, Tracker, Connection) -> - {ssl_passive, Connection:socket(CPids, Transport, Socket, Tracker)}. +format_passive(CPids, Transport, Socket, Trackers, Connection) -> + {ssl_passive, Connection:socket(CPids, Transport, Socket, Trackers)}. header(0, <<>>) -> <<>>; @@ -2955,13 +2963,13 @@ send_user(Pid, Msg) -> Pid ! Msg, ok. -alert_user(Pids, Transport, Tracker, Socket, connection, Opts, Pid, From, Alert, Role, StateName, Connection) -> - alert_user(Pids, Transport, Tracker, Socket, Opts#socket_options.active, Pid, From, Alert, Role, StateName, Connection); -alert_user(Pids, Transport, Tracker, Socket,_, _, _, From, Alert, Role, StateName, Connection) -> - alert_user(Pids, Transport, Tracker, Socket, From, Alert, Role, StateName, Connection). +alert_user(Pids, Transport, Trackers, Socket, connection, Opts, Pid, From, Alert, Role, StateName, Connection) -> + alert_user(Pids, Transport, Trackers, Socket, Opts#socket_options.active, Pid, From, Alert, Role, StateName, Connection); +alert_user(Pids, Transport, Trackers, Socket,_, _, _, From, Alert, Role, StateName, Connection) -> + alert_user(Pids, Transport, Trackers, Socket, From, Alert, Role, StateName, Connection). -alert_user(Pids, Transport, Tracker, Socket, From, Alert, Role, StateName, Connection) -> - alert_user(Pids, Transport, Tracker, Socket, false, no_pid, From, Alert, Role, StateName, Connection). +alert_user(Pids, Transport, Trackers, Socket, From, Alert, Role, StateName, Connection) -> + alert_user(Pids, Transport, Trackers, Socket, false, no_pid, From, Alert, Role, StateName, Connection). alert_user(_, _, _, _, false = Active, Pid, From, Alert, Role, StateName, Connection) when From =/= undefined -> %% If there is an outstanding ssl_accept | recv @@ -2969,24 +2977,28 @@ alert_user(_, _, _, _, false = Active, Pid, From, Alert, Role, StateName, Conne %% send the appropriate error message. ReasonCode = ssl_alert:reason_code(Alert, Role, Connection:protocol_name(), StateName), send_or_reply(Active, Pid, From, {error, ReasonCode}); -alert_user(Pids, Transport, Tracker, Socket, Active, Pid, From, Alert, Role, StateName, Connection) -> +alert_user(Pids, Transport, Trackers, Socket, Active, Pid, From, Alert, Role, StateName, Connection) -> case ssl_alert:reason_code(Alert, Role, Connection:protocol_name(), StateName) of closed -> send_or_reply(Active, Pid, From, - {ssl_closed, Connection:socket(Pids, Transport, Socket, Tracker)}); + {ssl_closed, Connection:socket(Pids, Transport, Socket, Trackers)}); ReasonCode -> send_or_reply(Active, Pid, From, - {ssl_error, Connection:socket(Pids, Transport, Socket, Tracker), ReasonCode}) + {ssl_error, Connection:socket(Pids, Transport, Socket, Trackers), ReasonCode}) end. log_alert(Level, Role, ProtocolName, StateName, #alert{role = Role} = Alert) -> - Txt = ssl_alert:own_alert_txt(Alert), - Report = ssl_alert:alert_txt(ProtocolName, Role, StateName, Txt), - ssl_logger:notice(Level, Report); -log_alert(Level, Role, ProtocolName, StateName, Alert) -> - Txt = ssl_alert:alert_txt(Alert), - Report = ssl_alert:alert_txt(ProtocolName, Role, StateName, Txt), - ssl_logger:notice(Level, Report). + ssl_logger:log(notice, Level, #{protocol => ProtocolName, + role => Role, + statename => StateName, + alert => Alert, + alerter => own}, Alert#alert.where); +log_alert(Level, Role, ProtocolName, StateName, Alert) -> + ssl_logger:log(notice, Level, #{protocol => ProtocolName, + role => Role, + statename => StateName, + alert => Alert, + alerter => peer}, Alert#alert.where). invalidate_session(client, Host, Port, Session) -> ssl_manager:invalidate_session(Host, Port, Session); @@ -3042,7 +3054,7 @@ update_ssl_options_from_sni(#{sni_fun := SNIFun, undefined -> undefined; _ -> - ssl:handle_options(SSLOption, OrigSSLOptions) + ssl:handle_options(SSLOption, server, OrigSSLOptions) end. new_emulated([], EmOpts) -> diff --git a/lib/ssl/src/ssl_connection.hrl b/lib/ssl/src/ssl_connection.hrl index be94fd05bb..284ded64d8 100644 --- a/lib/ssl/src/ssl_connection.hrl +++ b/lib/ssl/src/ssl_connection.hrl @@ -50,9 +50,10 @@ crl_db :: term(), file_ref_db :: db_handle(), cert_db_ref :: certdb_ref() | 'undefined', - tracker :: pid() | 'undefined' %% Tracker process for listen socket + trackers :: [{atom(), pid()}] | 'undefined' %% Tracker process for listen socket }). + -record(handshake_env, { client_hello_version :: ssl_record:ssl_version() | 'undefined', unprocessed_handshake_events = 0 :: integer(), @@ -60,6 +61,7 @@ | 'undefined', expecting_finished = false ::boolean(), renegotiation :: undefined | {boolean(), From::term() | internal | peer}, + resumption = false :: boolean(), %% TLS 1.3 allow_renegotiate = true ::boolean(), %% Ext handling hello, %%:: #client_hello{} | #server_hello{} @@ -77,7 +79,8 @@ srp_params :: #srp_user{} | secret_printout() | 'undefined', public_key_info :: ssl_handshake:public_key_info() | 'undefined', premaster_secret :: binary() | secret_printout() | 'undefined', - server_psk_identity :: binary() | 'undefined' % server psk identity hint + server_psk_identity :: binary() | 'undefined', % server psk identity hint + ticket_seed }). -record(connection_env, { diff --git a/lib/ssl/src/ssl_connection_sup.erl b/lib/ssl/src/ssl_connection_sup.erl index 934dd39df5..d43254d23a 100644 --- a/lib/ssl/src/ssl_connection_sup.erl +++ b/lib/ssl/src/ssl_connection_sup.erl @@ -45,57 +45,31 @@ start_link() -> init([]) -> - TLSConnetionManager = tls_connection_manager_child_spec(), - %% Handles emulated options so that they inherited by the accept - %% socket, even when setopts is performed on the listen socket - ListenOptionsTracker = listen_options_tracker_child_spec(), - - DTLSConnetionManager = dtls_connection_manager_child_spec(), - DTLSListeners = dtls_listeners_spec(), + TLSSup = tls_sup_child_spec(), + DTLSSup = dtls_sup_child_spec(), - {ok, {{one_for_one, 10, 3600}, [TLSConnetionManager, - ListenOptionsTracker, - DTLSConnetionManager, - DTLSListeners - ]}}. + {ok, {{one_for_one, 10, 3600}, [TLSSup, DTLSSup]}}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -tls_connection_manager_child_spec() -> - Name = tls_connection, - StartFunc = {tls_connection_sup, start_link, []}, +tls_sup_child_spec() -> + Name = tls_sup, + StartFunc = {tls_sup, start_link, []}, Restart = permanent, Shutdown = 4000, - Modules = [tls_connection_sup], + Modules = [tls_sup], Type = supervisor, {Name, StartFunc, Restart, Shutdown, Type, Modules}. -dtls_connection_manager_child_spec() -> - Name = dtls_connection, - StartFunc = {dtls_connection_sup, start_link, []}, +dtls_sup_child_spec() -> + Name = dtls_sup, + StartFunc = {dtls_sup, start_link, []}, Restart = permanent, Shutdown = 4000, - Modules = [dtls_connection_sup], - Type = supervisor, - {Name, StartFunc, Restart, Shutdown, Type, Modules}. - -listen_options_tracker_child_spec() -> - Name = tls_socket, - StartFunc = {ssl_listen_tracker_sup, start_link, []}, - Restart = permanent, - Shutdown = 4000, - Modules = [tls_socket], + Modules = [dtls_sup], Type = supervisor, {Name, StartFunc, Restart, Shutdown, Type, Modules}. -dtls_listeners_spec() -> - Name = dtls_listener, - StartFunc = {dtls_listener_sup, start_link, []}, - Restart = permanent, - Shutdown = 4000, - Modules = [], - Type = supervisor, - {Name, StartFunc, Restart, Shutdown, Type, Modules}. diff --git a/lib/ssl/src/ssl_crl_cache.erl b/lib/ssl/src/ssl_crl_cache.erl index 841620ce57..2dc538c251 100644 --- a/lib/ssl/src/ssl_crl_cache.erl +++ b/lib/ssl/src/ssl_crl_cache.erl @@ -46,6 +46,12 @@ lookup(#'DistributionPoint'{distributionPoint = {fullName, Names}}, lookup(_,_,_) -> not_available. +select(GenNames, CRLDbHandle) when is_list(GenNames) -> + lists:flatmap(fun({directoryName, Issuer}) -> + select(Issuer, CRLDbHandle); + (_) -> + [] + end, GenNames); select(Issuer, {{_Cache, Mapping},_}) -> case ssl_pkix_db:lookup(Issuer, Mapping) of undefined -> diff --git a/lib/ssl/src/ssl_crl_cache_api.erl b/lib/ssl/src/ssl_crl_cache_api.erl index 8a750b3929..0c7842dc4b 100644 --- a/lib/ssl/src/ssl_crl_cache_api.erl +++ b/lib/ssl/src/ssl_crl_cache_api.erl @@ -23,13 +23,16 @@ -module(ssl_crl_cache_api). -include_lib("public_key/include/public_key.hrl"). --export_type([dist_point/0, crl_cache_ref/0]). +-export_type([dist_point/0, crl_cache_ref/0, logger_info/0]). -type crl_cache_ref() :: any(). -type issuer_name() :: {rdnSequence,[#'AttributeTypeAndValue'{}]}. -type dist_point() :: #'DistributionPoint'{}. +-type logger_info() :: {logger:level(), Report::#{description => string(), reason => term()}, logger:metadata()}. - --callback lookup(dist_point(), issuer_name(), crl_cache_ref()) -> not_available | [public_key:der_encoded()]. --callback select(issuer_name(), crl_cache_ref()) -> [public_key:der_encoded()]. --callback fresh_crl(dist_point(), public_key:der_encoded()) -> public_key:der_encoded(). +-callback lookup(dist_point(), issuer_name(), crl_cache_ref()) -> not_available | [public_key:der_encoded()] | + {{logger, logger_info()}, [public_key:der_encoded()]}. +-callback select(issuer_name() | list(), crl_cache_ref()) -> [public_key:der_encoded()] | + {logger, logger_info(), [public_key:der_encoded()]}. +-callback fresh_crl(dist_point(), public_key:der_encoded()) -> public_key:der_encoded() | + {logger, logger_info(), public_key:der_encoded()}. diff --git a/lib/ssl/src/ssl_crl_hash_dir.erl b/lib/ssl/src/ssl_crl_hash_dir.erl index 9478ff9b78..635b82ed1e 100644 --- a/lib/ssl/src/ssl_crl_hash_dir.erl +++ b/lib/ssl/src/ssl_crl_hash_dir.erl @@ -27,72 +27,65 @@ -export([lookup/3, select/2, fresh_crl/2]). lookup(#'DistributionPoint'{cRLIssuer = CRLIssuer} = DP, CertIssuer, CRLDbInfo) -> - Issuer = - case CRLIssuer of - asn1_NOVALUE -> - %% If the distribution point extension doesn't - %% indicate a CRL issuer, use the certificate issuer. - CertIssuer; - _ -> - CRLIssuer - end, - %% Find all CRLs for this issuer, and return those that match the - %% given distribution point. - AllCRLs = select(Issuer, CRLDbInfo), - lists:filter(fun(DER) -> - public_key:pkix_match_dist_point(DER, DP) - end, AllCRLs). + case CRLIssuer of + asn1_NOVALUE -> + %% If the distribution point extension doesn't + %% indicate a CRL issuer, use the certificate issuer. + select(CertIssuer, CRLDbInfo); + _ -> + CRLs = select(CRLIssuer, CRLDbInfo), + lists:filter(fun(DER) -> + public_key:pkix_match_dist_point(DER, DP) + end, CRLs) + end. fresh_crl(#'DistributionPoint'{}, CurrentCRL) -> CurrentCRL. -select(Issuer, {_DbHandle, [{dir, Dir}]}) -> - case find_crls(Issuer, Dir) of - [_|_] = DERs -> - DERs; - [] -> - %% That's okay, just report that we didn't find any CRL. - %% If the crl_check setting is best_effort, ssl_handshake - %% is happy with that, but if it's true, this is an error. - []; +select({rdnSequence, _} = Issuer, DbHandle) -> + select([{directoryName, Issuer}], DbHandle); +select([], _) -> + []; +select([{directoryName, Issuer} | _], {_DbHandle, [{dir, Dir}]}) -> + case find_crls(public_key:pkix_normalize_name(Issuer), Dir) of + {#{reason := []}, DERs} -> + DERs; + {#{reason := [_|_]} = Report, DERs} -> + {logger, {notice, Report, ?LOCATION}, DERs}; {error, Error} -> - ?LOG_ERROR( - [{cannot_find_crl, Error}, - {dir, Dir}, - {module, ?MODULE}, - {line, ?LINE}]), - [] - end. + {logger, {error, #{description => "CRL retrival", + reason => [{cannot_find_crl, Error}, + {dir, Dir}]}, ?LOCATION}, []} + end; +select([_ | Rest], CRLDbInfo) -> + select(Rest, CRLDbInfo). find_crls(Issuer, Dir) -> case filelib:is_dir(Dir) of true -> Hash = public_key:short_name_hash(Issuer), - find_crls(Issuer, Hash, Dir, 0, []); + find_crls(Issuer, Hash, Dir, 0, [], #{description => "CRL file traversal", + reason => []}); false -> {error, not_a_directory} end. -find_crls(Issuer, Hash, Dir, N, Acc) -> +find_crls(Issuer, Hash, Dir, N, Acc, #{reason := Reason} = Report) -> Filename = filename:join(Dir, Hash ++ ".r" ++ integer_to_list(N)), case file:read_file(Filename) of {error, enoent} -> - Acc; - {ok, Bin} -> + {Report, Acc}; + {ok, Bin} -> try maybe_parse_pem(Bin) of DER when is_binary(DER) -> %% Found one file. Let's see if there are more. - find_crls(Issuer, Hash, Dir, N + 1, [DER] ++ Acc) + find_crls(Issuer, Hash, Dir, N + 1, [DER] ++ Acc, Report) catch error:Error -> %% Something is wrong with the file. Report %% it, and try the next one. - ?LOG_ERROR( - [{crl_parse_error, Error}, - {filename, Filename}, - {module, ?MODULE}, - {line, ?LINE}]), - find_crls(Issuer, Hash, Dir, N + 1, Acc) + find_crls(Issuer, Hash, Dir, N + 1, Acc, Report#{reason => [{{crl_parse_error, Error}, + {filename, Filename}} | Reason]}) end end. diff --git a/lib/ssl/src/ssl_handshake.erl b/lib/ssl/src/ssl_handshake.erl index 1c8a2ca452..ecbe905d28 100644 --- a/lib/ssl/src/ssl_handshake.erl +++ b/lib/ssl/src/ssl_handshake.erl @@ -72,7 +72,7 @@ premaster_secret/2, premaster_secret/3, premaster_secret/4]). %% Extensions handling --export([client_hello_extensions/6, +-export([client_hello_extensions/7, handle_client_hello_extensions/9, %% Returns server hello extensions handle_server_hello_extensions/9, select_curve/2, select_curve/3, select_hashsign/4, select_hashsign/5, @@ -82,7 +82,7 @@ -export([get_cert_params/1, server_name/3, - validation_fun_and_state/9, + validation_fun_and_state/10, handle_path_validation_error/7]). %%==================================================================== @@ -343,12 +343,13 @@ next_protocol(SelectedProtocol) -> %%-------------------------------------------------------------------- certify(#certificate{asn1_certificates = ASN1Certs}, CertDbHandle, CertDbRef, #{server_name_indication := ServerNameIndication, - partial_chain := PartialChain, - verify_fun := VerifyFun, - customize_hostname_check := CustomizeHostnameCheck, - crl_check := CrlCheck, - depth := Depth} = Opts, CRLDbHandle, Role, Host) -> - + partial_chain := PartialChain, + verify_fun := VerifyFun, + customize_hostname_check := CustomizeHostnameCheck, + crl_check := CrlCheck, + log_level := Level, + depth := Depth} = Opts, CRLDbHandle, Role, Host) -> + ServerName = server_name(ServerNameIndication, Host, Role), [PeerCert | ChainCerts ] = ASN1Certs, try @@ -358,7 +359,7 @@ certify(#certificate{asn1_certificates = ASN1Certs}, CertDbHandle, CertDbRef, ValidationFunAndState = validation_fun_and_state(VerifyFun, Role, CertDbHandle, CertDbRef, ServerName, CustomizeHostnameCheck, - CrlCheck, CRLDbHandle, CertPath), + CrlCheck, CRLDbHandle, CertPath, Level), Options = [{max_path_length, Depth}, {verify_fun, ValidationFunAndState}], case public_key:pkix_path_validation(TrustedCert, CertPath, Options) of @@ -369,7 +370,7 @@ certify(#certificate{asn1_certificates = ASN1Certs}, CertDbHandle, CertDbRef, CertDbHandle, CertDbRef) end catch - error:{badmatch,{error, {asn1, Asn1Reason}}} -> + error:{_,{error, {asn1, Asn1Reason}}} -> %% ASN-1 decode of certificate somehow failed ?ALERT_REC(?FATAL, ?CERTIFICATE_UNKNOWN, {failed_to_decode_certificate, Asn1Reason}); error:OtherReason -> @@ -724,12 +725,16 @@ encode_extensions([#psk_key_exchange_modes{ke_modes = KEModes0} | Rest], Acc) -> encode_extensions([#pre_shared_key_client_hello{ offered_psks = #offered_psks{ identities = Identities0, - binders = Binders0} = PSKs} | Rest], Acc) -> + binders = Binders0} = _PSKs} | Rest], Acc) -> Identities = encode_psk_identities(Identities0), Binders = encode_psk_binders(Binders0), Len = byte_size(Identities) + byte_size(Binders), - encode_extensions(Rest, <<?UINT16(?PRE_SHARED_KEY_EXT), - ?UINT16(Len), Identities/binary, Binders/binary, Acc/binary>>); + %% The "pre_shared_key" extension MUST be the last extension in the + %% ClientHello (this facilitates implementation as described below). + %% Servers MUST check that it is the last extension and otherwise fail + %% the handshake with an "illegal_parameter" alert. + encode_extensions(Rest, <<Acc/binary,?UINT16(?PRE_SHARED_KEY_EXT), + ?UINT16(Len), Identities/binary, Binders/binary>>); encode_extensions([#pre_shared_key_server_hello{selected_identity = Identity} | Rest], Acc) -> encode_extensions(Rest, <<?UINT16(?PRE_SHARED_KEY_EXT), ?UINT16(2), ?UINT16(Identity), Acc/binary>>). @@ -952,9 +957,9 @@ select_session(SuggestedSessionId, CipherSuites, HashSigns, Compressions, Port, Session, Version, #{ciphers := UserSuites, honor_cipher_order := HonorCipherOrder} = SslOpts, Cache, CacheCb, Cert) -> - {SessionId, Resumed} = ssl_session:server_id(Port, SuggestedSessionId, - SslOpts, Cert, - Cache, CacheCb), + {SessionId, Resumed} = ssl_session:server_select_session(Version, Port, SuggestedSessionId, + SslOpts, Cert, + Cache, CacheCb), case Resumed of undefined -> Suites = available_suites(Cert, UserSuites, Version, HashSigns, ECCCurve0), @@ -1069,10 +1074,11 @@ premaster_secret(EncSecret, #{algorithm := rsa} = Engine) -> %%==================================================================== %% Extensions handling %%==================================================================== -client_hello_extensions(Version, CipherSuites, SslOpts, ConnectionStates, Renegotiation, KeyShare) -> +client_hello_extensions(Version, CipherSuites, SslOpts, ConnectionStates, Renegotiation, KeyShare, + TicketData) -> HelloExtensions0 = add_tls12_extensions(Version, SslOpts, ConnectionStates, Renegotiation), HelloExtensions1 = add_common_extensions(Version, HelloExtensions0, CipherSuites, SslOpts), - maybe_add_tls13_extensions(Version, HelloExtensions1, SslOpts, KeyShare). + maybe_add_tls13_extensions(Version, HelloExtensions1, SslOpts, KeyShare, TicketData). add_tls12_extensions(_Version, @@ -1129,14 +1135,16 @@ maybe_add_tls13_extensions({3,4}, HelloExtensions0, #{signature_algs_cert := SignatureSchemes, versions := SupportedVersions}, - KeyShare) -> - HelloExtensions = + KeyShare, + TicketData) -> + HelloExtensions1 = HelloExtensions0#{client_hello_versions => #client_hello_versions{versions = SupportedVersions}, signature_algs_cert => signature_algs_cert(SignatureSchemes)}, - maybe_add_key_share(HelloExtensions, KeyShare); -maybe_add_tls13_extensions(_, HelloExtensions, _, _) -> + HelloExtensions = maybe_add_key_share(HelloExtensions1, KeyShare), + maybe_add_pre_shared_key(HelloExtensions, TicketData); +maybe_add_tls13_extensions(_, HelloExtensions, _, _, _) -> HelloExtensions. @@ -1181,6 +1189,43 @@ maybe_add_key_share(HelloExtensions, KeyShare) -> HelloExtensions#{key_share => #key_share_client_hello{ client_shares = ClientShares}}. + +maybe_add_pre_shared_key(HelloExtensions, undefined) -> + HelloExtensions; +maybe_add_pre_shared_key(HelloExtensions, TicketData) -> + {Identities, Binders} = get_identities_binders(TicketData), + + %% A client MUST provide a "psk_key_exchange_modes" extension if it + %% offers a "pre_shared_key" extension. + HelloExtensions#{pre_shared_key => + #pre_shared_key_client_hello{ + offered_psks = + #offered_psks{ + identities = Identities, + binders = Binders}}, + psk_key_exchange_modes => + #psk_key_exchange_modes{ + ke_modes = [psk_ke, psk_dhe_ke]}}. + + +get_identities_binders(TicketData) -> + get_identities_binders(TicketData, {[], []}, 0). +%% +get_identities_binders([], {Identities, Binders}, _) -> + {lists:reverse(Identities), lists:reverse(Binders)}; +get_identities_binders([{Key, _, Identity, _, _, HKDF}|T], {I0, B0}, N) -> + %% Use dummy binder for proper calculation of packet size when creating + %% the real binder value. + Binder = dummy_binder(HKDF), + %% Store ticket position in identities + tls_client_ticket_store:update_ticket(Key, N), + get_identities_binders(T, {[Identity|I0], [Binder|B0]}, N + 1). + + +dummy_binder(HKDF) -> + binary:copy(<<0>>, ssl_cipher:hash_size(HKDF)). + + add_server_share(server_hello, Extensions, KeyShare) -> #key_share_server_hello{server_share = ServerShare0} = KeyShare, %% Keep only public keys @@ -1216,6 +1261,7 @@ kse_remove_private_key(#key_share_entry{ group = Group, key_exchange = PublicKey}. + signature_algs_ext(undefined) -> undefined; signature_algs_ext(SignatureSchemes0) -> @@ -1520,7 +1566,13 @@ extension_value(#key_share_server_hello{server_share = ServerShare}) -> extension_value(#client_hello_versions{versions = Versions}) -> Versions; extension_value(#server_hello_selected_version{selected_version = SelectedVersion}) -> - SelectedVersion. + SelectedVersion; +extension_value(#pre_shared_key_client_hello{offered_psks = PSKs}) -> + PSKs; +extension_value(#pre_shared_key_server_hello{selected_identity = SelectedIdentity}) -> + SelectedIdentity; +extension_value(#psk_key_exchange_modes{ke_modes = Modes}) -> + Modes. %%-------------------------------------------------------------------- @@ -1581,7 +1633,8 @@ certificate_authorities_from_db(_CertDbHandle, {extracted, CertDbData}) -> %%-------------Handle handshake messages -------------------------------- validation_fun_and_state({Fun, UserState0}, Role, CertDbHandle, CertDbRef, - ServerNameIndication, CustomizeHostCheck, CRLCheck, CRLDbHandle, CertPath) -> + ServerNameIndication, CustomizeHostCheck, CRLCheck, + CRLDbHandle, CertPath, LogLevel) -> {fun(OtpCert, {extension, _} = Extension, {SslState, UserState}) -> case ssl_certificate:validate(OtpCert, Extension, @@ -1590,17 +1643,18 @@ validation_fun_and_state({Fun, UserState0}, Role, CertDbHandle, CertDbRef, {valid, {NewSslState, UserState}}; {fail, Reason} -> apply_user_fun(Fun, OtpCert, Reason, UserState, - SslState, CertPath); + SslState, CertPath, LogLevel); {unknown, _} -> apply_user_fun(Fun, OtpCert, - Extension, UserState, SslState, CertPath) + Extension, UserState, SslState, CertPath, LogLevel) end; (OtpCert, VerifyResult, {SslState, UserState}) -> apply_user_fun(Fun, OtpCert, VerifyResult, UserState, - SslState, CertPath) + SslState, CertPath, LogLevel) end, {{Role, CertDbHandle, CertDbRef, {ServerNameIndication, CustomizeHostCheck}, CRLCheck, CRLDbHandle}, UserState0}}; validation_fun_and_state(undefined, Role, CertDbHandle, CertDbRef, - ServerNameIndication, CustomizeHostCheck, CRLCheck, CRLDbHandle, CertPath) -> + ServerNameIndication, CustomizeHostCheck, CRLCheck, + CRLDbHandle, CertPath, LogLevel) -> {fun(OtpCert, {extension, _} = Extension, SslState) -> ssl_certificate:validate(OtpCert, Extension, @@ -1608,7 +1662,7 @@ validation_fun_and_state(undefined, Role, CertDbHandle, CertDbRef, (OtpCert, VerifyResult, SslState) when (VerifyResult == valid) or (VerifyResult == valid_peer) -> case crl_check(OtpCert, CRLCheck, CertDbHandle, CertDbRef, - CRLDbHandle, VerifyResult, CertPath) of + CRLDbHandle, VerifyResult, CertPath, LogLevel) of valid -> ssl_certificate:validate(OtpCert, VerifyResult, @@ -1623,21 +1677,21 @@ validation_fun_and_state(undefined, Role, CertDbHandle, CertDbRef, end, {Role, CertDbHandle, CertDbRef, {ServerNameIndication, CustomizeHostCheck}, CRLCheck, CRLDbHandle}}. apply_user_fun(Fun, OtpCert, VerifyResult, UserState0, - {_, CertDbHandle, CertDbRef, _, CRLCheck, CRLDbHandle} = SslState, CertPath) when + {_, CertDbHandle, CertDbRef, _, CRLCheck, CRLDbHandle} = SslState, CertPath, LogLevel) when (VerifyResult == valid) or (VerifyResult == valid_peer) -> case Fun(OtpCert, VerifyResult, UserState0) of {Valid, UserState} when (Valid == valid) or (Valid == valid_peer) -> case crl_check(OtpCert, CRLCheck, CertDbHandle, CertDbRef, - CRLDbHandle, VerifyResult, CertPath) of + CRLDbHandle, VerifyResult, CertPath, LogLevel) of valid -> {Valid, {SslState, UserState}}; Result -> - apply_user_fun(Fun, OtpCert, Result, UserState, SslState, CertPath) + apply_user_fun(Fun, OtpCert, Result, UserState, SslState, CertPath, LogLevel) end; {fail, _} = Fail -> Fail end; -apply_user_fun(Fun, OtpCert, ExtensionOrError, UserState0, SslState, _CertPath) -> +apply_user_fun(Fun, OtpCert, ExtensionOrError, UserState0, SslState, _CertPath, _LogLevel) -> case Fun(OtpCert, ExtensionOrError, UserState0) of {Valid, UserState} when (Valid == valid) or (Valid == valid_peer)-> {Valid, {SslState, UserState}}; @@ -1651,40 +1705,44 @@ handle_path_validation_error({bad_cert, unknown_ca} = Reason, PeerCert, Chain, Opts, Options, CertDbHandle, CertsDbRef) -> handle_incomplete_chain(PeerCert, Chain, Opts, Options, CertDbHandle, CertsDbRef, Reason); handle_path_validation_error({bad_cert, invalid_issuer} = Reason, PeerCert, Chain0, - #{partial_chain := PartialChain} = Opts, Options, CertDbHandle, CertsDbRef) -> - case ssl_certificate:certificate_chain(PeerCert, CertDbHandle, CertsDbRef, Chain0) of - {ok, _, [PeerCert | Chain] = OrdedChain} when Chain =/= Chain0 -> %% Chain appaears to be unorded - {Trusted, Path} = ssl_certificate:trusted_cert_and_path(OrdedChain, + Opts, Options, CertDbHandle, CertsDbRef) -> + handle_unordered_chain(PeerCert, Chain0, Opts, Options, CertDbHandle, CertsDbRef, Reason); +handle_path_validation_error(Reason, _, _, _, _,_, _) -> + path_validation_alert(Reason). + +handle_incomplete_chain(PeerCert, Chain0, + #{partial_chain := PartialChain} = Opts, Options, CertDbHandle, CertsDbRef, Reason) -> + case ssl_certificate:certificate_chain(PeerCert, CertDbHandle, CertsDbRef) of + {ok, _, [PeerCert | _] = Chain} when Chain =/= Chain0 -> %% Chain candidate found + {Trusted, Path} = ssl_certificate:trusted_cert_and_path(Chain, CertDbHandle, CertsDbRef, PartialChain), case public_key:pkix_path_validation(Trusted, Path, Options) of {ok, {PublicKeyInfo,_}} -> {PeerCert, PublicKeyInfo}; {error, PathError} -> - handle_path_validation_error(PathError, PeerCert, Path, - Opts, Options, CertDbHandle, CertsDbRef) + handle_unordered_chain(PeerCert, Chain0, Opts, Options, CertDbHandle, CertsDbRef, PathError) end; _ -> - path_validation_alert(Reason) - end; -handle_path_validation_error(Reason, _, _, _, _,_, _) -> - path_validation_alert(Reason). + handle_unordered_chain(PeerCert, Chain0, Opts, Options, CertDbHandle, CertsDbRef, Reason) + end. -handle_incomplete_chain(PeerCert, Chain0, - #{partial_chain := PartialChain}, Options, CertDbHandle, CertsDbRef, PathError0) -> - case ssl_certificate:certificate_chain(PeerCert, CertDbHandle, CertsDbRef) of - {ok, _, [PeerCert | _] = Chain} when Chain =/= Chain0 -> %% Chain candidate found +handle_unordered_chain(PeerCert, Chain0, + #{partial_chain := PartialChain}, Options, CertDbHandle, CertsDbRef, Reason) -> + {ok, ExtractedCerts} = ssl_pkix_db:extract_trusted_certs({der, Chain0}), + case ssl_certificate:certificate_chain(PeerCert, CertDbHandle, ExtractedCerts, Chain0) of + {ok, _, Chain} when Chain =/= Chain0 -> %% Chain appaears to be unordered {Trusted, Path} = ssl_certificate:trusted_cert_and_path(Chain, CertDbHandle, CertsDbRef, PartialChain), case public_key:pkix_path_validation(Trusted, Path, Options) of - {ok, {PublicKeyInfo,_}} -> - {PeerCert, PublicKeyInfo}; + {ok, {PublicKeyInfo,_}} -> + {PeerCert, PublicKeyInfo}; {error, PathError} -> - path_validation_alert(PathError) + path_validation_alert(PathError) end; _ -> - path_validation_alert(PathError0) + path_validation_alert(Reason) end. path_validation_alert({bad_cert, cert_expired}) -> @@ -1705,7 +1763,7 @@ path_validation_alert({bad_cert, {revocation_status_undetermined, Details}}) -> path_validation_alert({bad_cert, selfsigned_peer}) -> ?ALERT_REC(?FATAL, ?BAD_CERTIFICATE); path_validation_alert({bad_cert, unknown_ca}) -> - ?ALERT_REC(?FATAL, ?UNKNOWN_CA); + ?ALERT_REC(?FATAL, ?UNKNOWN_CA); path_validation_alert(Reason) -> ?ALERT_REC(?FATAL, ?HANDSHAKE_FAILURE, Reason). @@ -1741,29 +1799,38 @@ bad_key(#'RSAPrivateKey'{}) -> bad_key(#'ECPrivateKey'{}) -> unacceptable_ecdsa_key. -crl_check(_, false, _,_,_, _, _) -> +crl_check(_, false, _,_,_, _, _, _) -> valid; -crl_check(_, peer, _, _,_, valid, _) -> %% Do not check CAs with this option. +crl_check(_, peer, _, _,_, valid, _, _) -> %% Do not check CAs with this option. valid; -crl_check(OtpCert, Check, CertDbHandle, CertDbRef, {Callback, CRLDbHandle}, _, CertPath) -> +crl_check(OtpCert, Check, CertDbHandle, CertDbRef, {Callback, CRLDbHandle}, _, CertPath, LogLevel) -> Options = [{issuer_fun, {fun(_DP, CRL, Issuer, DBInfo) -> ssl_crl:trusted_cert_and_path(CRL, Issuer, {CertPath, DBInfo}) end, {CertDbHandle, CertDbRef}}}, - {update_crl, fun(DP, CRL) -> Callback:fresh_crl(DP, CRL) end}, + {update_crl, fun(DP, CRL) -> + case Callback:fresh_crl(DP, CRL) of + {logger, LogInfo, Fresh} -> + handle_log(LogLevel, LogInfo), + Fresh; + Fresh -> + Fresh + end + end}, {undetermined_details, true} ], - case dps_and_crls(OtpCert, Callback, CRLDbHandle, ext) of + case dps_and_crls(OtpCert, Callback, CRLDbHandle, ext, LogLevel) of no_dps -> crl_check_same_issuer(OtpCert, Check, - dps_and_crls(OtpCert, Callback, CRLDbHandle, same_issuer), + dps_and_crls(OtpCert, Callback, CRLDbHandle, same_issuer, LogLevel), Options); DpsAndCRLs -> %% This DP list may be empty if relevant CRLs existed %% but could not be retrived, will result in {bad_cert, revocation_status_undetermined} case public_key:pkix_crls_validate(OtpCert, DpsAndCRLs, Options) of {bad_cert, {revocation_status_undetermined, _}} -> - crl_check_same_issuer(OtpCert, Check, dps_and_crls(OtpCert, Callback, - CRLDbHandle, same_issuer), Options); + crl_check_same_issuer(OtpCert, Check, + dps_and_crls(OtpCert, Callback, + CRLDbHandle, same_issuer, LogLevel), Options); Other -> Other end @@ -1779,21 +1846,27 @@ crl_check_same_issuer(OtpCert, best_effort, Dps, Options) -> crl_check_same_issuer(OtpCert, _, Dps, Options) -> public_key:pkix_crls_validate(OtpCert, Dps, Options). -dps_and_crls(OtpCert, Callback, CRLDbHandle, ext) -> +dps_and_crls(OtpCert, Callback, CRLDbHandle, ext, LogLevel) -> case public_key:pkix_dist_points(OtpCert) of [] -> no_dps; DistPoints -> Issuer = OtpCert#'OTPCertificate'.tbsCertificate#'OTPTBSCertificate'.issuer, - CRLs = distpoints_lookup(DistPoints, Issuer, Callback, CRLDbHandle), + CRLs = distpoints_lookup(DistPoints, Issuer, Callback, CRLDbHandle, LogLevel), dps_and_crls(DistPoints, CRLs, []) end; -dps_and_crls(OtpCert, Callback, CRLDbHandle, same_issuer) -> +dps_and_crls(OtpCert, Callback, CRLDbHandle, same_issuer, LogLevel) -> DP = #'DistributionPoint'{distributionPoint = {fullName, GenNames}} = public_key:pkix_dist_point(OtpCert), CRLs = lists:flatmap(fun({directoryName, Issuer}) -> - Callback:select(Issuer, CRLDbHandle); + case Callback:select(Issuer, CRLDbHandle) of + {logger, LogInfo, Return} -> + handle_log(LogLevel, LogInfo), + Return; + Return -> + Return + end; (_) -> [] end, GenNames), @@ -1805,9 +1878,9 @@ dps_and_crls([DP | Rest], CRLs, Acc) -> DpCRL = [{DP, {CRL, public_key:der_decode('CertificateList', CRL)}} || CRL <- CRLs], dps_and_crls(Rest, CRLs, DpCRL ++ Acc). -distpoints_lookup([],_, _, _) -> +distpoints_lookup([],_, _, _, _) -> []; -distpoints_lookup([DistPoint | Rest], Issuer, Callback, CRLDbHandle) -> +distpoints_lookup([DistPoint | Rest], Issuer, Callback, CRLDbHandle, LogLevel) -> Result = try Callback:lookup(DistPoint, Issuer, CRLDbHandle) catch @@ -1818,8 +1891,11 @@ distpoints_lookup([DistPoint | Rest], Issuer, Callback, CRLDbHandle) -> end, case Result of not_available -> - distpoints_lookup(Rest, Issuer, Callback, CRLDbHandle); - CRLs -> + distpoints_lookup(Rest, Issuer, Callback, CRLDbHandle, LogLevel); + {logger, LogInfo, CRLs} -> + handle_log(LogLevel, LogInfo), + CRLs; + CRLs -> CRLs end. @@ -2144,7 +2220,7 @@ encode_psk_identities([#psk_identity{ identity = Identity, obfuscated_ticket_age = Age}|T], Acc) -> IdLen = byte_size(Identity), - encode_psk_identities(T, <<Acc/binary,?UINT16(IdLen),Identity/binary,Age/binary>>). + encode_psk_identities(T, <<Acc/binary,?UINT16(IdLen),Identity/binary,?UINT32(Age)>>). encode_psk_binders(Binders) -> @@ -2613,7 +2689,7 @@ decode_psk_identities(Identities) -> %% decode_psk_identities(<<>>, Acc) -> lists:reverse(Acc); -decode_psk_identities(<<?UINT16(Len), Identity:Len/binary, Age:4/binary, Rest/binary>>, Acc) -> +decode_psk_identities(<<?UINT16(Len), Identity:Len/binary, ?UINT32(Age), Rest/binary>>, Acc) -> decode_psk_identities(Rest, [#psk_identity{ identity = Identity, obfuscated_ticket_age = Age}|Acc]). @@ -3202,3 +3278,6 @@ empty_extensions(_, server_hello) -> alpn => undefined, next_protocol_negotiation => undefined, ec_point_formats => undefined}. + +handle_log(Level, {LogLevel, ReportMap, Meta}) -> + ssl_logger:log(Level, LogLevel, ReportMap, Meta). diff --git a/lib/ssl/src/ssl_internal.hrl b/lib/ssl/src/ssl_internal.hrl index fc9f16a189..ceca206605 100644 --- a/lib/ssl/src/ssl_internal.hrl +++ b/lib/ssl/src/ssl_internal.hrl @@ -60,6 +60,7 @@ -define(CDR_MAGIC, "GIOP"). -define(CDR_HDR_SIZE, 12). -define(INTERNAL_ACTIVE_N, 100). +-define(DEPTH, 20). -define(DEFAULT_TIMEOUT, 5000). -define(NO_DIST_POINT, "http://dummy/no_distribution_point"). @@ -108,76 +109,91 @@ -define('24H_in_msec', 86400000). -define('24H_in_sec', 86400). --record(ssl_options, { - protocol :: tls | dtls | 'undefined', - versions :: [ssl_record:ssl_version()] | 'undefined', %% ssl_record:atom_version() in API - verify :: verify_none | verify_peer | 'undefined', - verify_fun, %%:: fun(CertVerifyErrors::term()) -> boolean(), - partial_chain :: fun() | 'undefined', - fail_if_no_peer_cert :: boolean() | 'undefined', - verify_client_once :: boolean() | 'undefined', - %% fun(Extensions, State, Verify, AccError) -> {Extensions, State, AccError} - validate_extensions_fun, - depth :: integer() | 'undefined', - certfile :: binary() | 'undefined', - cert :: public_key:der_encoded() | secret_printout() | 'undefined', - keyfile :: binary() | 'undefined', - key :: {'RSAPrivateKey' | 'DSAPrivateKey' | 'ECPrivateKey' | 'PrivateKeyInfo' | 'undefined', - public_key:der_encoded()} | map() %%map() -> ssl:key() how to handle dialyzer? - | secret_printout() | 'undefined', - password :: string() | secret_printout() | 'undefined', - cacerts :: [public_key:der_encoded()] | secret_printout() | 'undefined', - cacertfile :: binary() | 'undefined', - dh :: public_key:der_encoded() | secret_printout() | 'undefined', - dhfile :: binary() | secret_printout() | 'undefined', - user_lookup_fun, % server option, fun to lookup the user - psk_identity :: binary() | secret_printout() | 'undefined', - srp_identity, % client option {User, Password} - ciphers, % - %% Local policy for the server if it want's to reuse the session - %% or not. Defaluts to allways returning true. - %% fun(SessionId, PeerCert, Compression, CipherSuite) -> boolean() - reuse_session :: fun() | binary() | undefined, %% Server side is a fun() - %% If false sessions will never be reused, if true they - %% will be reused if possible. - reuse_sessions :: boolean() | save | 'undefined', %% Only client side can use value save - renegotiate_at, - secure_renegotiate, - client_renegotiation, - %% undefined if not hibernating, or number of ms of - %% inactivity after which ssl_connection will go into - %% hibernation - hibernate_after :: timeout() | 'undefined', - %% This option should only be set to true by inet_tls_dist - erl_dist = false :: boolean(), - alpn_advertised_protocols = undefined :: [binary()] | undefined, - alpn_preferred_protocols = undefined :: [binary()] | undefined, - next_protocols_advertised = undefined :: [binary()] | undefined, - next_protocol_selector = undefined, %% fun([binary()]) -> binary()) - log_level = notice :: atom(), - server_name_indication = undefined, - sni_hosts :: [{inet:hostname(), [tuple()]}] | 'undefined', - sni_fun :: function() | undefined, - %% Should the server prefer its own cipher order over the one provided by - %% the client? - honor_cipher_order = false :: boolean(), - padding_check = true :: boolean(), - %%Should we use 1/n-1 or 0/n splitting to mitigate BEAST, or disable - %%mitigation entirely? - beast_mitigation = one_n_minus_one :: one_n_minus_one | zero_n | disabled, - fallback = false :: boolean(), - crl_check :: boolean() | peer | best_effort | 'undefined', - crl_cache, - signature_algs, - signature_algs_cert, - eccs, - supported_groups, %% RFC 8422, RFC 8446 - honor_ecc_order :: boolean() | 'undefined', - max_handshake_size :: integer() | 'undefined', - handshake, - customize_hostname_check - %% , - %% save_session :: boolean() + +%% This map stores all supported options with default values and +%% list of dependencies: +%% #{<option> => {<default_value>, [<option>]}, +%% ...} +-define(RULES, + #{ + alpn_advertised_protocols => {undefined, [versions]}, + alpn_preferred_protocols => {undefined, [versions]}, + beast_mitigation => {one_n_minus_one, [versions]}, + cacertfile => {undefined, [versions, + verify_fun, + cacerts]}, + cacerts => {undefined, [versions]}, + cert => {undefined, [versions]}, + certfile => {<<>>, [versions]}, + ciphers => {[], [versions]}, + client_renegotiation => {undefined, [versions]}, + crl_cache => {{ssl_crl_cache, {internal, []}}, [versions]}, + crl_check => {false, [versions]}, + customize_hostname_check => {[], [versions]}, + depth => {1, [versions]}, + dh => {undefined, [versions]}, + dhfile => {undefined, [versions]}, + eccs => {undefined, [versions]}, + erl_dist => {false, [versions]}, + fail_if_no_peer_cert => {false, [versions]}, + fallback => {false, [versions]}, + handshake => {full, [versions]}, + hibernate_after => {infinity, [versions]}, + honor_cipher_order => {false, [versions]}, + honor_ecc_order => {undefined, [versions]}, + key => {undefined, [versions]}, + keyfile => {undefined, [versions, + certfile]}, + log_level => {notice, [versions]}, + max_handshake_size => {?DEFAULT_MAX_HANDSHAKE_SIZE, [versions]}, + next_protocol_selector => {undefined, [versions]}, + next_protocols_advertised => {undefined, [versions]}, + padding_check => {true, [versions]}, + partial_chain => {fun(_) -> unknown_ca end, [versions]}, + password => {"", [versions]}, + protocol => {tls, []}, + psk_identity => {undefined, [versions]}, + renegotiate_at => {?DEFAULT_RENEGOTIATE_AT, [versions]}, + reuse_session => {undefined, [versions]}, + reuse_sessions => {true, [versions]}, + anti_replay => {undefined, [versions, session_tickets]}, + secure_renegotiate => {true, [versions]}, + server_name_indication => {undefined, [versions]}, + session_tickets => {disabled, [versions]}, + signature_algs => {undefined, [versions]}, + signature_algs_cert => {undefined, [versions]}, + sni_fun => {undefined, [versions, + sni_hosts]}, + sni_hosts => {[], [versions]}, + srp_identity => {undefined, [versions]}, + supported_groups => {undefined, [versions]}, + use_ticket => {undefined, [versions]}, + user_lookup_fun => {undefined, [versions]}, + validate_extensions_fun => {undefined, [versions]}, + verify => {verify_none, [versions, + fail_if_no_peer_cert, + partial_chain, + verify_client_once]}, + verify_client_once => {false, [versions]}, + verify_fun => + { + {fun(_,{bad_cert, _}, UserState) -> + {valid, UserState}; + (_,{extension, #'Extension'{critical = true}}, UserState) -> + %% This extension is marked as critical, so + %% certificate verification should fail if we don't + %% understand the extension. However, this is + %% `verify_none', so let's accept it anyway. + {valid, UserState}; + (_,{extension, _}, UserState) -> + {unknown, UserState}; + (_, valid, UserState) -> + {valid, UserState}; + (_, valid_peer, UserState) -> + {valid, UserState} + end, []}, + [versions, verify]}, + versions => {[], [protocol]} }). -record(socket_options, @@ -191,7 +207,8 @@ -record(config, {ssl, %% SSL parameters inet_user, %% User set inet options - emulated, %% Emulated option list or "inherit_tracker" pid + emulated, %% Emulated option list or + trackers, dtls_handler, inet_ssl, %% inet options for internal ssl socket transport_info, %% Callback info diff --git a/lib/ssl/src/ssl_logger.erl b/lib/ssl/src/ssl_logger.erl index 514a4464bc..9ee0c23aaa 100644 --- a/lib/ssl/src/ssl_logger.erl +++ b/lib/ssl/src/ssl_logger.erl @@ -20,9 +20,10 @@ -module(ssl_logger). --export([debug/4, +-export([log/4, + debug/4, format/2, - notice/2]). + format/1]). -define(DEC2HEX(X), if ((X) >= 0) andalso ((X) =< 9) -> (X) + $0; @@ -31,6 +32,7 @@ -define(rec_info(T,R),lists:zip(record_info(fields,T),tl(tuple_to_list(R)))). +-include("ssl_internal.hrl"). -include("tls_record.hrl"). -include("ssl_cipher.hrl"). -include("ssl_internal.hrl"). @@ -40,31 +42,21 @@ -include_lib("kernel/include/logger.hrl"). %%------------------------------------------------------------------------- -%% External API +%% Internal API -- Stateful logging %%------------------------------------------------------------------------- -%% SSL log formatter -format(#{level:= _Level, msg:= {report, Msg}, meta:= _Meta}, _Config0) -> - #{direction := Direction, - protocol := Protocol, - message := Content} = Msg, - case Protocol of - 'record' -> - BinMsg = - case Content of - #ssl_tls{} -> - [tls_record:build_tls_record(Content)]; - _ when is_list(Content) -> - lists:flatten(Content) - end, - format_tls_record(Direction, BinMsg); - 'handshake' -> - format_handshake(Direction, Content); - _Other -> - [] +log(Level, LogLevel, ReportMap, Meta) -> + case logger:compare_levels(LogLevel, Level) of + lt -> + logger:log(Level, ReportMap, Meta#{depth => ?DEPTH, + report_cb => fun ?MODULE:format/1}); + eq -> + logger:log(Level, ReportMap, Meta#{depth => ?DEPTH, + report_cb => fun ?MODULE:format/1}); + _ -> + ok end. -%% Stateful logging debug(Level, Direction, Protocol, Message) when (Direction =:= inbound orelse Direction =:= outbound) andalso (Protocol =:= 'record' orelse Protocol =:= 'handshake') -> @@ -83,17 +75,64 @@ debug(Level, Direction, Protocol, Message) ok end. -%% Stateful logging -notice(Level, Report) -> - case logger:compare_levels(Level, notice) of - lt -> - ?LOG_NOTICE(Report); - eq -> - ?LOG_NOTICE(Report); - _ -> - ok +%%------------------------------------------------------------------------- +%% Report formatting CB +%%------------------------------------------------------------------------- +format(#{alert := Alert, alerter := own} = Report) -> + #{protocol := ProtocolName, + role := Role, + alert := Alert, + statename := StateName } = Report, + ssl_alert:own_alert_format(ProtocolName, Role, StateName, Alert); +format(#{alert := Alert, alerter := peer} = Report) -> + #{protocol := ProtocolName, + role := Role, + alert := Alert, + statename := StateName } = Report, + ssl_alert:alert_format(ProtocolName, Role, StateName, Alert); +format(#{alert := Alert, alerter := ignored} = Report) -> + #{protocol := ProtocolName, + role := Role, + alert := Alert, + statename := StateName} = Report, + %% Happens in DTLS + {Fmt, Args} = ssl_alert:own_alert_format(ProtocolName, Role, StateName, Alert), + {"~s " ++ Fmt, ["Ignored alert to mitigate DoS attacks", Args]}; +format(#{description := Desc} = Report) -> + #{reason := Reason} = Report, + {"~s11:~p" + "~n" + "~s11:~p" + "~n", + ["Description", Desc, "Reason", Reason] + }. + +%%------------------------------------------------------------------------- +%% SSL log handler formatter +%%------------------------------------------------------------------------- +format(#{msg:= {report, Msg}}, _Config0) -> + #{direction := Direction, + protocol := Protocol, + message := Content} = Msg, + case Protocol of + 'record' -> + BinMsg = + case Content of + #ssl_tls{} -> + [tls_record:build_tls_record(Content)]; + _ when is_list(Content) -> + lists:flatten(Content) + end, + format_tls_record(Direction, BinMsg); + 'handshake' -> + format_handshake(Direction, Content); + _Other -> + [] end. +%%------------------------------------------------------------------------- +%% Internal functions +%%------------------------------------------------------------------------- %%------------------------------------------------------------------------- %% Handshake Protocol diff --git a/lib/ssl/src/ssl_manager.erl b/lib/ssl/src/ssl_manager.erl index 456a560bf6..fd842fcec4 100644 --- a/lib/ssl/src/ssl_manager.erl +++ b/lib/ssl/src/ssl_manager.erl @@ -53,7 +53,6 @@ session_lifetime :: integer(), certificate_db :: db_handle(), session_validation_timer :: reference(), - last_delay_timer = {undefined, undefined},%% Keep for testing purposes session_cache_client_max :: integer(), session_cache_server_max :: integer(), session_server_invalidator :: undefined | pid(), @@ -375,12 +374,6 @@ handle_info(validate_sessions, #state{session_cache_cb = CacheCb, session_client_invalidator = CPid, session_server_invalidator = SPid}}; - -handle_info({delayed_clean_session, Key, Cache}, #state{session_cache_cb = CacheCb - } = State) -> - CacheCb:delete(Cache, Key), - {noreply, State}; - handle_info({clean_cert_db, Ref, File}, #state{certificate_db = [CertDb, {RefDb, FileMapDb} | _]} = State) -> @@ -470,14 +463,6 @@ session_validation({{Port, _}, Session}, LifeTime) -> validate_session(Port, Session, LifeTime), LifeTime. -delay_time() -> - case application:get_env(ssl, session_delay_cleanup_time) of - {ok, Time} when is_integer(Time) -> - Time; - _ -> - ?CLEAN_SESSION_DB - end. - max_session_cache_size(CacheType) -> case application:get_env(ssl, CacheType) of {ok, Size} when is_integer(Size) -> @@ -486,36 +471,15 @@ max_session_cache_size(CacheType) -> ?DEFAULT_MAX_SESSION_CACHE end. -invalidate_session(Cache, CacheCb, Key, Session, State) -> +invalidate_session(Cache, CacheCb, Key, _Session, State) -> case CacheCb:lookup(Cache, Key) of undefined -> %% Session is already invalidated {noreply, State}; - #session{is_resumable = new} -> + #session{} -> CacheCb:delete(Cache, Key), - {noreply, State}; - _ -> - delayed_invalidate_session(CacheCb, Cache, Key, Session, State) + {noreply, State} end. -delayed_invalidate_session(CacheCb, Cache, Key, Session, - #state{last_delay_timer = LastTimer} = State) -> - %% When a registered session is invalidated we need to - %% wait a while before deleting it as there might be - %% pending connections that rightfully needs to look up - %% the session data but new connections should not get to - %% use this session. - CacheCb:update(Cache, Key, Session#session{is_resumable = false}), - TRef = - erlang:send_after(delay_time(), self(), - {delayed_clean_session, Key, Cache}), - {noreply, State#state{last_delay_timer = - last_delay_timer(Key, TRef, LastTimer)}}. - -last_delay_timer({{_,_},_}, TRef, {LastServer, _}) -> - {LastServer, TRef}; -last_delay_timer({_,_}, TRef, {_, LastClient}) -> - {TRef, LastClient}. - %% If we cannot generate a not allready in use session ID in %% ?GEN_UNIQUE_ID_MAX_TRIES we make the new session uncacheable The %% value of ?GEN_UNIQUE_ID_MAX_TRIES is stolen from open SSL which diff --git a/lib/ssl/src/ssl_pkix_db.erl b/lib/ssl/src/ssl_pkix_db.erl index dec48fa914..49db77ffd0 100644 --- a/lib/ssl/src/ssl_pkix_db.erl +++ b/lib/ssl/src/ssl_pkix_db.erl @@ -310,9 +310,8 @@ decode_certs(Ref, Cert) -> {decoded, {{Ref, SerialNumber, Issuer}, {Cert, ErlCert}}} catch error:_ -> - Report = io_lib:format("SSL WARNING: Ignoring a CA cert as " - "it could not be correctly decoded.~n", []), - ?LOG_NOTICE(Report), + ?LOG_NOTICE("SSL WARNING: Ignoring a CA cert as " + "it could not be correctly decoded.~n"), undefined end. diff --git a/lib/ssl/src/ssl_record.hrl b/lib/ssl/src/ssl_record.hrl index 6d4d47cedb..e37e3f714f 100644 --- a/lib/ssl/src/ssl_record.hrl +++ b/lib/ssl/src/ssl_record.hrl @@ -66,6 +66,7 @@ hash_size, % unit 8 compression_algorithm, % unit 8 master_secret, % opaque 48 + resumption_master_secret, client_random, % opaque 32 server_random, % opaque 32 exportable % boolean diff --git a/lib/ssl/src/ssl_session.erl b/lib/ssl/src/ssl_session.erl index fd012acd5e..0f7ea0502b 100644 --- a/lib/ssl/src/ssl_session.erl +++ b/lib/ssl/src/ssl_session.erl @@ -30,7 +30,7 @@ -include("ssl_api.hrl"). %% Internal application API --export([is_new/2, client_id/4, server_id/6, valid_session/2]). +-export([is_new/2, client_select_session/4, server_select_session/7, valid_session/2]). -type seconds() :: integer(). @@ -48,41 +48,38 @@ is_new(_ClientSuggestion, _ServerDecision) -> true. %%-------------------------------------------------------------------- --spec client_id({ssl:host(), inet:port_number(), ssl_options()}, db_handle(), atom(), - undefined | binary()) -> binary(). +-spec client_select_session({ssl:host(), inet:port_number(), map()}, db_handle(), atom(), + #session{}) -> #session{}. %% %% Description: Should be called by the client side to get an id %% for the client hello message. %%-------------------------------------------------------------------- -client_id({Host, Port, #{reuse_session := SessionId}}, Cache, CacheCb, _) when is_binary(SessionId)-> - case CacheCb:lookup(Cache, {{Host, Port}, SessionId}) of - undefined -> - <<>>; - #session{} -> - SessionId - end; -client_id(ClientInfo, Cache, CacheCb, OwnCert) -> - case select_session(ClientInfo, Cache, CacheCb, OwnCert) of - no_session -> - <<>>; - SessionId -> - SessionId - end. +client_select_session({_, _, #{versions := Versions, + protocol := Protocol}} = ClientInfo, + Cache, CacheCb, NewSession) -> + + RecordCb = record_cb(Protocol), + Version = RecordCb:lowest_protocol_version(Versions), + + case Version of + {3, N} when N >= 4 -> + NewSession#session{session_id = crypto:strong_rand_bytes(32)}; + _ -> + do_client_select_session(ClientInfo, Cache, CacheCb, NewSession) + end. --spec valid_session(#session{}, seconds() | {invalidate_before, integer()}) -> boolean(). +%%-------------------------------------------------------------------- +-spec server_select_session(ssl_record:ssl_version(), inet:port_number(), binary(), map(), + binary(),db_handle(), atom()) -> {binary(), #session{} | undefined}. %% -%% Description: Check that the session has not expired +%% Description: Should be called by the server side to get an id +%% for the client hello message. %%-------------------------------------------------------------------- -valid_session(#session{time_stamp = TimeStamp}, {invalidate_before, Before}) -> - TimeStamp > Before; -valid_session(#session{time_stamp = TimeStamp}, LifeTime) -> - Now = erlang:monotonic_time(), - Lived = erlang:convert_time_unit(Now-TimeStamp, native, seconds), - Lived < LifeTime. - -server_id(Port, <<>>, _SslOpts, _Cert, _, _) -> +server_select_session({_, Minor}, Port, <<>>, _SslOpts, _Cert, _, _) when Minor >= 4 -> {ssl_manager:new_session_id(Port), undefined}; -server_id(Port, SuggestedId, Options, Cert, Cache, CacheCb) -> +server_select_session(_, Port, <<>>, _SslOpts, _Cert, _, _) -> + {ssl_manager:new_session_id(Port), undefined}; +server_select_session(_, Port, SuggestedId, Options, Cert, Cache, CacheCb) -> LifeTime = case application:get_env(ssl, session_lifetime) of {ok, Time} when is_integer(Time) -> Time; _ -> ?'24H_in_sec' @@ -96,11 +93,38 @@ server_id(Port, SuggestedId, Options, Cert, Cache, CacheCb) -> {ssl_manager:new_session_id(Port), undefined} end. +-spec valid_session(#session{}, seconds() | {invalidate_before, integer()}) -> boolean(). +%% +%% Description: Check that the session has not expired +%%-------------------------------------------------------------------- +valid_session(#session{time_stamp = TimeStamp}, {invalidate_before, Before}) -> + TimeStamp > Before; +valid_session(#session{time_stamp = TimeStamp}, LifeTime) -> + Now = erlang:monotonic_time(), + Lived = erlang:convert_time_unit(Now-TimeStamp, native, seconds), + Lived < LifeTime. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- + +do_client_select_session({Host, Port, #{reuse_session := SessionId}}, Cache, CacheCb, NewSession) when is_binary(SessionId)-> + case CacheCb:lookup(Cache, {{Host, Port}, SessionId}) of + undefined -> + NewSession#session{session_id = <<>>}; + #session{} = Session-> + Session + end; +do_client_select_session(ClientInfo, + Cache, CacheCb, #session{own_certificate = OwnCert} = NewSession) -> + case select_session(ClientInfo, Cache, CacheCb, OwnCert) of + no_session -> + NewSession#session{session_id = <<>>}; + Session -> + Session + end. + select_session({_, _, #{reuse_sessions := Reuse}}, _Cache, _CacheCb, _OwnCert) when Reuse =/= true -> - %% If reuse_sessions == true | save a new session should be created + %% If reuse_sessions == false | save a new session should be created no_session; select_session({HostIP, Port, SslOpts}, Cache, CacheCb, OwnCert) -> Sessions = CacheCb:select_session(Cache, {HostIP, Port}), @@ -117,7 +141,7 @@ select_session(Sessions, #{ciphers := Ciphers}, OwnCert) -> end, case lists:dropwhile(IsNotResumable, Sessions) of [] -> no_session; - [Session | _] -> Session#session.session_id + [Session | _] -> Session end. is_resumable(_, _, #{reuse_sessions := false}, _, _, _, _) -> @@ -154,3 +178,8 @@ reusable_options(#{fail_if_no_peer_cert := true, (Session#session.peer_certificate =/= undefined); reusable_options(_,_) -> true. + +record_cb(tls) -> + tls_record; +record_cb(dtls) -> + dtls_record. diff --git a/lib/ssl/src/tls_bloom_filter.erl b/lib/ssl/src/tls_bloom_filter.erl new file mode 100644 index 0000000000..5cc29b6048 --- /dev/null +++ b/lib/ssl/src/tls_bloom_filter.erl @@ -0,0 +1,113 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2007-2019. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +%%---------------------------------------------------------------------- +%% Purpose: Bloom Filter implementation for anti-replay protection +%% in TLS 1.3 (stateless tickets) +%%---------------------------------------------------------------------- + +-module(tls_bloom_filter). + +-export([add_elem/2, + contains/2, + new/2, + rotate/1]). + +%%-------------------------------------------------------------------- +%% API --------------------------------------------------------------- +%%-------------------------------------------------------------------- + +%% Create new Bloom Filter with k hashes, m bits in the filter +new(K, M) -> + Size = round(math:ceil(M / 8)), + BitField = binary:copy(<<0>>, Size), + #{k => K, + m => M, + current => BitField, + old => BitField + }. + + +%% Add new element to Bloom Filter +add_elem(#{k := K, + m := M, + current := BitField0} = BloomFilter, + Elem) -> + Hash = hash(Elem, K, M), + BitField = set_bits(BitField0, Hash), + BloomFilter#{current => BitField}. + + +%% Check if Bloom Filter contains element. +contains(#{k := K, + m := M, + current := BFCurrent, + old := BFOld}, + Elem) -> + Hash = hash(Elem, K, M), + lists:all(fun (Pos) -> bit_is_set(BFCurrent, Pos) end, Hash) orelse + lists:all(fun (Pos) -> bit_is_set(BFOld, Pos) end, Hash). + + +rotate(#{m := M, + current := BFCurrent} = BloomFilter) -> + Size = round(math:ceil(M / 8)), + BFNew = binary:copy(<<0>>, Size), + BloomFilter#{current := BFNew, + old := BFCurrent}. + + +%%-------------------------------------------------------------------- +%% Internal functions ------------------------------------------------ +%%-------------------------------------------------------------------- +bit_is_set(<<1:1,_/bitstring>>, 0) -> + true; +bit_is_set(BitField, N) -> + case BitField of + <<_:N,1:1,_/bitstring>> -> + true; + _ -> + false + end. + + +set_bits(BitField, []) -> + BitField; +set_bits(BitField, [H|T]) -> + set_bits(set_bit(BitField, H), T). + + +set_bit(BitField, 0) -> + <<_:1,Rest/bitstring>> = BitField, + <<1:1,Rest/bitstring>>; +set_bit(BitField, B) -> + <<Front:B,_:1,Rest/bitstring>> = BitField, + <<Front:B,1:1,Rest/bitstring>>. + + +%% Kirsch-Mitzenmacher-Optimization +hash(Elem, K, M) -> + hash(Elem, K, M, []). +%% +hash(_, 0, _, Acc) -> + Acc; +hash(Elem, K, M, Acc) -> + H = (erlang:phash2({Elem, 0}, M) + (K - 1) * erlang:phash2({Elem, 1}, M)) rem M, + hash(Elem, K - 1, M, [H|Acc]). diff --git a/lib/ssl/src/tls_client_ticket_store.erl b/lib/ssl/src/tls_client_ticket_store.erl new file mode 100644 index 0000000000..6343b9cf0b --- /dev/null +++ b/lib/ssl/src/tls_client_ticket_store.erl @@ -0,0 +1,355 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2007-2019. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +%%---------------------------------------------------------------------- +%% Purpose: Handle client side TLS-1.3 session ticket storage +%%---------------------------------------------------------------------- + +-module(tls_client_ticket_store). +-behaviour(gen_server). + +-include("tls_handshake_1_3.hrl"). + +%% API +-export([find_ticket/2, + get_tickets/2, + lock_tickets/2, + remove_tickets/1, + start_link/2, + store_ticket/4, + unlock_tickets/2, + update_ticket/2]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3, format_status/2]). + +-define(SERVER, ?MODULE). + +-record(state, { + db, + lifetime, + max + }). + +-record(data, { + pos = undefined, + hkdf, + sni, + psk, + timestamp, + ticket, + lock = undefined + }). + +%%%=================================================================== +%%% API +%%%=================================================================== +-spec start_link(integer(), integer()) -> {ok, Pid :: pid()} | + {error, Error :: {already_started, pid()}} | + {error, Error :: term()} | + ignore. +start_link(Max, Lifetime) -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [Max, Lifetime], []). + +find_ticket(Pid, HashAlgos) -> + %% TODO use also SNI when selecting tickets + gen_server:call(?MODULE, {find_ticket, Pid, HashAlgos}, infinity). + +get_tickets(Pid, Keys) -> + gen_server:call(?MODULE, {get_tickets, Pid, Keys}, infinity). + +lock_tickets(_, undefined) -> + ok; +lock_tickets(Pid, Keys) -> + gen_server:call(?MODULE, {lock, Pid, Keys}, infinity). + +remove_tickets([]) -> + ok; +remove_tickets(Keys) -> + gen_server:cast(?MODULE, {remove_tickets, Keys}). + +store_ticket(Ticket, HKDF, SNI, PSK) -> + gen_server:call(?MODULE, {store_ticket, Ticket, HKDF, SNI, PSK}, infinity). + +unlock_tickets(Pid, Keys) -> + gen_server:call(?MODULE, {unlock, Pid, Keys}, infinity). + +update_ticket(Key, Pos) -> + gen_server:call(?MODULE, {update_ticket, Key, Pos}, infinity). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +-spec init(Args :: term()) -> {ok, State :: term()}. + +init(Args) -> + process_flag(trap_exit, true), + State = inital_state(Args), + {ok, State}. + +-spec handle_call(Request :: term(), From :: {pid(), term()}, State :: term()) -> + {reply, Reply :: term(), NewState :: term()} . +handle_call({find_ticket, Pid, HashAlgos}, _From, State) -> + Key = find_ticket(State, Pid, HashAlgos), + {reply, Key, State}; +handle_call({get_tickets, Pid, Keys}, _From, State) -> + Data = get_tickets(State, Pid, Keys), + {reply, Data, State}; +handle_call({lock, Pid, Keys}, _From, State0) -> + State = lock_tickets(State0, Pid, Keys), + {reply, ok, State}; +handle_call({store_ticket, Ticket, HKDF, SNI, PSK}, _From, State0) -> + State = store_ticket(State0, Ticket, HKDF, SNI, PSK), + {reply, ok, State}; +handle_call({unlock, Pid, Keys}, _From, State0) -> + State = unlock_tickets(State0, Pid, Keys), + {reply, ok, State}; +handle_call({update_ticket, Key, Pos}, _From, State0) -> + State = update_ticket(State0, Key, Pos), + {reply, ok, State}. + +-spec handle_cast(Request :: term(), State :: term()) -> + {noreply, NewState :: term()}. +handle_cast({remove_tickets, Key}, State0) -> + State = remove_tickets(State0, Key), + {noreply, State}; +handle_cast(_Request, State) -> + {noreply, State}. + +-spec handle_info(Info :: timeout() | term(), State :: term()) -> + {noreply, NewState :: term()}. +handle_info(remove_invalid_tickets, State0) -> + State = remove_invalid_tickets(State0), + {noreply, State}; +handle_info(_Info, State) -> + {noreply, State}. + +-spec terminate(Reason :: normal | shutdown | {shutdown, term()} | term(), + State :: term()) -> any(). +terminate(_Reason, _State) -> + ok. + +-spec code_change(OldVsn :: term() | {down, term()}, + State :: term(), + Extra :: term()) -> {ok, NewState :: term()} | + {error, Reason :: term()}. +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + + +-spec format_status(Opt :: normal | terminate, + Status :: list()) -> Status :: term(). +format_status(_Opt, Status) -> + Status. +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +inital_state([Max, Lifetime]) -> + erlang:send_after(Lifetime * 1000, self(), remove_invalid_tickets), + #state{db = gb_trees:empty(), + lifetime = Lifetime, + max = Max + }. + + +find_ticket(_, _, []) -> + undefined; +find_ticket(#state{db = Db, + lifetime = Lifetime} = State, Pid, [Hash|T]) -> + case iterate_tickets(gb_trees:iterator(Db), Pid, Hash, Lifetime) of + none -> + find_ticket(State, Pid, T); + Key -> + Key + end. + + +iterate_tickets(Iter0, Pid, Hash, Lifetime) -> + case gb_trees:next(Iter0) of + {Key, #data{hkdf = Hash, + timestamp = Timestamp, + lock = Lock}, Iter} when Lock =:= undefined orelse + Lock =:= Pid -> + Age = erlang:system_time(seconds) - Timestamp, + if Age < Lifetime -> + Key; + true -> + iterate_tickets(Iter, Pid, Hash, Lifetime) + end; + {_, _, Iter} -> + iterate_tickets(Iter, Pid, Hash, Lifetime); + none -> + none + end. + + +%% Get tickets that are not locked by another process +get_tickets(State, Pid, Keys) -> + get_tickets(State, Pid, Keys, []). +%% +get_tickets(_, _, [], []) -> + undefined; %% No tickets found +get_tickets(_, _, [], Acc) -> + Acc; +get_tickets(#state{db = Db} = State, Pid, [Key|T], Acc) -> + try gb_trees:get(Key, Db) of + #data{pos = Pos, + hkdf = HKDF, + psk = PSK, + timestamp = Timestamp, + ticket = NewSessionTicket, + lock = Lock} when Lock =:= undefined orelse + Lock =:= Pid -> + #new_session_ticket{ + ticket_lifetime = _LifeTime, + ticket_age_add = AgeAdd, + ticket_nonce = Nonce, + ticket = Ticket, + extensions = _Extensions + } = NewSessionTicket, + TicketAge = erlang:system_time(seconds) - Timestamp, + ObfuscatedTicketAge = obfuscate_ticket_age(TicketAge, AgeAdd), + Identity = #psk_identity{ + identity = Ticket, + obfuscated_ticket_age = ObfuscatedTicketAge}, + get_tickets(State, Pid, T, [{Key, Pos, Identity, PSK, Nonce, HKDF}|Acc]) + catch + _:_ -> + get_tickets(State, Pid, T, Acc) + end. + +%% The "obfuscated_ticket_age" +%% field of each PskIdentity contains an obfuscated version of the +%% ticket age formed by taking the age in milliseconds and adding the +%% "ticket_age_add" value that was included with the ticket +%% (see Section 4.6.1), modulo 2^32. +obfuscate_ticket_age(TicketAge, AgeAdd) -> + (TicketAge + AgeAdd) rem round(math:pow(2,32)). + + +remove_tickets(State, []) -> + State; +remove_tickets(State0, [Key|T]) -> + remove_tickets(remove_ticket(State0, Key), T). + + +remove_ticket(#state{db = Db0} = State, Key) -> + Db = gb_trees:delete_any(Key, Db0), + State#state{db = Db}. + + +remove_invalid_tickets(#state{db = Db, + lifetime = Lifetime} = State0) -> + Keys = collect_invalid_tickets(gb_trees:iterator(Db), Lifetime), + State = remove_tickets(State0, Keys), + erlang:send_after(Lifetime * 1000, self(), remove_invalid_tickets), + State. + + +collect_invalid_tickets(Iter, Lifetime) -> + collect_invalid_tickets(Iter, Lifetime, []). +%% +collect_invalid_tickets(Iter0, Lifetime, Acc) -> + case gb_trees:next(Iter0) of + {Key, #data{timestamp = Timestamp, + lock = undefined}, Iter} -> + Age = erlang:system_time(seconds) - Timestamp, + if Age < Lifetime -> + collect_invalid_tickets(Iter, Lifetime, Acc); + true -> + collect_invalid_tickets(Iter, Lifetime, [Key|Acc]) + end; + {_, _, Iter} -> %% Skip locked tickets + collect_invalid_tickets(Iter, Lifetime, Acc); + none -> + Acc + end. + + +store_ticket(#state{db = Db0, max = Max} = State, Ticket, HKDF, SNI, PSK) -> + Timestamp = erlang:system_time(seconds), + Size = gb_trees:size(Db0), + Db1 = if Size =:= Max -> + delete_oldest(Db0); + true -> + Db0 + end, + Key = erlang:monotonic_time(), + Db = gb_trees:insert(Key, + #data{hkdf = HKDF, + sni = SNI, + psk = PSK, + timestamp = Timestamp, + ticket = Ticket}, + Db1), + State#state{db = Db}. + + +update_ticket(#state{db = Db0} = State, Key, Pos) -> + try gb_trees:get(Key, Db0) of + Value -> + Db = gb_trees:update(Key, Value#data{pos = Pos}, Db0), + State#state{db = Db} + catch + _:_ -> + State + end. + + +delete_oldest(Db0) -> + try gb_trees:take_smallest(Db0) of + {_, _, Db} -> + Db + catch + _:_ -> + Db0 + end. + + +lock_tickets(State, Pid, Keys) -> + set_lock(State, Pid, Keys, lock). + + +unlock_tickets(State, Pid, Keys) -> + set_lock(State, Pid, Keys, unlock). + + +set_lock(State, _, [], _) -> + State; +set_lock(#state{db = Db0} = State, Pid, [Key|T], Cmd) -> + try gb_trees:get(Key, Db0) of + Value -> + Db = gb_trees:update(Key, update_data_lock(Value, Pid, Cmd), Db0), + set_lock(State#state{db = Db}, Pid, T, Cmd) + catch + _:_ -> + set_lock(State, Pid, T, Cmd) + end. + + +update_data_lock(Value, Pid, lock) -> + Value#data{lock = Pid}; +update_data_lock(#data{lock = Pid} = Value, Pid, unlock) -> + Value#data{lock = undefined}; +update_data_lock(Value, _, _) -> + Value. diff --git a/lib/ssl/src/tls_connection.erl b/lib/ssl/src/tls_connection.erl index cf104cd805..20224f4c65 100644 --- a/lib/ssl/src/tls_connection.erl +++ b/lib/ssl/src/tls_connection.erl @@ -98,28 +98,28 @@ %%==================================================================== %% Setup %%==================================================================== -start_fsm(Role, Host, Port, Socket, {#{erl_dist := false},_, Tracker} = Opts, +start_fsm(Role, Host, Port, Socket, {#{erl_dist := false},_, Trackers} = Opts, User, {CbModule, _,_, _, _} = CbInfo, Timeout) -> try {ok, Sender} = tls_sender:start(), {ok, Pid} = tls_connection_sup:start_child([Role, Sender, Host, Port, Socket, Opts, User, CbInfo]), - {ok, SslSocket} = ssl_connection:socket_control(?MODULE, Socket, [Pid, Sender], CbModule, Tracker), + {ok, SslSocket} = ssl_connection:socket_control(?MODULE, Socket, [Pid, Sender], CbModule, Trackers), ssl_connection:handshake(SslSocket, Timeout) catch error:{badmatch, {error, _} = Error} -> Error end; -start_fsm(Role, Host, Port, Socket, {#{erl_dist := true},_, Tracker} = Opts, +start_fsm(Role, Host, Port, Socket, {#{erl_dist := true},_, Trackers} = Opts, User, {CbModule, _,_, _, _} = CbInfo, Timeout) -> try {ok, Sender} = tls_sender:start([{spawn_opt, ?DIST_CNTRL_SPAWN_OPTS}]), {ok, Pid} = tls_connection_sup:start_child_dist([Role, Sender, Host, Port, Socket, Opts, User, CbInfo]), - {ok, SslSocket} = ssl_connection:socket_control(?MODULE, Socket, [Pid, Sender], CbModule, Tracker), + {ok, SslSocket} = ssl_connection:socket_control(?MODULE, Socket, [Pid, Sender], CbModule, Trackers), ssl_connection:handshake(SslSocket, Timeout) catch error:{badmatch, {error, _} = Error} -> @@ -298,9 +298,12 @@ handle_protocol_record(#ssl_tls{type = ?HANDSHAKE, fragment = Data}, StateName, #state{protocol_buffers = #protocol_buffers{tls_handshake_buffer = Buf0} = Buffers, connection_env = #connection_env{negotiated_version = Version}, + static_env = #static_env{role = Role}, ssl_options = Options} = State0) -> try - EffectiveVersion = effective_version(Version, Options), + %% Calculate the effective version that should be used when decoding an incoming handshake + %% message. + EffectiveVersion = effective_version(Version, Options, Role), {Packets, Buf} = tls_handshake:get_tls_handshake(EffectiveVersion,Data,Buf0, Options), State = State0#state{protocol_buffers = @@ -514,8 +517,8 @@ protocol_name() -> %% Data handling %%==================================================================== -socket(Pids, Transport, Socket, Tracker) -> - tls_socket:socket(Pids, Transport, Socket, ?MODULE, Tracker). +socket(Pids, Transport, Socket, Trackers) -> + tls_socket:socket(Pids, Transport, Socket, ?MODULE, Trackers). setopts(Transport, Socket, Other) -> tls_socket:setopts(Transport, Socket, Other). @@ -543,26 +546,48 @@ init({call, From}, {start, Timeout}, handshake_env = #handshake_env{renegotiation = {Renegotiation, _}} = HsEnv, connection_env = CEnv, ssl_options = #{log_level := LogLevel, - versions := Versions} = SslOpts, - session = #session{own_certificate = Cert} = Session0, + %% Use highest version in initial ClientHello. + %% Versions is a descending list of supported versions. + versions := [HelloVersion|_] = Versions, + session_tickets := SessionTickets} = SslOpts, + session = NewSession, connection_states = ConnectionStates0 } = State0) -> KeyShare = maybe_generate_client_shares(SslOpts), + Session = ssl_session:client_select_session({Host, Port, SslOpts}, Cache, CacheCb, NewSession), + %% Update UseTicket in case of automatic session resumption + {UseTicket, State1} = tls_handshake_1_3:maybe_automatic_session_resumption(State0), + TicketData = tls_handshake_1_3:get_ticket_data(self(), SessionTickets, UseTicket), Hello = tls_handshake:client_hello(Host, Port, ConnectionStates0, SslOpts, - Cache, CacheCb, Renegotiation, Cert, KeyShare), + Session#session.session_id, + Renegotiation, + Session#session.own_certificate, + KeyShare, + TicketData), - HelloVersion = tls_record:hello_version(Versions), Handshake0 = ssl_handshake:init_handshake_history(), + + %% Update pre_shared_key extension with binders (TLS 1.3) + Hello1 = tls_handshake_1_3:maybe_add_binders(Hello, TicketData, HelloVersion), + {BinMsg, ConnectionStates, Handshake} = - encode_handshake(Hello, HelloVersion, ConnectionStates0, Handshake0), + encode_handshake(Hello1, HelloVersion, ConnectionStates0, Handshake0), + tls_socket:send(Transport, Socket, BinMsg), - ssl_logger:debug(LogLevel, outbound, 'handshake', Hello), + ssl_logger:debug(LogLevel, outbound, 'handshake', Hello1), ssl_logger:debug(LogLevel, outbound, 'record', BinMsg), - State = State0#state{connection_states = ConnectionStates, - connection_env = CEnv#connection_env{negotiated_version = HelloVersion}, %% Requested version - session = - Session0#session{session_id = Hello#client_hello.session_id}, + %% RequestedVersion is used as the legacy record protocol version and shall be + %% {3,3} in case of TLS 1.2 and higher. In all other cases it defaults to the + %% lowest supported protocol version. + %% + %% negotiated_version is also used by the TLS 1.3 state machine and is set after + %% ServerHello is processed. + RequestedVersion = tls_record:hello_version(Versions), + State = State1#state{connection_states = ConnectionStates, + connection_env = CEnv#connection_env{ + negotiated_version = RequestedVersion}, + session = Session, handshake_env = HsEnv#handshake_env{tls_handshake_history = Handshake}, start_or_recv_from = From, key_share = KeyShare}, @@ -599,7 +624,7 @@ hello(internal, #client_hello{extensions = Extensions} = Hello, {next_state, user_hello, State#state{start_or_recv_from = undefined, handshake_env = HsEnv#handshake_env{hello = Hello}}, [{reply, From, {ok, Extensions}}]}; -hello(internal, #server_hello{extensions = Extensions} = Hello, +hello(internal, #server_hello{extensions = Extensions} = Hello, #state{ssl_options = #{handshake := hello}, handshake_env = HsEnv, start_or_recv_from = From} = State) -> @@ -670,9 +695,12 @@ hello(internal, #server_hello{} = Hello, ssl_connection:handle_session(Hello, Version, NewId, ConnectionStates, ProtoExt, Protocol, State); %% TLS 1.3 - {next_state, wait_sh} -> + {next_state, wait_sh, SelectedVersion} -> %% Continue in TLS 1.3 'wait_sh' state - {next_state, wait_sh, State, [{next_event, internal, Hello}]} + {next_state, wait_sh, + State#state{ + connection_env = CEnv#connection_env{negotiated_version = SelectedVersion}}, + [{next_event, internal, Hello}]} end; hello(info, Event, State) -> gen_info(Event, ?FUNCTION_NAME, State); @@ -762,11 +790,14 @@ connection(internal, #hello_request{}, connection_states = ConnectionStates} = State0) -> try tls_sender:peer_renegotiate(Pid) of {ok, Write} -> + Session = ssl_session:client_select_session({Host, Port, SslOpts}, Cache, CacheCb, Session0), Hello = tls_handshake:client_hello(Host, Port, ConnectionStates, SslOpts, - Cache, CacheCb, Renegotiation, Cert, undefined), - {State, Actions} = send_handshake(Hello, State0#state{connection_states = ConnectionStates#{current_write => Write}}), - next_event(hello, no_record, State#state{session = Session0#session{session_id - = Hello#client_hello.session_id}}, Actions) + Session#session.session_id, + Renegotiation, Cert, undefined, + undefined), + {State, Actions} = send_handshake(Hello, State0#state{connection_states = ConnectionStates#{current_write => Write}, + session = Session}), + next_event(hello, no_record, State, Actions) catch _:_ -> {stop, {shutdown, sender_blocked}, State0} @@ -774,19 +805,17 @@ connection(internal, #hello_request{}, connection(internal, #hello_request{}, #state{static_env = #static_env{role = client, host = Host, - port = Port, - session_cache = Cache, - session_cache_cb = CacheCb}, + port = Port}, handshake_env = #handshake_env{renegotiation = {Renegotiation, _}}, - session = #session{own_certificate = Cert} = Session0, + session = #session{own_certificate = Cert}, ssl_options = SslOpts, connection_states = ConnectionStates} = State0) -> Hello = tls_handshake:client_hello(Host, Port, ConnectionStates, SslOpts, - Cache, CacheCb, Renegotiation, Cert, undefined), + <<>>, Renegotiation, Cert, undefined, + undefined), {State, Actions} = send_handshake(Hello, State0), - next_event(hello, no_record, State#state{session = Session0#session{session_id - = Hello#client_hello.session_id}}, Actions); + next_event(hello, no_record, State, Actions); connection(internal, #client_hello{} = Hello, #state{static_env = #static_env{role = server}, handshake_env = #handshake_env{allow_renegotiate = true}= HsEnv, @@ -813,9 +842,9 @@ connection(internal, #client_hello{}, State = reinit_handshake_data(State0), next_event(?FUNCTION_NAME, no_record, State); -connection(internal, #new_session_ticket{}, State) -> +connection(internal, #new_session_ticket{} = NewSessionTicket, State) -> %% TLS 1.3 - %% Drop NewSessionTicket (currently not supported) + handle_new_session_ticket(NewSessionTicket, State), next_event(?FUNCTION_NAME, no_record, State); connection(Type, Event, State) -> @@ -981,7 +1010,7 @@ code_change(_OldVsn, StateName, State, _) -> %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -initial_state(Role, Sender, Host, Port, Socket, {SSLOptions, SocketOptions, Tracker}, User, +initial_state(Role, Sender, Host, Port, Socket, {SSLOptions, SocketOptions, Trackers}, User, {CbModule, DataTag, CloseTag, ErrorTag, PassiveTag}) -> #{beast_mitigation := BeastMitigation, erl_dist := IsErlDist, @@ -1012,8 +1041,8 @@ initial_state(Role, Sender, Host, Port, Socket, {SSLOptions, SocketOptions, Trac port = Port, socket = Socket, session_cache_cb = SessionCacheCb, - tracker = Tracker - }, + trackers = Trackers + }, #state{ static_env = InitStatEnv, handshake_env = #handshake_env{ @@ -1040,7 +1069,7 @@ initialize_tls_sender(#state{static_env = #static_env{ role = Role, transport_cb = Transport, socket = Socket, - tracker = Tracker + trackers = Trackers }, connection_env = #connection_env{negotiated_version = Version}, socket_options = SockOpts, @@ -1052,7 +1081,7 @@ initialize_tls_sender(#state{static_env = #static_env{ role => Role, socket => Socket, socket_options => SockOpts, - tracker => Tracker, + trackers => Trackers, transport_cb => Transport, negotiated_version => Version, renegotiate_at => RenegotiateAt, @@ -1065,12 +1094,19 @@ next_tls_record(Data, StateName, tls_cipher_texts = CT0} = Buffers, ssl_options = SslOpts} = State0) -> Versions = - %% TLS 1.3 Client/Server - %% - Ignore TLSPlaintext.legacy_record_version - %% - Verify that TLSCiphertext.legacy_record_version is set to 0x0303 for all records - %% other than an initial ClientHello, where it MAY also be 0x0301. + %% TLSPlaintext.legacy_record_version is ignored in TLS 1.3 and thus all + %% record version are accepted when receiving initial ClientHello and + %% ServerHello. This can happen in state 'hello' in case of all TLS + %% versions and also in state 'start' when TLS 1.3 is negotiated. + %% After the version is negotiated all subsequent TLS records shall have + %% the proper legacy_record_version (= negotiated_version). + %% Note: TLS record version {3,4} is used internally in TLS 1.3 and at this + %% point it is the same as the negotiated protocol version. + %% TODO: Refactor state machine and introduce a record_protocol_version beside + %% the negotiated_version. case StateName of - hello -> + State when State =:= hello orelse + State =:= start -> [tls_record:protocol_version(Vsn) || Vsn <- ?ALL_AVAILABLE_VERSIONS]; _ -> State0#state.connection_env#connection_env.negotiated_version @@ -1323,7 +1359,55 @@ choose_tls_version(_, _) -> 'tls_v1.2'. -effective_version(undefined, #{versions := [Version|_]}) -> +%% Special version handling for TLS 1.3 clients: +%% In the shared state 'init' negotiated_version is set to requested version and +%% that is expected by the legacy part of the state machine. However, in order to +%% be able to process new TLS 1.3 extensions, the effective version shall be set +%% {3,4}. +%% When highest supported version is {3,4} the negotiated version is set to {3,3}. +effective_version({3,3} , #{versions := [Version|_]}, client) when Version >= {3,4} -> + Version; +%% Use highest supported version during startup (TLS server, all versions). +effective_version(undefined, #{versions := [Version|_]}, _) -> Version; -effective_version(Version, _) -> +%% Use negotiated version in all other cases. +effective_version(Version, _, _) -> Version. + + +handle_new_session_ticket(_, #state{ssl_options = #{session_tickets := disabled}}) -> + ok; +handle_new_session_ticket(#new_session_ticket{ticket_nonce = Nonce} = NewSessionTicket, + #state{connection_states = ConnectionStates, + ssl_options = #{session_tickets := SessionTickets, + server_name_indication := SNI}, + connection_env = #connection_env{user_application = {_, User}}}) + when SessionTickets =:= enabled -> + #{security_parameters := SecParams} = + ssl_record:current_connection_state(ConnectionStates, read), + HKDF = SecParams#security_parameters.prf_algorithm, + RMS = SecParams#security_parameters.resumption_master_secret, + PSK = tls_v1:pre_shared_key(RMS, Nonce, HKDF), + send_ticket_data(User, NewSessionTicket, HKDF, SNI, PSK); +handle_new_session_ticket(#new_session_ticket{ticket_nonce = Nonce} = NewSessionTicket, + #state{connection_states = ConnectionStates, + ssl_options = #{session_tickets := SessionTickets, + server_name_indication := SNI}}) + when SessionTickets =:= auto -> + #{security_parameters := SecParams} = + ssl_record:current_connection_state(ConnectionStates, read), + HKDF = SecParams#security_parameters.prf_algorithm, + RMS = SecParams#security_parameters.resumption_master_secret, + PSK = tls_v1:pre_shared_key(RMS, Nonce, HKDF), + tls_client_ticket_store:store_ticket(NewSessionTicket, HKDF, SNI, PSK). + + +%% Send ticket data to user as opaque binary +send_ticket_data(User, NewSessionTicket, HKDF, SNI, PSK) -> + Timestamp = erlang:system_time(seconds), + TicketData = #{hkdf => HKDF, + sni => SNI, + psk => PSK, + timestamp => Timestamp, + ticket => NewSessionTicket}, + User ! {ssl, session_ticket, {SNI, erlang:term_to_binary(TicketData)}}. diff --git a/lib/ssl/src/tls_connection_1_3.erl b/lib/ssl/src/tls_connection_1_3.erl index 117e4f059d..6fb96b1f5a 100644 --- a/lib/ssl/src/tls_connection_1_3.erl +++ b/lib/ssl/src/tls_connection_1_3.erl @@ -128,7 +128,9 @@ start(internal, #client_hello{} = Hello, State0, _Module) -> {State, start} -> {next_state, start, State, []}; {State, negotiated} -> - {next_state, negotiated, State, [{next_event, internal, start_handshake}]} + {next_state, negotiated, State, [{next_event, internal, {start_handshake, undefined}}]}; + {State, negotiated, PSK} -> %% Session Resumption with PSK + {next_state, negotiated, State, [{next_event, internal, {start_handshake, PSK}}]} end; start(internal, #server_hello{} = ServerHello, State0, _Module) -> case tls_handshake_1_3:do_start(ServerHello, State0) of diff --git a/lib/ssl/src/tls_handshake.erl b/lib/ssl/src/tls_handshake.erl index 203f89a0b8..474cbd621b 100644 --- a/lib/ssl/src/tls_handshake.erl +++ b/lib/ssl/src/tls_handshake.erl @@ -51,18 +51,18 @@ %%==================================================================== %%-------------------------------------------------------------------- -spec client_hello(ssl:host(), inet:port_number(), ssl_record:connection_states(), - ssl_options(), integer(), atom(), boolean(), der_cert(), - #key_share_client_hello{} | undefined) -> + ssl_options(), binary(), boolean(), der_cert(), + #key_share_client_hello{} | undefined, tuple() | undefined) -> #client_hello{}. %% %% Description: Creates a client hello message. %%-------------------------------------------------------------------- -client_hello(Host, Port, ConnectionStates, +client_hello(_Host, _Port, ConnectionStates, #{versions := Versions, ciphers := UserSuites, fallback := Fallback } = SslOpts, - Cache, CacheCb, Renegotiation, OwnCert, KeyShare) -> + Id, Renegotiation, _OwnCert, KeyShare, TicketData) -> Version = tls_record:highest_protocol_version(Versions), %% In TLS 1.3, the client indicates its version preferences in the @@ -83,9 +83,9 @@ client_hello(Host, Port, ConnectionStates, AvailableCipherSuites, SslOpts, ConnectionStates, Renegotiation, - KeyShare), + KeyShare, + TicketData), CipherSuites = ssl_handshake:cipher_suites(AvailableCipherSuites, Renegotiation, Fallback), - Id = ssl_session:client_id({Host, Port, SslOpts}, Cache, CacheCb, OwnCert), #client_hello{session_id = Id, client_version = LegacyVersion, cipher_suites = CipherSuites, @@ -105,7 +105,7 @@ client_hello(Host, Port, ConnectionStates, {tls_record:tls_version(), {resumed | new, #session{}}, ssl_record:connection_states(), binary() | undefined, HelloExt::map(), {ssl:hash(), ssl:sign_algo()} | - undefined} | {atom(), atom()} |#alert{}. + undefined} | {atom(), atom()} | {atom(), atom(), tuple()} | #alert{}. %% %% Description: Handles a received hello message %%-------------------------------------------------------------------- @@ -175,9 +175,9 @@ hello(#server_hello{server_version = LegacyVersion, handle_server_hello_extensions(Version, SessionId, Random, CipherSuite, Compression, HelloExt, SslOpt, ConnectionStates0, Renegotiation); - {3,4} -> + SelectedVersion -> %% TLS 1.3 - {next_state, wait_sh} + {next_state, wait_sh, SelectedVersion} end; false -> ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER) diff --git a/lib/ssl/src/tls_handshake_1_3.erl b/lib/ssl/src/tls_handshake_1_3.erl index 5f4c0b9a4a..119ed75d36 100644 --- a/lib/ssl/src/tls_handshake_1_3.erl +++ b/lib/ssl/src/tls_handshake_1_3.erl @@ -48,8 +48,13 @@ do_wait_finished/2, do_wait_sh/2, do_wait_ee/2, - do_wait_cert_cr/2]). + do_wait_cert_cr/2, + get_ticket_data/3, + maybe_add_binders/3, + maybe_add_binders/4, + maybe_automatic_session_resumption/1]). +-export([is_valid_binder/4]). %% crypto:hash(sha256, "HelloRetryRequest"). -define(HELLO_RETRY_REQUEST_RANDOM, <<207,33,173,116,229,154,97,17, @@ -61,10 +66,10 @@ %% Create handshake messages %%==================================================================== -server_hello(MsgType, SessionId, KeyShare, ConnectionStates) -> +server_hello(MsgType, SessionId, KeyShare, PSK, ConnectionStates) -> #{security_parameters := SecParams} = ssl_record:pending_connection_state(ConnectionStates, read), - Extensions = server_hello_extensions(MsgType, KeyShare), + Extensions = server_hello_extensions(MsgType, KeyShare, PSK), #server_hello{server_version = {3,3}, %% legacy_version cipher_suite = SecParams#security_parameters.cipher_suite, compression_method = 0, %% legacy attribute @@ -81,13 +86,19 @@ server_hello(MsgType, SessionId, KeyShare, ConnectionStates) -> %% extensions that were not first offered by the client in its %% ClientHello, with the exception of optionally the "cookie" (see %% Section 4.2.2) extension. -server_hello_extensions(hello_retry_request = MsgType, KeyShare) -> +server_hello_extensions(hello_retry_request = MsgType, KeyShare, _) -> SupportedVersions = #server_hello_selected_version{selected_version = {3,4}}, Extensions = #{server_hello_selected_version => SupportedVersions}, ssl_handshake:add_server_share(MsgType, Extensions, KeyShare); -server_hello_extensions(MsgType, KeyShare) -> +server_hello_extensions(MsgType, KeyShare, undefined) -> SupportedVersions = #server_hello_selected_version{selected_version = {3,4}}, Extensions = #{server_hello_selected_version => SupportedVersions}, + ssl_handshake:add_server_share(MsgType, Extensions, KeyShare); +server_hello_extensions(MsgType, KeyShare, {SelectedIdentity, _}) -> + SupportedVersions = #server_hello_selected_version{selected_version = {3,4}}, + PreSharedKey = #pre_shared_key_server_hello{selected_identity = SelectedIdentity}, + Extensions = #{server_hello_selected_version => SupportedVersions, + pre_shared_key => PreSharedKey}, ssl_handshake:add_server_share(MsgType, Extensions, KeyShare). @@ -271,9 +282,11 @@ encode_handshake(#new_session_ticket{ ticket = Ticket, extensions = Exts}) -> TicketSize = byte_size(Ticket), + NonceSize = byte_size(Nonce), BinExts = encode_extensions(Exts), {?NEW_SESSION_TICKET, <<?UINT32(LifeTime), ?UINT32(Age), - ?BYTE(Nonce), ?UINT16(TicketSize), Ticket/binary, + ?BYTE(NonceSize), Nonce/binary, + ?UINT16(TicketSize), Ticket/binary, BinExts/binary>>}; encode_handshake(#end_of_early_data{}) -> {?END_OF_EARLY_DATA, <<>>}; @@ -352,6 +365,18 @@ decode_handshake(?KEY_UPDATE, <<?BYTE(Update)>>) -> decode_handshake(Tag, HandshakeMsg) -> ssl_handshake:decode_handshake({3,4}, Tag, HandshakeMsg). +is_valid_binder(Binder, HHistory, PSK, Hash) -> + case HHistory of + [ClientHello2, HRR, MessageHash|_] -> + Truncated = truncate_client_hello(ClientHello2), + FinishedKey = calculate_finished_key(PSK, Hash), + Binder == calculate_binder(FinishedKey, Hash, [MessageHash, HRR, Truncated]); + [ClientHello1|_] -> + Truncated = truncate_client_hello(ClientHello1), + FinishedKey = calculate_finished_key(PSK, Hash), + Binder == calculate_binder(FinishedKey, Hash, Truncated) + end. + %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- @@ -515,6 +540,8 @@ do_start(#client_hello{cipher_suites = ClientCiphers, ClientShares0 = maps:get(key_share, Extensions, undefined), ClientShares = get_key_shares(ClientShares0), + OfferedPSKs = get_offered_psks(Extensions), + ClientALPN0 = maps:get(alpn, Extensions, undefined), ClientALPN = ssl_handshake:decode_alpn(ClientALPN0), @@ -534,6 +561,7 @@ do_start(#client_hello{cipher_suites = ClientCiphers, %% and a signature algorithm/certificate pair to authenticate itself to %% the client. Cipher = Maybe(select_cipher_suite(HonorCipherOrder, ClientCiphers, ServerCiphers)), + Groups = Maybe(select_common_groups(ServerGroups, ClientGroups)), Maybe(validate_client_key_share(ClientGroups, ClientShares)), @@ -569,10 +597,14 @@ do_start(#client_hello{cipher_suites = ClientCiphers, %% message if it is able to find an acceptable set of parameters but the %% ClientHello does not contain sufficient information to proceed with %% the handshake. - Maybe(send_hello_retry_request(State1, ClientPubKey, KeyShare, SessionId)) - - %% TODO: session handling - + case Maybe(send_hello_retry_request(State1, ClientPubKey, KeyShare, SessionId)) of + {_, start} = NextStateTuple -> + NextStateTuple; + {_, negotiated} = NextStateTuple -> + %% Exclude any incompatible PSKs. + PSK = Maybe(handle_pre_shared_key(State1, OfferedPSKs, Cipher)), + Maybe(session_resumption(NextStateTuple, PSK)) + end catch {Ref, {insufficient_security, no_suitable_groups}} -> ?ALERT_REC(?FATAL, ?INSUFFICIENT_SECURITY, no_suitable_groups); @@ -590,20 +622,18 @@ do_start(#client_hello{cipher_suites = ClientCiphers, %% TLS Client do_start(#server_hello{cipher_suite = SelectedCipherSuite, session_id = SessionId, - extensions = Extensions} = _ServerHello, + extensions = Extensions}, #state{static_env = #static_env{role = client, host = Host, port = Port, transport_cb = Transport, - socket = Socket, - session_cache = Cache, - session_cache_cb = CacheCb}, - handshake_env = #handshake_env{renegotiation = {Renegotiation, _}, - tls_handshake_history = _HHistory} = HsEnv, - connection_env = CEnv, + socket = Socket}, + handshake_env = #handshake_env{renegotiation = {Renegotiation, _}} = HsEnv, + connection_env = #connection_env{negotiated_version = NegotiatedVersion}, ssl_options = #{ciphers := ClientCiphers, supported_groups := ClientGroups0, - versions := Versions, + use_ticket := UseTicket, + session_tickets := SessionTickets, log_level := LogLevel} = SslOpts, session = #session{own_certificate = Cert} = Session0, connection_states = ConnectionStates0 @@ -632,10 +662,10 @@ do_start(#server_hello{cipher_suite = SelectedCipherSuite, %% new KeyShareEntry for the group indicated in the selected_group field %% of the triggering HelloRetryRequest. ClientKeyShare = ssl_cipher:generate_client_shares([SelectedGroup]), - Hello = tls_handshake:client_hello(Host, Port, ConnectionStates0, SslOpts, - Cache, CacheCb, Renegotiation, Cert, ClientKeyShare), - - HelloVersion = tls_record:hello_version(Versions), + TicketData = get_ticket_data(self(), SessionTickets, UseTicket), + Hello0 = tls_handshake:client_hello(Host, Port, ConnectionStates0, SslOpts, + SessionId, Renegotiation, Cert, ClientKeyShare, + TicketData), %% Update state State1 = update_start_state(State0, @@ -646,19 +676,21 @@ do_start(#server_hello{cipher_suite = SelectedCipherSuite, %% Replace ClientHello1 with a special synthetic handshake message State2 = replace_ch1_with_message_hash(State1), - #state{handshake_env = #handshake_env{tls_handshake_history = HHistory}} = State2, + #state{handshake_env = #handshake_env{tls_handshake_history = HHistory0}} = State2, + + %% Update pre_shared_key extension with binders (TLS 1.3) + Hello = tls_handshake_1_3:maybe_add_binders(Hello0, HHistory0, TicketData, NegotiatedVersion), - {BinMsg, ConnectionStates, Handshake} = - tls_connection:encode_handshake(Hello, HelloVersion, ConnectionStates0, HHistory), + {BinMsg, ConnectionStates, HHistory} = + tls_connection:encode_handshake(Hello, NegotiatedVersion, ConnectionStates0, HHistory0), tls_socket:send(Transport, Socket, BinMsg), ssl_logger:debug(LogLevel, outbound, 'handshake', Hello), ssl_logger:debug(LogLevel, outbound, 'record', BinMsg), State = State2#state{ connection_states = ConnectionStates, - connection_env = CEnv#connection_env{negotiated_version = HelloVersion}, %% Requested version session = Session0#session{session_id = Hello#client_hello.session_id}, - handshake_env = HsEnv#handshake_env{tls_handshake_history = Handshake}, + handshake_env = HsEnv#handshake_env{tls_handshake_history = HHistory}, key_share = ClientKeyShare}, {State, wait_sh} @@ -669,36 +701,32 @@ do_start(#server_hello{cipher_suite = SelectedCipherSuite, end. -do_negotiated(start_handshake, +do_negotiated({start_handshake, PSK0}, #state{connection_states = ConnectionStates0, session = #session{session_id = SessionId, - own_certificate = OwnCert, ecc = SelectedGroup, - sign_alg = SignatureScheme, dh_public_value = ClientPublicKey}, ssl_options = #{} = SslOpts, - key_share = KeyShare, - handshake_env = #handshake_env{tls_handshake_history = _HHistory0}, - connection_env = #connection_env{private_key = CertPrivateKey}, - static_env = #static_env{ - cert_db = CertDbHandle, - cert_db_ref = CertDbRef, - socket = _Socket, - transport_cb = _Transport} - } = State0) -> + key_share = KeyShare} = State0) -> ServerPrivateKey = get_server_private_key(KeyShare), - {Ref,Maybe} = maybe(), + #{security_parameters := SecParamsR} = + ssl_record:pending_connection_state(ConnectionStates0, read), + #security_parameters{prf_algorithm = HKDF} = SecParamsR, + + {Ref,Maybe} = maybe(), try %% Create server_hello - %% Extensions: supported_versions, key_share, (pre_shared_key) - ServerHello = server_hello(server_hello, SessionId, KeyShare, ConnectionStates0), + ServerHello = server_hello(server_hello, SessionId, KeyShare, PSK0, ConnectionStates0), {State1, _} = tls_connection:send_handshake(ServerHello, State0), + PSK = get_pre_shared_key(PSK0, HKDF), + State2 = - calculate_handshake_secrets(ClientPublicKey, ServerPrivateKey, SelectedGroup, State1), + calculate_handshake_secrets(ClientPublicKey, ServerPrivateKey, SelectedGroup, + PSK, State1), State3 = ssl_record:step_encryption_state(State2), @@ -709,19 +737,13 @@ do_negotiated(start_handshake, State4 = tls_connection:queue_handshake(EncryptedExtensions, State3), %% Create and send CertificateRequest ({verify, verify_peer}) - {State5, NextState} = maybe_send_certificate_request(State4, SslOpts), + {State5, NextState} = maybe_send_certificate_request(State4, SslOpts, PSK0), - %% Create Certificate - Certificate = Maybe(certificate(OwnCert, CertDbHandle, CertDbRef, <<>>, server)), + %% Create and send Certificate (if PSK is undefined) + State6 = Maybe(maybe_send_certificate(State5, PSK0)), - %% Encode Certificate - State6 = tls_connection:queue_handshake(Certificate, State5), - - %% Create CertificateVerify - CertificateVerify = Maybe(certificate_verify(CertPrivateKey, SignatureScheme, - State6, server)), - %% Encode CertificateVerify - State7 = tls_connection:queue_handshake(CertificateVerify, State6), + %% Create and send CertificateVerify (if PSK is undefined) + State7 = Maybe(maybe_send_certificate_verify(State6, PSK0)), %% Create Finished Finished = finished(State7), @@ -779,17 +801,19 @@ do_wait_cv(#certificate_verify_1_3{} = CertificateVerify, State0) -> %% TLS Server do_wait_finished(#finished{verify_data = VerifyData}, #state{static_env = #static_env{role = server}} = State0) -> - {Ref,Maybe} = maybe(), try Maybe(validate_client_finished(State0, VerifyData)), State1 = calculate_traffic_secrets(State0), + State2 = maybe_calculate_resumption_master_secret(State1), %% Configure traffic keys - ssl_record:step_encryption_state(State1) + State3 = ssl_record:step_encryption_state(State2), + %% Send session ticket + maybe_send_session_ticket(State3, 3) catch {Ref, decrypt_error} -> @@ -816,9 +840,10 @@ do_wait_finished(#finished{verify_data = _VerifyData}, {State3, _} = tls_connection:send_handshake_flight(State2), State4 = calculate_traffic_secrets(State3), + State5 = maybe_calculate_resumption_master_secret(State4), %% Configure traffic keys - ssl_record:step_encryption_state(State4) + ssl_record:step_encryption_state(State5) catch {Ref, decrypt_error} -> @@ -835,9 +860,13 @@ do_wait_sh(#server_hello{cipher_suite = SelectedCipherSuite, extensions = Extensions} = ServerHello, #state{key_share = ClientKeyShare0, ssl_options = #{ciphers := ClientCiphers, - supported_groups := ClientGroups0}} = State0) -> + supported_groups := ClientGroups0, + session_tickets := SessionTickets, + use_ticket := UseTicket}} = State0) -> ClientGroups = get_supported_groups(ClientGroups0), ServerKeyShare0 = maps:get(key_share, Extensions, undefined), + ServerPreSharedKey = maps:get(pre_shared_key, Extensions, undefined), + SelectedIdentity = get_selected_identity(ServerPreSharedKey), ClientKeyShare = get_key_shares(ClientKeyShare0), {Ref,Maybe} = maybe(), @@ -845,6 +874,8 @@ do_wait_sh(#server_hello{cipher_suite = SelectedCipherSuite, %% Go to state 'start' if server replies with 'HelloRetryRequest'. Maybe(maybe_hello_retry_request(ServerHello, State0)), + %% Resumption and PSK + State1 = handle_resumption(State0, SelectedIdentity), ServerKeyShare = get_key_shares(ServerKeyShare0), Maybe(validate_cipher_suite(SelectedCipherSuite, ClientCiphers)), @@ -856,18 +887,24 @@ do_wait_sh(#server_hello{cipher_suite = SelectedCipherSuite, {_, ClientPrivateKey} = get_client_private_key([SelectedGroup], ClientKeyShare), %% Update state - State1 = update_start_state(State0, + State2 = update_start_state(State1, #{cipher => SelectedCipherSuite, key_share => ClientKeyShare0, session_id => SessionId, group => SelectedGroup, peer_public_key => ServerPublicKey}), - State2 = calculate_handshake_secrets(ServerPublicKey, ClientPrivateKey, SelectedGroup, State1), + #state{connection_states = ConnectionStates} = State2, + #{security_parameters := SecParamsR} = + ssl_record:pending_connection_state(ConnectionStates, read), + #security_parameters{prf_algorithm = HKDFAlgo} = SecParamsR, - State3 = ssl_record:step_encryption_state(State2), + PSK = Maybe(get_pre_shared_key(SessionTickets, UseTicket, HKDFAlgo, SelectedIdentity)), + State3 = calculate_handshake_secrets(ServerPublicKey, ClientPrivateKey, SelectedGroup, + PSK, State2), + State4 = ssl_record:step_encryption_state(State3), - {State3, wait_ee} + {State4, wait_ee} catch {Ref, {State, StateName, ServerHello}} -> @@ -890,9 +927,12 @@ do_wait_ee(#encrypted_extensions{extensions = Extensions}, State0) -> ALPNProtocol0 = maps:get(alpn, Extensions, undefined), ALPNProtocol = get_alpn(ALPNProtocol0), - {Ref,_Maybe} = maybe(), + {Ref, Maybe} = maybe(), try + %% Go to state 'wait_finished' if using PSK. + Maybe(maybe_resumption(State0)), + %% Update state #state{handshake_env = HsEnv} = State0, State1 = State0#state{handshake_env = HsEnv#handshake_env{alpn = ALPNProtocol}}, @@ -908,7 +948,9 @@ do_wait_ee(#encrypted_extensions{extensions = Extensions}, State0) -> {Ref, {insufficient_security, no_suitable_signature_algorithm}} -> ?ALERT_REC(?FATAL, ?INSUFFICIENT_SECURITY, "No suitable signature algorithm"); {Ref, {insufficient_security, no_suitable_public_key}} -> - ?ALERT_REC(?FATAL, ?INSUFFICIENT_SECURITY, no_suitable_public_key) + ?ALERT_REC(?FATAL, ?INSUFFICIENT_SECURITY, no_suitable_public_key); + {Ref, {State, StateName}} -> + {State, StateName} end. @@ -974,6 +1016,19 @@ maybe_hello_retry_request(_, _) -> ok. +maybe_resumption(#state{handshake_env = #handshake_env{resumption = true}} = State) -> + {error, {State, wait_finished}}; +maybe_resumption(_) -> + ok. + + +handle_resumption(State, undefined) -> + State; +handle_resumption(#state{handshake_env = HSEnv0} = State, _) -> + HSEnv = HSEnv0#handshake_env{resumption = true}, + State#state{handshake_env = HSEnv}. + + maybe_queue_cert_cert_cv(#state{client_certificate_requested = false} = State) -> {ok, State}; maybe_queue_cert_cert_cv(#state{connection_states = _ConnectionStates0, @@ -1054,7 +1109,7 @@ compare_verify_data(_, _) -> send_hello_retry_request(#state{connection_states = ConnectionStates0} = State0, no_suitable_key, KeyShare, SessionId) -> - ServerHello = server_hello(hello_retry_request, SessionId, KeyShare, ConnectionStates0), + ServerHello = server_hello(hello_retry_request, SessionId, KeyShare, undefined, ConnectionStates0), {State1, _} = tls_connection:send_handshake(ServerHello, State0), %% Update handshake history @@ -1065,16 +1120,72 @@ send_hello_retry_request(State0, _, _, _) -> %% Suitable key found. {ok, {State0, negotiated}}. +session_resumption({#state{ssl_options = #{session_tickets := disabled}} = State, negotiated}, _) -> + {ok, {State, negotiated}}; +session_resumption({#state{ssl_options = #{session_tickets := Tickets}} = State, negotiated}, undefined) + when Tickets =/= disabled -> + {ok, {State, negotiated}}; +session_resumption({#state{ssl_options = #{session_tickets := Tickets}} = State0, negotiated}, PSK) + when Tickets =/= disabled -> + State = handle_resumption(State0, ok), + {ok, {State, negotiated, PSK}}. -maybe_send_certificate_request(State, #{verify := verify_none}) -> + +%% Do not send CR during session resumption +maybe_send_certificate_request(State, _, PSK) when PSK =/= undefined -> + {State, wait_finished}; +maybe_send_certificate_request(State, #{verify := verify_none}, _) -> {State, wait_finished}; maybe_send_certificate_request(State, #{verify := verify_peer, signature_algs := SignAlgs, - signature_algs_cert := SignAlgsCert}) -> + signature_algs_cert := SignAlgsCert}, _) -> CertificateRequest = certificate_request(SignAlgs, SignAlgsCert), {tls_connection:queue_handshake(CertificateRequest, State), wait_cert}. +maybe_send_certificate(State, PSK) when PSK =/= undefined -> + {ok, State}; +maybe_send_certificate(#state{session = #session{own_certificate = OwnCert}, + static_env = #static_env{ + cert_db = CertDbHandle, + cert_db_ref = CertDbRef}} = State, _) -> + case certificate(OwnCert, CertDbHandle, CertDbRef, <<>>, server) of + {ok, Certificate} -> + {ok, tls_connection:queue_handshake(Certificate, State)}; + Error -> + Error + end. + + +maybe_send_certificate_verify(State, PSK) when PSK =/= undefined -> + {ok, State}; +maybe_send_certificate_verify(#state{session = #session{sign_alg = SignatureScheme}, + connection_env = #connection_env{ + private_key = CertPrivateKey}} = State, _) -> + case certificate_verify(CertPrivateKey, SignatureScheme, State, server) of + {ok, CertificateVerify} -> + {ok, tls_connection:queue_handshake(CertificateVerify, State)}; + Error -> + Error + end. + + +maybe_send_session_ticket(#state{ssl_options = #{session_tickets := disabled}} = State, _) -> + %% Do nothing! + State; +maybe_send_session_ticket(State, 0) -> + State; +maybe_send_session_ticket(#state{connection_states = ConnectionStates, + static_env = #static_env{trackers = Trackers}} = State0, N) -> + Tracker = proplists:get_value(session_tickets_tracker, Trackers), + #{security_parameters := SecParamsR} = + ssl_record:current_connection_state(ConnectionStates, read), + #security_parameters{prf_algorithm = HKDF, + resumption_master_secret = RMS} = SecParamsR, + Ticket = tls_server_session_ticket:new(Tracker, HKDF, RMS), + {State, _} = tls_connection:send_handshake(Ticket, State0), + maybe_send_session_ticket(State, N - 1). + process_certificate_request(#certificate_request_1_3{}, #state{session = #session{own_certificate = undefined}} = State) -> {ok, {State#state{client_certificate_requested = true}, wait_cert}}; @@ -1186,6 +1297,7 @@ validate_certificate_chain(Certs, CertDbHandle, CertDbRef, verify_fun := VerifyFun, customize_hostname_check := CustomizeHostnameCheck, crl_check := CrlCheck, + log_level := LogLevel, depth := Depth} = SslOptions, CRLDbHandle, Role, Host) -> ServerName = ssl_handshake:server_name(ServerNameIndication, Host, Role), @@ -1198,7 +1310,7 @@ validate_certificate_chain(Certs, CertDbHandle, CertDbRef, ssl_handshake:validation_fun_and_state(VerifyFun, Role, CertDbHandle, CertDbRef, ServerName, CustomizeHostnameCheck, - CrlCheck, CRLDbHandle, CertPath), + CrlCheck, CRLDbHandle, CertPath, LogLevel), Options = [{max_path_length, Depth}, {verify_fun, ValidationFunAndState}], %% TODO: Validate if Certificate is using a supported signature algorithm @@ -1271,7 +1383,7 @@ message_hash(ClientHello1, HKDFAlgo) -> crypto:hash(HKDFAlgo, ClientHello1)]. -calculate_handshake_secrets(PublicKey, PrivateKey, SelectedGroup, +calculate_handshake_secrets(PublicKey, PrivateKey, SelectedGroup, PSK, #state{connection_states = ConnectionStates, handshake_env = #handshake_env{ @@ -1280,9 +1392,6 @@ calculate_handshake_secrets(PublicKey, PrivateKey, SelectedGroup, ssl_record:pending_connection_state(ConnectionStates, read), #security_parameters{prf_algorithm = HKDFAlgo, cipher_suite = CipherSuite} = SecParamsR, - - %% Calculate handshake_secret - PSK = binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo)), EarlySecret = tls_v1:key_schedule(early_secret, HKDFAlgo , {psk, PSK}), IKM = calculate_shared_secret(PublicKey, PrivateKey, SelectedGroup), @@ -1305,11 +1414,63 @@ calculate_handshake_secrets(PublicKey, PrivateKey, SelectedGroup, ReadFinishedKey = tls_v1:finished_key(ClientHSTrafficSecret, HKDFAlgo), WriteFinishedKey = tls_v1:finished_key(ServerHSTrafficSecret, HKDFAlgo), - update_pending_connection_states(State0, HandshakeSecret, + update_pending_connection_states(State0, HandshakeSecret, undefined, ReadKey, ReadIV, ReadFinishedKey, WriteKey, WriteIV, WriteFinishedKey). +%% Server +get_pre_shared_key(undefined, HKDFAlgo) -> + binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo)); +get_pre_shared_key({_, PSK}, _) -> + PSK. +%% +%% Client +%% Server initiates a full handshake +get_pre_shared_key(_, _, HKDFAlgo, undefined) -> + {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))}; +%% Session resumption not configured +get_pre_shared_key(undefined, _, HKDFAlgo, _) -> + {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))}; +get_pre_shared_key(_, undefined, HKDFAlgo, _) -> + {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))}; +%% Session resumption +get_pre_shared_key(enabled = SessionTickets, UseTicket, HKDFAlgo, SelectedIdentity) -> + TicketData = get_ticket_data(self(), SessionTickets, UseTicket), + case choose_psk(TicketData, SelectedIdentity) of + undefined -> %% full handshake, default PSK + {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))}; + illegal_parameter -> + {error, illegal_parameter}; + {_, PSK} -> + {ok, PSK} + end; +get_pre_shared_key(auto = SessionTickets, UseTicket, HKDFAlgo, SelectedIdentity) -> + TicketData = get_ticket_data(self(), SessionTickets, UseTicket), + case choose_psk(TicketData, SelectedIdentity) of + undefined -> %% full handshake, default PSK + tls_client_ticket_store:unlock_tickets(self(), UseTicket), + {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))}; + illegal_parameter -> + tls_client_ticket_store:unlock_tickets(self(), UseTicket), + {error, illegal_parameter}; + {Key, PSK} -> + tls_client_ticket_store:remove_tickets([Key]), %% Remove single-use ticket + tls_client_ticket_store:unlock_tickets(self(), UseTicket -- [Key]), + {ok, PSK} + end. + + +choose_psk(undefined, _) -> + undefined; +choose_psk([], _) -> + illegal_parameter; +choose_psk([{Key, SelectedIdentity, _, PSK, _, _}|_], SelectedIdentity) -> + {Key, PSK}; +choose_psk([_|T], SelectedIdentity) -> + choose_psk(T, SelectedIdentity). + + calculate_traffic_secrets(#state{ static_env = #static_env{role = Role}, connection_states = ConnectionStates, @@ -1339,7 +1500,7 @@ calculate_traffic_secrets(#state{ {ReadKey, ReadIV} = tls_v1:calculate_traffic_keys(HKDFAlgo, Cipher, ClientAppTrafficSecret0), {WriteKey, WriteIV} = tls_v1:calculate_traffic_keys(HKDFAlgo, Cipher, ServerAppTrafficSecret0), - update_pending_connection_states(State0, MasterSecret, + update_pending_connection_states(State0, MasterSecret, undefined, ReadKey, ReadIV, undefined, WriteKey, WriteIV, undefined). @@ -1374,17 +1535,35 @@ calculate_shared_secret(OthersKey, MyKey = #'ECPrivateKey'{}, _Group) public_key:compute_key(Point, MyKey). +maybe_calculate_resumption_master_secret(#state{ssl_options = #{session_tickets := disabled}} = State) -> + State; +maybe_calculate_resumption_master_secret(#state{ + ssl_options = #{session_tickets := SessionTickets}, + connection_states = ConnectionStates, + handshake_env = + #handshake_env{ + tls_handshake_history = HHistory}} = State) + when SessionTickets =/= disabled -> + #{security_parameters := SecParamsR} = + ssl_record:pending_connection_state(ConnectionStates, read), + #security_parameters{master_secret = MasterSecret, + prf_algorithm = HKDFAlgo} = SecParamsR, + {Messages0, _} = HHistory, + RMS = tls_v1:resumption_master_secret(HKDFAlgo, MasterSecret, lists:reverse(Messages0)), + update_resumption_master_secret(State, RMS). + + update_pending_connection_states(#state{ static_env = #static_env{role = server}, connection_states = CS = #{pending_read := PendingRead0, pending_write := PendingWrite0}} = State, - HandshakeSecret, + HandshakeSecret, ResumptionMasterSecret, ReadKey, ReadIV, ReadFinishedKey, WriteKey, WriteIV, WriteFinishedKey) -> - PendingRead = update_connection_state(PendingRead0, HandshakeSecret, + PendingRead = update_connection_state(PendingRead0, HandshakeSecret, ResumptionMasterSecret, ReadKey, ReadIV, ReadFinishedKey), - PendingWrite = update_connection_state(PendingWrite0, HandshakeSecret, + PendingWrite = update_connection_state(PendingWrite0, HandshakeSecret, ResumptionMasterSecret, WriteKey, WriteIV, WriteFinishedKey), State#state{connection_states = CS#{pending_read => PendingRead, pending_write => PendingWrite}}; @@ -1393,22 +1572,23 @@ update_pending_connection_states(#state{ connection_states = CS = #{pending_read := PendingRead0, pending_write := PendingWrite0}} = State, - HandshakeSecret, + HandshakeSecret, ResumptionMasterSecret, ReadKey, ReadIV, ReadFinishedKey, WriteKey, WriteIV, WriteFinishedKey) -> - PendingRead = update_connection_state(PendingRead0, HandshakeSecret, + PendingRead = update_connection_state(PendingRead0, HandshakeSecret, ResumptionMasterSecret, WriteKey, WriteIV, WriteFinishedKey), - PendingWrite = update_connection_state(PendingWrite0, HandshakeSecret, + PendingWrite = update_connection_state(PendingWrite0, HandshakeSecret, ResumptionMasterSecret, ReadKey, ReadIV, ReadFinishedKey), State#state{connection_states = CS#{pending_read => PendingRead, pending_write => PendingWrite}}. update_connection_state(ConnectionState = #{security_parameters := SecurityParameters0}, - HandshakeSecret, Key, IV, FinishedKey) -> + HandshakeSecret, ResumptionMasterSecret, Key, IV, FinishedKey) -> %% Store secret SecurityParameters = SecurityParameters0#security_parameters{ - master_secret = HandshakeSecret}, + master_secret = HandshakeSecret, + resumption_master_secret = ResumptionMasterSecret}, ConnectionState#{security_parameters => SecurityParameters, cipher_state => cipher_init(Key, IV, FinishedKey)}. @@ -1451,6 +1631,21 @@ update_start_state(#state{connection_states = ConnectionStates0, connection_env = CEnv#connection_env{negotiated_version = {3,4}}}. +update_resumption_master_secret(#state{connection_states = ConnectionStates0} = State, + ResumptionMasterSecret) -> + #{security_parameters := SecParamsR0} = PendingRead = + maps:get(pending_read, ConnectionStates0), + #{security_parameters := SecParamsW0} = PendingWrite = + maps:get(pending_write, ConnectionStates0), + + SecParamsR = SecParamsR0#security_parameters{resumption_master_secret = ResumptionMasterSecret}, + SecParamsW = SecParamsW0#security_parameters{resumption_master_secret = ResumptionMasterSecret}, + ConnectionStates = + ConnectionStates0#{pending_read => PendingRead#{security_parameters => SecParamsR}, + pending_write => PendingWrite#{security_parameters => SecParamsW}}, + State#state{connection_states = ConnectionStates}. + + cipher_init(Key, IV, FinishedKey) -> #cipher_state{key = Key, iv = IV, @@ -1882,6 +2077,42 @@ get_key_shares(#key_share_client_hello{client_shares = ClientShares}) -> get_key_shares(#key_share_server_hello{server_share = ServerShare}) -> ServerShare. +get_selected_identity(undefined) -> + undefined; +get_selected_identity(#pre_shared_key_server_hello{selected_identity = SelectedIdentity}) -> + SelectedIdentity. + +get_offered_psks(Extensions) -> + PSK = maps:get(pre_shared_key, Extensions, undefined), + case PSK of + undefined -> + undefined; + #pre_shared_key_client_hello{offered_psks = OfferedPSKs} -> + OfferedPSKs + end. + + +%% Prior to accepting PSK key establishment, the server MUST validate +%% the corresponding binder value (see Section 4.2.11.2 below). If this +%% value is not present or does not validate, the server MUST abort the +%% handshake. Servers SHOULD NOT attempt to validate multiple binders; +%% rather, they SHOULD select a single PSK and validate solely the +%% binder that corresponds to that PSK. +%% +%% If no acceptable PSKs are found, the server SHOULD perform a non-PSK +%% handshake if possible. +handle_pre_shared_key(_, undefined, _) -> + {ok, undefined}; +handle_pre_shared_key(#state{ssl_options = #{session_tickets := disabled}}, _, _) -> + {ok, undefined}; +handle_pre_shared_key(#state{ssl_options = #{session_tickets := Tickets}, + handshake_env = #handshake_env{tls_handshake_history = {HHistory, _}}, + static_env = #static_env{trackers = Trackers}}, + OfferedPreSharedKeys, Cipher) when Tickets =/= disabled -> + Tracker = proplists:get_value(session_tickets_tracker, Trackers), + #{prf := CipherHash} = ssl_cipher_format:suite_bin_to_map(Cipher), + tls_server_session_ticket:use(Tracker, OfferedPreSharedKeys, CipherHash, HHistory). + get_selected_group(#key_share_hello_retry_request{selected_group = SelectedGroup}) -> SelectedGroup. @@ -1901,3 +2132,195 @@ maybe() -> throw({Ref,Reason}) end, {Ref,Ok}. + + +%% If the handshake includes a HelloRetryRequest, the initial +%% ClientHello and HelloRetryRequest are included in the transcript +%% along with the new ClientHello. For instance, if the client sends +%% ClientHello1, its binder will be computed over: +%% +%% Transcript-Hash(Truncate(ClientHello1)) +%% +%% Where Truncate() removes the binders list from the ClientHello. +%% +%% If the server responds with a HelloRetryRequest and the client then +%% sends ClientHello2, its binder will be computed over: +%% +%% Transcript-Hash(ClientHello1, +%% HelloRetryRequest, +%% Truncate(ClientHello2)) +%% +%% The full ClientHello1/ClientHello2 is included in all other handshake +%% hash computations. Note that in the first flight, +%% Truncate(ClientHello1) is hashed directly, but in the second flight, +%% ClientHello1 is hashed and then reinjected as a "message_hash" +%% message, as described in Section 4.4.1. +maybe_add_binders(Hello, undefined, _) -> + Hello; +maybe_add_binders(Hello0, TicketData, Version) when Version =:= {3,4} -> + HelloBin0 = tls_handshake:encode_handshake(Hello0, Version), + HelloBin1 = iolist_to_binary(HelloBin0), + Truncated = truncate_client_hello(HelloBin1), + Binders = create_binders([Truncated], TicketData), + update_binders(Hello0, Binders); +maybe_add_binders(Hello, _, Version) when Version =< {3,3} -> + Hello. +%% +%% HelloRetryRequest +maybe_add_binders(Hello, _, undefined, _) -> + Hello; +maybe_add_binders(Hello0, {[HRR,MessageHash|_], _}, TicketData, Version) when Version =:= {3,4} -> + HelloBin0 = tls_handshake:encode_handshake(Hello0, Version), + HelloBin1 = iolist_to_binary(HelloBin0), + Truncated = truncate_client_hello(HelloBin1), + Binders = create_binders([MessageHash,HRR,Truncated], TicketData), + update_binders(Hello0, Binders); +maybe_add_binders(Hello, _, _, Version) when Version =< {3,3} -> + Hello. + + +create_binders(Context, TicketData) -> + create_binders(Context, TicketData, []). +%% +create_binders(_, [], Acc) -> + lists:reverse(Acc); +create_binders(Context, [{_, _, _, PSK, _, HKDF}|T], Acc) -> + FinishedKey = calculate_finished_key(PSK, HKDF), + Binder = calculate_binder(FinishedKey, HKDF, Context), + create_binders(Context, T, [Binder|Acc]). + + +%% Removes the binders list from the ClientHello. +%% opaque PskBinderEntry<32..255>; +%% +%% struct { +%% PskIdentity identities<7..2^16-1>; +%% PskBinderEntry binders<33..2^16-1>; +%% } OfferedPsks; +truncate_client_hello(HelloBin0) -> + HelloBin1 = remove_binders(HelloBin0), + {Truncated, _} = split_binary(HelloBin1, size(HelloBin1) - 2), + Truncated. + + +remove_binders(Binary0) -> + OrigSize = byte_size(Binary0), + HashSize256 = ssl_cipher:hash_size(sha256), + HashSize384 = ssl_cipher:hash_size(sha384), + HashSize512 = ssl_cipher:hash_size(sha512), + + NewSize256 = OrigSize - HashSize256 - 1, + NewSize384 = OrigSize - HashSize384 - 1, + NewSize512 = OrigSize - HashSize512 - 1, + case Binary0 of + <<Binary:NewSize256/binary,?BYTE(HashSize256),_:HashSize256/binary>> -> + remove_binders(Binary); + <<Binary:NewSize384/binary,?BYTE(HashSize384),_:HashSize384/binary>> -> + remove_binders(Binary); + <<Binary:NewSize512/binary,?BYTE(HashSize512),_:HashSize512/binary>> -> + remove_binders(Binary); + Else -> + Else + end. + + +%% The PskBinderEntry is computed in the same way as the Finished +%% message (Section 4.4.4) but with the BaseKey being the binder_key +%% derived via the key schedule from the corresponding PSK which is +%% being offered (see Section 7.1). +calculate_finished_key(PSK, HKDFAlgo) -> + EarlySecret = tls_v1:key_schedule(early_secret, HKDFAlgo , {psk, PSK}), + PRK = tls_v1:resumption_binder_key(HKDFAlgo, EarlySecret), + tls_v1:finished_key(PRK, HKDFAlgo). + + +calculate_binder(BinderKey, HKDF, Truncated) -> + tls_v1:finished_verify_data(BinderKey, HKDF, [Truncated]). + + +update_binders(#client_hello{extensions = + #{pre_shared_key := PreSharedKey0} = Extensions0} = Hello, Binders) -> + #pre_shared_key_client_hello{ + offered_psks = + #offered_psks{identities = Identities}} = PreSharedKey0, + + PreSharedKey = + #pre_shared_key_client_hello{ + offered_psks = + #offered_psks{identities = Identities, + binders = Binders}}, + + Extensions = Extensions0#{pre_shared_key => PreSharedKey}, + Hello#client_hello{extensions = Extensions}. + +%% Configure a suitable session ticket +maybe_automatic_session_resumption(#state{ + ssl_options = #{versions := [Version|_], + ciphers := UserSuites, + session_tickets := SessionTickets} = SslOpts0 + } = State0) + when Version >= {3,4} andalso + SessionTickets =:= auto -> + AvailableCipherSuites = ssl_handshake:available_suites(UserSuites, Version), + HashAlgos = cipher_hash_algos(AvailableCipherSuites), + UseTicket = tls_client_ticket_store:find_ticket(self(), HashAlgos), + tls_client_ticket_store:lock_tickets(self(), [UseTicket]), + State = State0#state{ssl_options = SslOpts0#{use_ticket => [UseTicket]}}, + {[UseTicket], State}; +maybe_automatic_session_resumption(#state{ + ssl_options = #{use_ticket := UseTicket} + } = State) -> + {UseTicket, State}. + + +cipher_hash_algos(Ciphers) -> + Fun = fun(Cipher) -> + #{prf := Hash} = ssl_cipher_format:suite_bin_to_map(Cipher), + Hash + end, + lists:map(Fun, Ciphers). + + +get_ticket_data(_, undefined, _) -> + undefined; +get_ticket_data(_, _, undefined) -> + undefined; +get_ticket_data(_, enabled, UseTicket) -> + process_user_tickets(UseTicket); +get_ticket_data(Pid, auto, UseTicket) -> + tls_client_ticket_store:get_tickets(Pid, UseTicket). + + +process_user_tickets(UseTicket) -> + process_user_tickets(UseTicket, [], 0). +%% +process_user_tickets([], Acc, _) -> + lists:reverse(Acc); +process_user_tickets([H|T], Acc, N) -> + #{hkdf := HKDF, + sni := _SNI, + psk := PSK, + timestamp := Timestamp, + ticket := NewSessionTicket} = erlang:binary_to_term(H), + #new_session_ticket{ + ticket_lifetime = _LifeTime, + ticket_age_add = AgeAdd, + ticket_nonce = Nonce, + ticket = Ticket, + extensions = _Extensions + } = NewSessionTicket, + TicketAge = erlang:system_time(seconds) - Timestamp, + ObfuscatedTicketAge = obfuscate_ticket_age(TicketAge, AgeAdd), + Identity = #psk_identity{ + identity = Ticket, + obfuscated_ticket_age = ObfuscatedTicketAge}, + process_user_tickets(T, [{undefined, N, Identity, PSK, Nonce, HKDF}|Acc], N + 1). + + +%% The "obfuscated_ticket_age" +%% field of each PskIdentity contains an obfuscated version of the +%% ticket age formed by taking the age in milliseconds and adding the +%% "ticket_age_add" value that was included with the ticket +%% (see Section 4.6.1), modulo 2^32. +obfuscate_ticket_age(TicketAge, AgeAdd) -> + (TicketAge + AgeAdd) rem round(math:pow(2,32)). diff --git a/lib/ssl/src/tls_handshake_1_3.hrl b/lib/ssl/src/tls_handshake_1_3.hrl index eb85f216c8..cb28ca78f6 100644 --- a/lib/ssl/src/tls_handshake_1_3.hrl +++ b/lib/ssl/src/tls_handshake_1_3.hrl @@ -254,8 +254,10 @@ -type tls_handshake_1_3() :: #encrypted_extensions{} | #certificate_request_1_3{} | #certificate_1_3{} | - #certificate_verify_1_3{}. + #certificate_verify_1_3{} | + #new_session_ticket{}. -export_type([tls_handshake_1_3/0]). + -endif. % -ifdef(tls_handshake_1_3). diff --git a/lib/ssl/src/tls_record.erl b/lib/ssl/src/tls_record.erl index cfd75076b3..e8040b461d 100644 --- a/lib/ssl/src/tls_record.erl +++ b/lib/ssl/src/tls_record.erl @@ -391,7 +391,7 @@ is_acceptable_version(_,_) -> -spec hello_version([tls_version()]) -> tls_version(). hello_version([Highest|_]) when Highest >= {3,3} -> - Highest; + {3,3}; hello_version(Versions) -> lowest_protocol_version(Versions). diff --git a/lib/ssl/src/tls_sender.erl b/lib/ssl/src/tls_sender.erl index d0604565e3..50811ef3dd 100644 --- a/lib/ssl/src/tls_sender.erl +++ b/lib/ssl/src/tls_sender.erl @@ -43,7 +43,7 @@ role, socket, socket_options, - tracker, + trackers, transport_cb, negotiated_version, renegotiate_at, @@ -200,7 +200,7 @@ init({call, From}, {Pid, #{current_write := WriteState, role := Role, socket := Socket, socket_options := SockOpts, - tracker := Tracker, + trackers := Trackers, transport_cb := Transport, negotiated_version := Version, renegotiate_at := RenegotiateAt, @@ -214,7 +214,7 @@ init({call, From}, {Pid, #{current_write := WriteState, role = Role, socket = Socket, socket_options = SockOpts, - tracker = Tracker, + trackers = Trackers, transport_cb = Transport, negotiated_version = Version, renegotiate_at = RenegotiateAt, @@ -255,8 +255,8 @@ connection({call, From}, dist_get_tls_socket, #data{static = #static{transport_cb = Transport, socket = Socket, connection_pid = Pid, - tracker = Tracker}} = StateData) -> - TLSSocket = tls_connection:socket([Pid, self()], Transport, Socket, Tracker), + trackers = Trackers}} = StateData) -> + TLSSocket = tls_connection:socket([Pid, self()], Transport, Socket, Trackers), {next_state, ?FUNCTION_NAME, StateData, [{reply, From, {ok, TLSSocket}}]}; connection({call, From}, {dist_handshake_complete, _Node, DHandle}, #data{static = #static{connection_pid = Pid} = Static} = StateData) -> @@ -394,16 +394,13 @@ handle_common( info, {'DOWN', Monitor, _, _, _}, #data{static = #static{connection_monitor = Monitor}} = StateData) -> {stop, normal, StateData}; -handle_common(info, Msg, _) -> - Report = - io_lib:format("TLS sender: Got unexpected info: ~p ~n", [Msg]), - error_logger:info_report(Report), +handle_common(info, Msg, #data{static = #static{log_level = Level}}) -> + ssl_logger:log(info, Level, #{event => "TLS sender recived unexpected info", + reason => [{message, Msg}]}, ?LOCATION), keep_state_and_data; -handle_common(Type, Msg, _) -> - Report = - io_lib:format( - "TLS sender: Got unexpected event: ~p ~n", [{Type,Msg}]), - error_logger:error_report(Report), +handle_common(Type, Msg, #data{static = #static{log_level = Level}}) -> + ssl_logger:log(error, Level, #{event => "TLS sender recived unexpected event", + reason => [{type, Type}, {message, Msg}]}, ?LOCATION), keep_state_and_data. send_tls_alert(#alert{} = Alert, diff --git a/lib/ssl/src/tls_server_session_ticket.erl b/lib/ssl/src/tls_server_session_ticket.erl new file mode 100644 index 0000000000..e30205f4b8 --- /dev/null +++ b/lib/ssl/src/tls_server_session_ticket.erl @@ -0,0 +1,391 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2007-2019. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +%%---------------------------------------------------------------------- +%% Purpose: Handle server side TLS-1.3 session ticket storage +%%---------------------------------------------------------------------- + +-module(tls_server_session_ticket). +-behaviour(gen_server). + +-include("tls_handshake_1_3.hrl"). +-include("ssl_internal.hrl"). +-include("ssl_alert.hrl"). +-include("ssl_cipher.hrl"). + +%% API +-export([start_link/3, + new/3, + use/4 + ]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3, format_status/2]). + +-define(SERVER, ?MODULE). + +-record(state, { + stateless, + stateful, + nonce, + lifetime + }). + +%%%=================================================================== +%%% API +%%%=================================================================== +-spec start_link(atom(), integer(), tuple()) -> {ok, Pid :: pid()} | + {error, Error :: {already_started, pid()}} | + {error, Error :: term()} | + ignore. +start_link(Mode, Lifetime, AntiReplay) -> + gen_server:start_link(?MODULE, [Mode, Lifetime, AntiReplay], []). + +new(Pid, Prf, MasterSecret) -> + gen_server:call(Pid, {new_session_ticket, Prf, MasterSecret}, infinity). + +use(Pid, Identifiers, Prf, HandshakeHist) -> + gen_server:call(Pid, {use_ticket, Identifiers, Prf, HandshakeHist}, + infinity). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +-spec init(Args :: term()) -> {ok, State :: term()}. +init(Args) -> + process_flag(trap_exit, true), + State = inital_state(Args), + {ok, State}. + +-spec handle_call(Request :: term(), From :: {pid(), term()}, State :: term()) -> + {reply, Reply :: term(), NewState :: term()} . +handle_call({new_session_ticket, Prf, MasterSecret}, _From, + #state{nonce = Nonce, + lifetime = LifeTime, + stateful = #{}} = State0) -> + Id = stateful_psk_id(), + PSK = tls_v1:pre_shared_key(MasterSecret, ticket_nonce(Nonce), Prf), + SessionTicket = new_session_ticket(Id, Nonce, LifeTime), + State = stateful_ticket_store(Id, SessionTicket, Prf, PSK, State0), + {reply, SessionTicket, State}; +handle_call({new_session_ticket, Prf, MasterSecret}, _From, + #state{nonce = Nonce, + stateless = #{}} = State) -> + BaseSessionTicket = new_session_ticket_base(State), + SessionTicket = generate_statless_ticket(BaseSessionTicket, Prf, + MasterSecret, State), + {reply, SessionTicket, State#state{nonce = Nonce+1}}; +handle_call({use_ticket, Identifiers, Prf, HandshakeHist}, _From, + #state{stateful = #{}} = State0) -> + {Result, State} = stateful_use(Identifiers, Prf, + HandshakeHist, State0), + {reply, Result, State}; +handle_call({use_ticket, Identifiers, Prf, HandshakeHist}, _From, + #state{stateless = #{}} = State0) -> + {Result, State} = stateless_use(Identifiers, Prf, + HandshakeHist, State0), + {reply, Result, State}. + +-spec handle_cast(Request :: term(), State :: term()) -> + {noreply, NewState :: term()}. +handle_cast(_Request, State) -> + {noreply, State}. + +-spec handle_info(Info :: timeout() | term(), State :: term()) -> + {noreply, NewState :: term()}. +handle_info(rotate_bloom_filters, + #state{stateless = #{bloom_filter := BloomFilter0, + window := Window} = Stateless} = State) -> + BloomFilter = tls_bloom_filter:rotate(BloomFilter0), + erlang:send_after(Window * 1000, self(), rotate_bloom_filters), + {noreply, State#state{stateless = Stateless#{bloom_filter => BloomFilter}}}; +handle_info(_Info, State) -> + {noreply, State}. + +-spec terminate(Reason :: normal | shutdown | {shutdown, term()} | term(), + State :: term()) -> any(). +terminate(_Reason, _State) -> + ok. + +-spec code_change(OldVsn :: term() | {down, term()}, + State :: term(), + Extra :: term()) -> {ok, NewState :: term()} | + {error, Reason :: term()}. +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + + +-spec format_status(Opt :: normal | terminate, + Status :: list()) -> Status :: term(). +format_status(_Opt, Status) -> + Status. +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +inital_state([stateless, Lifetime, undefined]) -> + #state{nonce = 0, + stateless = #{seed => {crypto:strong_rand_bytes(16), + crypto:strong_rand_bytes(32)}, + window => undefined}, + lifetime = Lifetime + }; +inital_state([stateless, Lifetime, {Window, K, M}]) -> + erlang:send_after(Window * 1000, self(), rotate_bloom_filters), + #state{nonce = 0, + stateless = #{bloom_filter => tls_bloom_filter:new(K, M), + seed => {crypto:strong_rand_bytes(16), + crypto:strong_rand_bytes(32)}, + window => Window}, + lifetime = Lifetime + }; +inital_state([stateful, Lifetime|_]) -> + %% statfeful servers replay + %% protection is that it saves + %% all valid tickets + #state{lifetime = Lifetime, + nonce = 0, + stateful = #{db => stateful_store(), + max => 1000, + ref_index => #{} + } + }. + +ticket_age_add() -> + MaxTicketAge = 7 * 24 * 3600, + IntMax = round(math:pow(2,32)) - 1, + MaxAgeAdd = IntMax - MaxTicketAge, + <<?UINT32(I)>> = crypto:strong_rand_bytes(4), + case I > MaxAgeAdd of + true -> + I - MaxTicketAge; + false -> + I + end. + +ticket_nonce(I) -> + <<?UINT64(I)>>. + +new_session_ticket_base(#state{nonce = Nonce, + lifetime = Lifetime}) -> + new_session_ticket(undefined, Nonce, Lifetime). + +new_session_ticket(Id, Nonce, Lifetime) -> + TicketAgeAdd = ticket_age_add(), + #new_session_ticket{ + ticket = Id, + ticket_lifetime = Lifetime, + ticket_age_add = TicketAgeAdd, + ticket_nonce = ticket_nonce(Nonce), + extensions = #{} + }. + + +validate_binder(Binder, HandshakeHist, PSK, Prf, AlertDetail) -> + case tls_handshake_1_3:is_valid_binder(Binder, HandshakeHist, PSK, Prf) of + true -> + true; + false -> + {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER, AlertDetail)} + end. + +%%%=================================================================== +%%% Stateful store +%%%=================================================================== + +stateful_store() -> + gb_trees:empty(). + +stateful_ticket_store(Ref, NewSessionTicket, Hash, Psk, + #state{nonce = Nonce, + stateful = #{db := Tree0, + max := Max, + ref_index := Index0} = Stateful} + = State0) -> + Id = erlang:monotonic_time(), + StatefulTicket = {NewSessionTicket, Hash, Psk}, + case gb_trees:size(Tree0) of + Max -> + %% Trow away oldes ticket + {_, {#new_session_ticket{ticket = OldRef},_,_}, Tree1} + = gb_trees:take_smallest(Tree0), + Tree = gb_trees:insert(Id, StatefulTicket, Tree1), + Index = maps:without([OldRef], Index0), + State0#state{nonce = Nonce+1, stateful = + Stateful#{db => Tree, + ref_index => Index#{Ref => Id}}}; + _ -> + Tree = gb_trees:insert(Id, StatefulTicket, Tree0), + State0#state{nonce = Nonce+1, stateful = + Stateful#{db => Tree, + ref_index => Index0#{Ref => Id}}} + end. + +stateful_use(#offered_psks{ + identities = Identities, + binders = Binders + }, Prf, HandshakeHist, State) -> + stateful_use(Identities, Binders, Prf, HandshakeHist, 0, State). + +stateful_use([], [], _, _, _, State) -> + {{ok, undefined}, State}; +stateful_use([#psk_identity{identity = Ref} | Refs], [Binder | Binders], + Prf, HandshakeHist, Index, + #state{stateful = #{db := Tree0, + ref_index := RefIndex0} = Stateful} = State) -> + try maps:get(Ref, RefIndex0) of + Key -> + case stateful_usable_ticket(Key, Prf, Binder, + HandshakeHist, Tree0) of + true -> + RefIndex = maps:without([Ref], RefIndex0), + {{_,_, PSK}, Tree} = gb_trees:take(Key, Tree0), + {{ok, {Index, PSK}}, + State#state{stateful = Stateful#{db => Tree, + ref_index => RefIndex}}}; + false -> + stateful_use(Refs, Binders, Prf, + HandshakeHist, Index + 1, State); + {error, _} = Error -> + {Error, State} + end + catch + _:{badkey, Ref} -> + stateful_use(Refs, Binders, Prf, HandshakeHist, Index + 1, State) + end. + +stateful_usable_ticket(Key, Prf, Binder, HandshakeHist, Tree) -> + case gb_trees:lookup(Key, Tree) of + none -> + false; + {value, {NewSessionTicket, Prf, PSK}} -> + case stateful_living_ticket(Key, NewSessionTicket) of + true -> + validate_binder(Binder, HandshakeHist, PSK, Prf, stateful); + _ -> + false + end; + _ -> + false + end. + +stateful_living_ticket(TimeStamp, + #new_session_ticket{ticket_lifetime = LifeTime}) -> + Now = erlang:monotonic_time(), + Lived = erlang:convert_time_unit(Now-TimeStamp, native, seconds), + Lived < LifeTime. + + +stateful_psk_id() -> + term_to_binary(make_ref()). + +%%%=================================================================== +%%% Stateless ticket +%%%=================================================================== +generate_statless_ticket(#new_session_ticket{ticket_nonce = Nonce, + ticket_age_add = TicketAgeAdd, + ticket_lifetime = Lifetime} + = Ticket, Prf, MasterSecret, + #state{stateless = #{seed := {IV, Shard}}}) -> + PSK = tls_v1:pre_shared_key(MasterSecret, Nonce, Prf), + Timestamp = erlang:system_time(second), + Encrypted = ssl_cipher:encrypt_ticket(#stateless_ticket{ + hash = Prf, + pre_shared_key = PSK, + ticket_age_add = TicketAgeAdd, + lifetime = Lifetime, + timestamp = Timestamp + }, Shard, IV), + Ticket#new_session_ticket{ticket = Encrypted}. + +stateless_use(#offered_psks{ + identities = Identities, + binders = Binders + }, Prf, HandshakeHist, State) -> + stateless_use(Identities, Binders, Prf, HandshakeHist, 0, State). + +stateless_use([], [], _, _, _, State) -> + {{ok, undefined}, State}; +stateless_use([#psk_identity{identity = Encrypted, + obfuscated_ticket_age = ObfAge} | Ids], + [Binder | Binders], Prf, HandshakeHist, Index, + #state{stateless = #{seed := {IV, Shard}, + window := Window}} = State) -> + case ssl_cipher:decrypt_ticket(Encrypted, Shard, IV) of + #stateless_ticket{hash = Prf, + pre_shared_key = PSK} = Ticket -> + case statless_usable_ticket(Ticket, ObfAge, Binder, + HandshakeHist, Window) of + true -> + stateless_anti_replay(Index, PSK, Binder, State); + false -> + stateless_use(Ids, Binders, Prf, HandshakeHist, + Index+1, State); + {error, _} = Error -> + {Error, State} + end; + _ -> + stateless_use(Ids, Binders, Prf, HandshakeHist, Index+1, State) + end. + +statless_usable_ticket(#stateless_ticket{hash = Prf, + ticket_age_add = TicketAgeAdd, + lifetime = Lifetime, + timestamp = Timestamp, + pre_shared_key = PSK}, ObfAge, + Binder, HandshakeHist, Window) -> + case stateless_living_ticket(ObfAge, TicketAgeAdd, Lifetime, + Timestamp, Window) of + true -> + validate_binder(Binder, HandshakeHist, PSK, Prf, stateless); + false -> + false + end. + +stateless_living_ticket(0, _, _, _, _) -> + true; +stateless_living_ticket(ObfAge, TicketAgeAdd, Lifetime, Timestamp, Window) -> + ReportedAge = ObfAge - TicketAgeAdd, + RealAge = erlang:system_time(second) - Timestamp, + (ReportedAge =< Lifetime) + andalso (RealAge =< Lifetime) + andalso (in_window(RealAge, Window)). + +in_window(_, undefined) -> + true; +in_window(Age, {Window, _, _}) -> + Age =< Window. + +stateless_anti_replay(Index, PSK, Binder, + #state{stateless = #{bloom_filter := BloomFilter0} + = Stateless} = State) -> + case tls_bloom_filter:contains(BloomFilter0, Binder) of + true -> + %%possible_replay + {{ok, undefined}, State}; + false -> + BloomFilter = tls_bloom_filter:add_elem(BloomFilter0, Binder), + {{ok, {Index, PSK}}, + State#state{stateless = Stateless#{bloom_filter => BloomFilter}}} + end; +stateless_anti_replay(Index, PSK, _, State) -> + {{ok, {Index, PSK}}, State}. diff --git a/lib/ssl/src/tls_server_session_ticket_sup.erl b/lib/ssl/src/tls_server_session_ticket_sup.erl new file mode 100644 index 0000000000..739491cf1e --- /dev/null +++ b/lib/ssl/src/tls_server_session_ticket_sup.erl @@ -0,0 +1,72 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2014-2016. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +%% +%%---------------------------------------------------------------------- +%% Purpose: Supervisor for a listen options tracker +%%---------------------------------------------------------------------- +-module(tls_server_session_ticket_sup). + +-behaviour(supervisor). + +%% API +-export([start_link/0, start_link_dist/0]). +-export([start_child/1, start_child_dist/1]). + +%% Supervisor callback +-export([init/1]). + +%%%========================================================================= +%%% API +%%%========================================================================= +start_link() -> + supervisor:start_link({local, tracker_name(normal)}, ?MODULE, []). + +start_link_dist() -> + supervisor:start_link({local, tracker_name(dist)}, ?MODULE, []). + +start_child(Args) -> + supervisor:start_child(tracker_name(normal), Args). + +start_child_dist(Args) -> + supervisor:start_child(tracker_name(dist), Args). + +%%%========================================================================= +%%% Supervisor callback +%%%========================================================================= +init(_O) -> + RestartStrategy = simple_one_for_one, + MaxR = 0, + MaxT = 3600, + + Name = undefined, % As simple_one_for_one is used. + StartFunc = {tls_server_session_ticket, start_link, []}, + Restart = temporary, % E.g. should not be restarted + Shutdown = 4000, + Modules = [tls_server_session_ticket], + Type = worker, + + ChildSpec = {Name, StartFunc, Restart, Shutdown, Type, Modules}, + {ok, {{RestartStrategy, MaxR, MaxT}, [ChildSpec]}}. + +tracker_name(normal) -> + ?MODULE; +tracker_name(dist) -> + list_to_atom(atom_to_list(?MODULE) ++ "dist"). diff --git a/lib/ssl/src/tls_server_sup.erl b/lib/ssl/src/tls_server_sup.erl new file mode 100644 index 0000000000..7834589206 --- /dev/null +++ b/lib/ssl/src/tls_server_sup.erl @@ -0,0 +1,77 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2019-2019. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +%% + +-module(tls_server_sup). + +-behaviour(supervisor). + +%% API +-export([start_link/0]). + +%% Supervisor callback +-export([init/1]). + +%%%========================================================================= +%%% API +%%%========================================================================= + +-spec start_link() -> {ok, pid()} | ignore | {error, term()}. + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%%%========================================================================= +%%% Supervisor callback +%%%========================================================================= + +init([]) -> + ListenTracker = listen_options_tracker_child_spec(), + SessionTracker = tls_server_session_child_spec(), + + {ok, {{one_for_all, 10, 3600}, [ListenTracker, + SessionTracker + ]}}. + + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- + +%% Handles emulated options so that they inherited by the accept +%% socket, even when setopts is performed on the listen socket +listen_options_tracker_child_spec() -> + Name = tls_socket, + StartFunc = {ssl_listen_tracker_sup, start_link, []}, + Restart = permanent, + Shutdown = 4000, + Modules = [ssl_listen_tracker_sup], + Type = supervisor, + {Name, StartFunc, Restart, Shutdown, Type, Modules}. + +tls_server_session_child_spec() -> + Name = tls_server_session_ticket, + StartFunc = {tls_server_session_ticket_sup, start_link, []}, + Restart = permanent, + Shutdown = 4000, + Modules = [tls_server_session_ticket_sup], + Type = supervisor, + {Name, StartFunc, Restart, Shutdown, Type, Modules}. diff --git a/lib/ssl/src/tls_socket.erl b/lib/ssl/src/tls_socket.erl index acd907905c..031665bfe8 100644 --- a/lib/ssl/src/tls_socket.erl +++ b/lib/ssl/src/tls_socket.erl @@ -24,14 +24,39 @@ -include("ssl_internal.hrl"). -include("ssl_api.hrl"). --export([send/3, listen/3, accept/3, socket/5, connect/4, upgrade/3, - setopts/3, getopts/3, getstat/3, peername/2, sockname/2, port/2]). --export([split_options/1, get_socket_opts/3]). --export([emulated_options/0, emulated_options/1, internal_inet_values/0, default_inet_values/0, - init/1, start_link/3, terminate/2, inherit_tracker/3, - emulated_socket_options/2, get_emulated_opts/1, - set_emulated_opts/2, get_all_opts/1, handle_call/3, handle_cast/2, - handle_info/2, code_change/3]). +-export([send/3, + listen/3, + accept/3, + socket/5, + connect/4, + upgrade/3, + setopts/3, + getopts/3, + getstat/3, + peername/2, + sockname/2, + port/2]). + +-export([split_options/1, + get_socket_opts/3]). + +-export([emulated_options/0, + emulated_options/1, + internal_inet_values/0, + default_inet_values/0, + init/1, + start_link/3, + terminate/2, + inherit_tracker/3, + emulated_socket_options/2, + get_emulated_opts/1, + set_emulated_opts/2, + get_all_opts/1, + handle_call/3, + handle_cast/2, + handle_info/2, + code_change/3]). + -export([update_active_n/2]). -record(state, { @@ -52,8 +77,11 @@ listen(Transport, Port, #config{transport_info = {Transport, _, _, _, _}, case Transport:listen(Port, Options ++ internal_inet_values()) of {ok, ListenSocket} -> {ok, Tracker} = inherit_tracker(ListenSocket, EmOpts, SslOpts), - Socket = #sslsocket{pid = {ListenSocket, Config#config{emulated = Tracker}}}, - check_active_n(EmOpts, Socket), + %% TODO not hard code + {ok, SessionHandler} = session_tickets_tracker(7200, SslOpts), + Trackers = [{option_tracker, Tracker}, {session_tickets_tracker, SessionHandler}], + Socket = #sslsocket{pid = {ListenSocket, Config#config{trackers = Trackers}}}, + check_active_n(EmOpts, Socket), {ok, Socket}; Err = {error, _} -> Err @@ -62,17 +90,18 @@ listen(Transport, Port, #config{transport_info = {Transport, _, _, _, _}, accept(ListenSocket, #config{transport_info = {Transport,_,_,_,_} = CbInfo, connection_cb = ConnectionCb, ssl = SslOpts, - emulated = Tracker}, Timeout) -> + trackers = Trackers}, Timeout) -> case Transport:accept(ListenSocket, Timeout) of {ok, Socket} -> - {ok, EmOpts} = get_emulated_opts(Tracker), + Tracker = proplists:get_value(option_tracker, Trackers), + {ok, EmOpts} = get_emulated_opts(Tracker), {ok, Port} = tls_socket:port(Transport, Socket), {ok, Sender} = tls_sender:start(), ConnArgs = [server, Sender, "localhost", Port, Socket, - {SslOpts, emulated_socket_options(EmOpts, #socket_options{}), Tracker}, self(), CbInfo], + {SslOpts, emulated_socket_options(EmOpts, #socket_options{}), Trackers}, self(), CbInfo], case tls_connection_sup:start_child(ConnArgs) of {ok, Pid} -> - ssl_connection:socket_control(ConnectionCb, Socket, [Pid, Sender], Transport, Tracker); + ssl_connection:socket_control(ConnectionCb, Socket, [Pid, Sender], Transport, Trackers); {error, Reason} -> {error, Reason} end; @@ -116,17 +145,19 @@ connect(Address, Port, {error, {options, {socket_options, UserOpts}}} end. -socket(Pids, Transport, Socket, ConnectionCb, Tracker) -> +socket(Pids, Transport, Socket, ConnectionCb, Trackers) -> #sslsocket{pid = Pids, %% "The name "fd" is keept for backwards compatibility - fd = {Transport, Socket, ConnectionCb, Tracker}}. -setopts(gen_tcp, Socket = #sslsocket{pid = {ListenSocket, #config{emulated = Tracker}}}, Options) -> + fd = {Transport, Socket, ConnectionCb, Trackers}}. +setopts(gen_tcp, Socket = #sslsocket{pid = {ListenSocket, #config{trackers = Trackers}}}, Options) -> + Tracker = proplists:get_value(option_tracker, Trackers), {SockOpts, EmulatedOpts} = split_options(Options), ok = set_emulated_opts(Tracker, EmulatedOpts), check_active_n(EmulatedOpts, Socket), inet:setopts(ListenSocket, SockOpts); setopts(_, Socket = #sslsocket{pid = {ListenSocket, #config{transport_info = {Transport,_,_,_,_}, - emulated = Tracker}}}, Options) -> + trackers = Trackers}}}, Options) -> + Tracker = proplists:get_value(option_tracker, Trackers), {SockOpts, EmulatedOpts} = split_options(Options), ok = set_emulated_opts(Tracker, EmulatedOpts), check_active_n(EmulatedOpts, Socket), @@ -137,7 +168,8 @@ setopts(gen_tcp, Socket, Options) -> setopts(Transport, Socket, Options) -> Transport:setopts(Socket, Options). -check_active_n(EmulatedOpts, Socket = #sslsocket{pid = {_, #config{emulated = Tracker}}}) -> +check_active_n(EmulatedOpts, Socket = #sslsocket{pid = {_, #config{trackers = Trackers}}}) -> + Tracker = proplists:get_value(option_tracker, Trackers), %% We check the resulting options to send an ssl_passive message if necessary. case proplists:lookup(active, EmulatedOpts) of %% The provided value is out of bound. @@ -162,12 +194,14 @@ check_active_n(EmulatedOpts, Socket = #sslsocket{pid = {_, #config{emulated = Tr ok end. -getopts(gen_tcp, #sslsocket{pid = {ListenSocket, #config{emulated = Tracker}}}, Options) -> +getopts(gen_tcp, #sslsocket{pid = {ListenSocket, #config{trackers = Trackers}}}, Options) -> + Tracker = proplists:get_value(option_tracker, Trackers), {SockOptNames, EmulatedOptNames} = split_options(Options), EmulatedOpts = get_emulated_opts(Tracker, EmulatedOptNames), SocketOpts = get_socket_opts(ListenSocket, SockOptNames, inet), {ok, EmulatedOpts ++ SocketOpts}; -getopts(Transport, #sslsocket{pid = {ListenSocket, #config{emulated = Tracker}}}, Options) -> +getopts(Transport, #sslsocket{pid = {ListenSocket, #config{trackers = Trackers}}}, Options) -> + Tracker = proplists:get_value(option_tracker, Trackers), {SockOptNames, EmulatedOptNames} = split_options(Options), EmulatedOpts = get_emulated_opts(Tracker, EmulatedOptNames), SocketOpts = get_socket_opts(ListenSocket, SockOptNames, Transport), @@ -215,6 +249,17 @@ inherit_tracker(ListenSocket, EmOpts, #{erl_dist := false} = SslOpts) -> inherit_tracker(ListenSocket, EmOpts, #{erl_dist := true} = SslOpts) -> ssl_listen_tracker_sup:start_child_dist([ListenSocket, EmOpts, SslOpts]). +session_tickets_tracker(_, #{erl_dist := false, + session_tickets := disabled}) -> + {ok, disabled}; +session_tickets_tracker(Lifetime, #{erl_dist := false, + session_tickets := Mode, + anti_replay := AntiReplay}) -> + tls_server_session_ticket_sup:start_child([Mode, Lifetime, AntiReplay]); +session_tickets_tracker(Lifetime, #{erl_dist := true, session_tickets := Mode}) -> + tls_server_session_ticket_sup:start_child_dist([Mode, Lifetime]). + + get_emulated_opts(TrackerPid) -> call(TrackerPid, get_emulated_opts). set_emulated_opts(TrackerPid, InetValues) -> diff --git a/lib/ssl/src/tls_sup.erl b/lib/ssl/src/tls_sup.erl new file mode 100644 index 0000000000..25c1db0272 --- /dev/null +++ b/lib/ssl/src/tls_sup.erl @@ -0,0 +1,76 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2019-2019. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +%% + +-module(tls_sup). + +-behaviour(supervisor). + +%% API +-export([start_link/0]). + +%% Supervisor callback +-export([init/1]). + +%%%========================================================================= +%%% API +%%%========================================================================= + +-spec start_link() -> {ok, pid()} | ignore | {error, term()}. + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%%%========================================================================= +%%% Supervisor callback +%%%========================================================================= + +init([]) -> + + TLSConnetionManager = tls_connection_manager_child_spec(), + ServerInstanceSup = server_instance_child_spec(), + + {ok, {{one_for_one, 10, 3600}, [TLSConnetionManager, + ServerInstanceSup + ]}}. + + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- + +tls_connection_manager_child_spec() -> + Name = tls_connection, + StartFunc = {tls_connection_sup, start_link, []}, + Restart = permanent, + Shutdown = 4000, + Modules = [tls_connection_sup], + Type = supervisor, + {Name, StartFunc, Restart, Shutdown, Type, Modules}. + +server_instance_child_spec() -> + Name = tls_server_sup, + StartFunc = {tls_server_sup, start_link, []}, + Restart = permanent, + Shutdown = 4000, + Modules = [tls_server_sup], + Type = supervisor, + {Name, StartFunc, Restart, Shutdown, Type, Modules}. diff --git a/lib/ssl/src/tls_v1.erl b/lib/ssl/src/tls_v1.erl index f7c8c770ae..381793c65d 100644 --- a/lib/ssl/src/tls_v1.erl +++ b/lib/ssl/src/tls_v1.erl @@ -44,7 +44,7 @@ client_application_traffic_secret_0/3, server_application_traffic_secret_0/3, exporter_master_secret/3, resumption_master_secret/3, update_traffic_secret/2, calculate_traffic_keys/3, - transcript_hash/2, finished_key/2, finished_verify_data/3]). + transcript_hash/2, finished_key/2, finished_verify_data/3, pre_shared_key/3]). -type named_curve() :: sect571r1 | sect571k1 | secp521r1 | brainpoolP512r1 | sect409k1 | sect409r1 | brainpoolP384r1 | secp384r1 | @@ -393,6 +393,15 @@ finished_verify_data(FinishedKey, HKDFAlgo, Messages) -> THash = tls_v1:transcript_hash(Context, HKDFAlgo), tls_v1:hmac_hash(HKDFAlgo, FinishedKey, THash). +-spec pre_shared_key(binary(), binary(), atom()) -> binary(). +pre_shared_key(RMS, Nonce, Algo) -> + %% The PSK associated with the ticket is computed as: + %% + %% HKDF-Expand-Label(resumption_master_secret, + %% "resumption", ticket_nonce, Hash.length) + ssl_cipher:hash_size(Algo), + hkdf_expand_label(RMS, <<"resumption">>, Nonce, ssl_cipher:hash_size(Algo), Algo). + %% The next-generation application_traffic_secret is computed as: %% %% application_traffic_secret_N+1 = diff --git a/lib/ssl/test/Makefile b/lib/ssl/test/Makefile index ec0addac59..cb52a6d3d5 100644 --- a/lib/ssl/test/Makefile +++ b/lib/ssl/test/Makefile @@ -43,7 +43,8 @@ MODULES = \ ssl_dist_test_lib \ ssl_api_SUITE\ tls_api_SUITE\ - ssl_basic_SUITE \ + dtls_api_SUITE\ + ssl_basic_SUITE \ ssl_bench_SUITE \ ssl_cipher_SUITE \ ssl_cipher_suite_SUITE \ @@ -71,6 +72,7 @@ MODULES = \ ssl_pem_cache_SUITE \ ssl_session_SUITE \ ssl_session_cache_SUITE \ + ssl_session_ticket_SUITE \ openssl_session_SUITE \ ssl_ECC_SUITE \ ssl_ECC_openssl_SUITE \ diff --git a/lib/ssl/test/dtls_api_SUITE.erl b/lib/ssl/test/dtls_api_SUITE.erl new file mode 100644 index 0000000000..37130abcbf --- /dev/null +++ b/lib/ssl/test/dtls_api_SUITE.erl @@ -0,0 +1,131 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2019-2019. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +%% +-module(dtls_api_SUITE). + +%% Note: This directive should only be used in test suites. +-compile(export_all). + +all() -> + [ + {group, 'dtlsv1.2'}, + {group, 'dtlsv1'} + ]. + +groups() -> + [ + {'dtlsv1.2', [], api_tests()}, + {'dtlsv1', [], api_tests()} + ]. + +api_tests() -> + [ + dtls_listen_owner_dies + ]. + +init_per_suite(Config0) -> + catch crypto:stop(), + try crypto:start() of + ok -> + ssl_test_lib:clean_start(), + ssl_test_lib:make_rsa_cert(Config0) + catch _:_ -> + {skip, "Crypto did not start"} + end. + +end_per_suite(_Config) -> + ssl:stop(), + application:unload(ssl), + application:stop(crypto). + + +init_per_group(GroupName, Config) -> + case ssl_test_lib:is_tls_version(GroupName) of + true -> + case ssl_test_lib:sufficient_crypto_support(GroupName) of + true -> + ssl_test_lib:init_tls_version(GroupName, Config); + false -> + {skip, "Missing crypto support"} + end; + _ -> + ssl:start(), + Config + end. + +end_per_group(GroupName, Config) -> + case ssl_test_lib:is_tls_version(GroupName) of + true -> + ssl_test_lib:clean_tls_version(Config); + false -> + Config + end. + +init_per_testcase(_TestCase, Config) -> + ssl_test_lib:ct_log_supported_protocol_versions(Config), + ct:timetrap({seconds, 10}), + Config. + +end_per_testcase(_TestCase, Config) -> + Config. + +%%-------------------------------------------------------------------- +%% Test Cases -------------------------------------------------------- +%%-------------------------------------------------------------------- + +dtls_listen_owner_dies() -> + [{doc, "Test that you can start new DTLS 'listner' if old owner dies"}]. + +dtls_listen_owner_dies(Config) when is_list(Config) -> + ClientOpts = ssl_test_lib:ssl_options(client_rsa_opts, Config), + ServerOpts = ssl_test_lib:ssl_options(server_rsa_opts, Config), + {_, ServerNode, Hostname} = ssl_test_lib:run_where(Config), + + Port = ssl_test_lib:inet_port(ServerNode), + Test = self(), + Pid = spawn(fun() -> {ok, _} = + ssl:listen(Port, [{protocol, dtls} | ServerOpts]), + {error, _} = ssl:listen(Port, [{protocol, dtls} | ServerOpts]), + Test ! {self(), listened} + end), + receive + {Pid, listened} -> + ok + end, + {ok, LSocket} = ssl:listen(Port, [{protocol, dtls} | ServerOpts]), + spawn(fun() -> + {ok, ASocket} = ssl:transport_accept(LSocket), + Result0 = ssl:handshake(ASocket), + receive + {ssl, Socket, "from client"} -> + ssl:send(Socket, "from server"), + ssl:close(Socket) + end + end), + {ok, Client} = ssl:connect(Hostname, Port, ClientOpts), + + ssl:send(Client, "from client"), + receive + {ssl, Client, "from server"} -> + ssl:close(Client) + end. + + diff --git a/lib/ssl/test/make_certs.erl b/lib/ssl/test/make_certs.erl index 76bf0fa895..70f718cb12 100644 --- a/lib/ssl/test/make_certs.erl +++ b/lib/ssl/test/make_certs.erl @@ -33,6 +33,7 @@ v2_crls = true, ecc_certs = false, issuing_distribution_point = false, + crldp_crlissuer = false, crl_port = 8000, openssl_cmd = "openssl", hostname = "host.example.com"}). @@ -66,6 +67,8 @@ make_config([{ecc_certs, Bool}|T], C) when is_boolean(Bool) -> make_config(T, C#config{ecc_certs = Bool}); make_config([{issuing_distribution_point, Bool}|T], C) when is_boolean(Bool) -> make_config(T, C#config{issuing_distribution_point = Bool}); +make_config([{crldp_crlissuer, Bool}|T], C) when is_boolean(Bool) -> + make_config(T, C#config{crldp_crlissuer = Bool}); make_config([{openssl_cmd, Cmd}|T], C) when is_list(Cmd) -> make_config(T, C#config{openssl_cmd = Cmd}); make_config([{hostname, Hostname}|T], C) when is_list(Hostname) -> @@ -485,6 +488,87 @@ ca_cnf( ca_cnf( Root, #config{ + crldp_crlissuer = true, + hostname = Hostname} = C) -> + ["# Purpose: Configuration for CAs.\n" + "\n" + "ROOTDIR = " ++ Root ++ "\n" + "default_ca = ca\n" + "\n" + + "[ca]\n" + "dir = $ROOTDIR/", C#config.commonName, "\n" + "certs = $dir/certs\n" + "crl_dir = $dir/crl\n" + "database = $dir/index.txt\n" + "new_certs_dir = $dir/newcerts\n" + "certificate = $dir/cert.pem\n" + "serial = $dir/serial\n" + "crl = $dir/crl.pem\n", + ["crlnumber = $dir/crlnumber\n" || C#config.v2_crls], + "private_key = $dir/private/key.pem\n" + "RANDFILE = $dir/private/RAND\n" + "\n" + "x509_extensions = user_cert\n", + ["crl_extensions = crl_ext\n" || C#config.v2_crls], + "unique_subject = no\n" + "default_days = 3600\n" + "default_md = sha1\n" + "preserve = no\n" + "policy = policy_match\n" + "\n" + + "[policy_match]\n" + "commonName = supplied\n" + "organizationalUnitName = optional\n" + "organizationName = match\n" + "countryName = match\n" + "localityName = match\n" + "emailAddress = supplied\n" + "\n" + + "[crl_ext]\n" + "authorityKeyIdentifier=keyid:always,issuer:always\n", + + "[user_cert]\n" + "basicConstraints = CA:false\n" + "keyUsage = nonRepudiation, digitalSignature, keyEncipherment\n" + "subjectKeyIdentifier = hash\n" + "authorityKeyIdentifier = keyid,issuer:always\n" + "subjectAltName = DNS.1:" ++ Hostname ++ "\n" + "issuerAltName = issuer:copy\n" + "crlDistributionPoints=crl_section\n" + + "[crl_section]\n" + "fullname=URI:http://localhost/",C#config.commonName,"/crl.pem\n" + "CRLissuer=dirName:issuer_sect\n" + + "[issuer_sect]\n" + "C=UK\n" + "O=Organisation\n" + "CN=Some Name\n" + + "[user_cert_digital_signature_only]\n" + "basicConstraints = CA:false\n" + "keyUsage = digitalSignature\n" + "subjectKeyIdentifier = hash\n" + "authorityKeyIdentifier = keyid,issuer:always\n" + "subjectAltName = DNS.1:" ++ Hostname ++ "\n" + "issuerAltName = issuer:copy\n" + "\n" + + "[ca_cert]\n" + "basicConstraints = critical,CA:true\n" + "keyUsage = cRLSign, keyCertSign\n" + "subjectKeyIdentifier = hash\n" + "authorityKeyIdentifier = keyid:always,issuer:always\n" + "subjectAltName = email:copy\n" + "issuerAltName = issuer:copy\n" + ]; + +ca_cnf( + Root, + #config{ issuing_distribution_point = false, hostname = Hostname } = C) -> diff --git a/lib/ssl/test/openssl_npn_SUITE.erl b/lib/ssl/test/openssl_npn_SUITE.erl index ed3d81eba7..7322e228bd 100644 --- a/lib/ssl/test/openssl_npn_SUITE.erl +++ b/lib/ssl/test/openssl_npn_SUITE.erl @@ -74,6 +74,7 @@ init_per_suite(Config0) -> try crypto:start() of ok -> ssl_test_lib:clean_start(), + ssl:clear_pem_cache(), ssl_test_lib:make_rsa_cert(Config0) catch _:_ -> {skip, "Crypto did not start"} diff --git a/lib/ssl/test/openssl_session_SUITE.erl b/lib/ssl/test/openssl_session_SUITE.erl index 7c129633da..6f74408cb4 100644 --- a/lib/ssl/test/openssl_session_SUITE.erl +++ b/lib/ssl/test/openssl_session_SUITE.erl @@ -129,7 +129,21 @@ init_per_testcase(reuse_session_erlang_client, Config) -> application:set_env(ssl, session_lifetime, ?EXPIRE), ssl:start(), Config; - +init_per_testcase(reuse_session_erlang_server, Config) -> + Version = ssl_test_lib:protocol_version(Config), + case ssl_test_lib:is_dtls_version(Version) of + true -> + case ssl_test_lib:openssl_sane_dtls_session_reuse() of + true -> + ct:timetrap({seconds, 10}), + Config; + false -> + {skip, "Broken OpenSSL DTLS session reuse"} + end; + false -> + ct:timetrap({seconds, 10}), + Config + end; init_per_testcase(TestCase, Config) -> ct:timetrap({seconds, 10}), Config. diff --git a/lib/ssl/test/openssl_tls_1_3_version_SUITE.erl b/lib/ssl/test/openssl_tls_1_3_version_SUITE.erl index 8a2692ec1d..443236f166 100644 --- a/lib/ssl/test/openssl_tls_1_3_version_SUITE.erl +++ b/lib/ssl/test/openssl_tls_1_3_version_SUITE.erl @@ -51,7 +51,7 @@ cert_groups() -> {group, ecdsa}]. tests() -> - [tls13_client_tls12_server, + [%%tls13_client_tls12_server, %% Not testable with current openssl s_client %%tls13_client_with_ext_tls12_server, tls12_client_tls13_server]. @@ -127,6 +127,10 @@ end_per_group(GroupName, Config) -> %% Test Cases -------------------------------------------------------- %%-------------------------------------------------------------------- +%% openssl s_client cannot be configured to support both TLS 1.3 and TLS 1.2. +%% In its ClientHello the supported_versions extension contains only one element +%% [{3,4}] that the server does not accept if it is configured to not support +%% TLS 1.3. tls13_client_tls12_server() -> [{doc,"Test that a TLS 1.3 client can connect to a TLS 1.2 server."}]. @@ -159,11 +163,13 @@ tls13_client_tls12_server(Config) when is_list(Config) -> %% ssl_test_lib:basic_test(ClientOpts, ServerOpts, Config). - + + +%% TODO: wrong version of TLS is configured for the client tls12_client_tls13_server() -> [{doc,"Test that a TLS 1.2 client can connect to a TLS 1.3 server."}]. -tls12_client_tls13_server(Config) when is_list(Config) -> +tls12_client_tls13_server(Config) when is_list(Config) -> ClientOpts = [{versions, ['tlsv1.1', 'tlsv1.2']} | ssl_test_lib:ssl_options(client_cert_opts, Config)], ServerOpts = [{versions, diff --git a/lib/ssl/test/property_test/ssl_eqc_handshake.erl b/lib/ssl/test/property_test/ssl_eqc_handshake.erl index 2ceb540e15..9ae267a2d3 100644 --- a/lib/ssl/test/property_test/ssl_eqc_handshake.erl +++ b/lib/ssl/test/property_test/ssl_eqc_handshake.erl @@ -863,9 +863,9 @@ psk_identities(N, Acc) -> psk_identities(N - 1, [psk_identity()|Acc]). psk_identity() -> - Len = rand:uniform(32), + Len = 8 + rand:uniform(8), Identity = crypto:strong_rand_bytes(Len), - Age = crypto:strong_rand_bytes(4), + <<?UINT32(Age)>> = crypto:strong_rand_bytes(4), #psk_identity{ identity = Identity, obfuscated_ticket_age = Age}. diff --git a/lib/ssl/test/ssl_alert_SUITE.erl b/lib/ssl/test/ssl_alert_SUITE.erl index cc0b636580..a5b9fe9b0e 100644 --- a/lib/ssl/test/ssl_alert_SUITE.erl +++ b/lib/ssl/test/ssl_alert_SUITE.erl @@ -49,7 +49,7 @@ end_per_testcase(_TestCase, Config) -> %% Test Cases -------------------------------------------------------- %%-------------------------------------------------------------------- alerts() -> - [{doc, "Test ssl_alert:alert_txt/1"}]. + [{doc, "Test ssl_alert formating code"}]. alerts(Config) when is_list(Config) -> Descriptions = [?CLOSE_NOTIFY, ?UNEXPECTED_MESSAGE, ?BAD_RECORD_MAC, ?DECRYPTION_FAILED_RESERVED, ?RECORD_OVERFLOW, ?DECOMPRESSION_FAILURE, @@ -66,7 +66,9 @@ alerts(Config) when is_list(Config) -> Alerts = [?ALERT_REC(?WARNING, ?CLOSE_NOTIFY) | [?ALERT_REC(?FATAL, Desc) || Desc <- Descriptions]], lists:foreach(fun(Alert) -> - try ssl_alert:alert_txt(Alert) + try + ssl_alert:reason_code(Alert, server, "TLS", cipher), + ssl_alert:reason_code(Alert, client, "TLS", hello) catch C:E:T -> ct:fail({unexpected, {C, E, T}}) @@ -78,8 +80,9 @@ alert_details() -> alert_details(Config) when is_list(Config) -> Unique = make_ref(), UniqueStr = lists:flatten(io_lib:format("~w", [Unique])), - Alert = ?ALERT_REC(?WARNING, ?CLOSE_NOTIFY, Unique), - case string:str(ssl_alert:alert_txt(Alert), UniqueStr) of + Alert = ?ALERT_REC(?WARNING, ?INTERNAL_ERROR, Unique), + {tls_alert, {_, Txt}} = ssl_alert:reason_code(Alert#alert{role=server}, server, "TLS", cipher), + case string:str(Txt, UniqueStr) of 0 -> ct:fail(error_details_missing); _ -> @@ -90,11 +93,16 @@ alert_details(Config) when is_list(Config) -> alert_details_not_too_big() -> [{doc, "Test that ssl_alert:alert_txt/1 limits printed depth of extended error description"}]. alert_details_not_too_big(Config) when is_list(Config) -> - Reason = lists:duplicate(10, lists:duplicate(10, lists:duplicate(10, {some, data}))), - Alert = ?ALERT_REC(?WARNING, ?CLOSE_NOTIFY, Reason), - case length(ssl_alert:alert_txt(Alert)) < 1000 of + Reason = ssl:cipher_suites(all, 'tlsv1.2'), + ReasonText = lists:flatten(io_lib:format("~p", [Reason])), + Alert = ?ALERT_REC(?FATAL, ?HANDSHAKE_FAILURE, Reason), + PrefixLen = + length("TLS server: In state cipher at ssl_handshake.erl:1710 generated SERVER ALERT: Fatal - Handshake Failure"), + {tls_alert, {_, Txt}} = ssl_alert:reason_code(Alert#alert{role=server, where = #{file => "ssl_handshake.erl", + line => 1710}}, server, "TLS", cipher), + case byte_size(term_to_binary(Txt)) < (byte_size(term_to_binary(ReasonText)) - PrefixLen) of true -> - ok; + ct:pal("~s", [Txt]); false -> ct:fail(ssl_alert_text_too_big) end. diff --git a/lib/ssl/test/ssl_api_SUITE.erl b/lib/ssl/test/ssl_api_SUITE.erl index c0f23cce58..d2fe83a58e 100644 --- a/lib/ssl/test/ssl_api_SUITE.erl +++ b/lib/ssl/test/ssl_api_SUITE.erl @@ -44,17 +44,22 @@ all() -> groups() -> [ - {'tlsv1.3', [], ((gen_api_tests() ++ tls13_group() ++ handshake_paus_tests()) -- [dh_params, honor_server_cipher_order, honor_client_cipher_order, - new_options_in_handshake]) + {'tlsv1.3', [], ((gen_api_tests() ++ tls13_group() ++ handshake_paus_tests()) -- + [dh_params, honor_server_cipher_order, honor_client_cipher_order, + new_options_in_handshake, handshake_continue_tls13_client]) ++ (since_1_2() -- [conf_signature_algs])}, {'tlsv1.2', [], gen_api_tests() ++ since_1_2() ++ handshake_paus_tests() ++ pre_1_3()}, {'tlsv1.1', [], gen_api_tests() ++ handshake_paus_tests() ++ pre_1_3()}, {'tlsv1', [], gen_api_tests() ++ handshake_paus_tests() ++ pre_1_3() ++ beast_mitigation_test()}, {'sslv3', [], (gen_api_tests() -- [new_options_in_handshake]) ++ beast_mitigation_test() ++ pre_1_3()}, - {'dtlsv1.2', [], (gen_api_tests() -- [invalid_keyfile, invalid_certfile, invalid_cacertfile, - invalid_options, new_options_in_handshake]) ++ handshake_paus_tests() ++ pre_1_3()}, - {'dtlsv1', [], (gen_api_tests() -- [invalid_keyfile, invalid_certfile, invalid_cacertfile, - invalid_options, new_options_in_handshake]) ++ handshake_paus_tests() ++ pre_1_3()} + {'dtlsv1.2', [], (gen_api_tests() -- + [invalid_keyfile, invalid_certfile, invalid_cacertfile, + invalid_options, new_options_in_handshake]) ++ + handshake_paus_tests() -- [handshake_continue_tls13_client] ++ pre_1_3()}, + {'dtlsv1', [], (gen_api_tests() -- + [invalid_keyfile, invalid_certfile, invalid_cacertfile, + invalid_options, new_options_in_handshake]) ++ + handshake_paus_tests() -- [handshake_continue_tls13_client] ++ pre_1_3()} ]. since_1_2() -> @@ -102,7 +107,9 @@ gen_api_tests() -> invalid_cacertfile, invalid_keyfile, options_not_proplist, - invalid_options + invalid_options, + cb_info, + log_alert ]. handshake_paus_tests() -> @@ -110,7 +117,8 @@ handshake_paus_tests() -> handshake_continue, handshake_continue_timeout, hello_client_cancel, - hello_server_cancel + hello_server_cancel, + handshake_continue_tls13_client ]. %% Only relevant for SSL 3.0 and TLS 1.1 @@ -435,7 +443,8 @@ handshake_continue(Config) when is_list(Config) -> Server = ssl_test_lib:start_server([{node, ServerNode}, {port, 0}, {from, self()}, {mfa, {ssl_test_lib, send_recv_result_active, []}}, - {options, ssl_test_lib:ssl_options([{reuseaddr, true}, + {options, ssl_test_lib:ssl_options([{reuseaddr, true}, + {log_level, debug}, {verify, verify_peer}, {handshake, hello} | ServerOpts ], @@ -460,6 +469,69 @@ handshake_continue(Config) when is_list(Config) -> ssl_test_lib:close(Server), ssl_test_lib:close(Client). +%%-------------------------------------------------------------------- +handshake_continue_tls13_client() -> + [{doc, "Test API function ssl:handshake_continue/3 with fixed TLS 1.3 client"}]. +handshake_continue_tls13_client(Config) when is_list(Config) -> + ClientOpts = ssl_test_lib:ssl_options(client_rsa_opts, Config), + ServerOpts = ssl_test_lib:ssl_options(server_rsa_opts, Config), + {ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config), + + Server = ssl_test_lib:start_server([{node, ServerNode}, {port, 0}, + {from, self()}, + {mfa, {ssl_test_lib, send_recv_result_active, []}}, + {options, ssl_test_lib:ssl_options([{reuseaddr, true}, + {log_level, debug}, + {verify, verify_peer}, + {handshake, hello} | ServerOpts + ], + Config)}, + {continue_options, proplists:delete(reuseaddr, ServerOpts)} + ]), + + Port = ssl_test_lib:inet_port(Server), + + DummyTicket = + <<131,116,0,0,0,5,100,0,4,104,107,100,102,100,0,6,115,104,97,51, + 56,52,100,0,3,112,115,107,109,0,0,0,48,150,90,38,127,26,12,5, + 228,180,235,229,214,215,27,236,149,182,82,14,140,50,81,0,150, + 248,152,180,193,207,80,52,107,196,200,2,77,4,96,140,65,239,205, + 224,125,129,179,147,103,100,0,3,115,110,105,107,0,25,112,114, + 111,117,100,102,111,111,116,46,111,116,112,46,101,114,105,99, + 115,115,111,110,46,115,101,100,0,6,116,105,99,107,101,116,104,6, + 100,0,18,110,101,119,95,115,101,115,115,105,111,110,95,116,105, + 99,107,101,116,98,0,0,28,32,98,127,110,83,249,109,0,0,0,8,0,0,0, + 0,0,0,0,5,109,0,0,0,113,112,154,74,26,27,0,111,147,51,110,216, + 43,45,4,100,215,152,195,118,96,22,34,1,184,170,42,166,238,109, + 187,138,196,147,102,205,116,83,241,174,227,232,156,148,60,153,3, + 175,128,115,192,36,103,191,239,58,222,192,172,190,239,92,8,131, + 195,0,217,187,222,143,104,6,86,53,93,27,218,198,205,138,223,202, + 11,55,168,104,6,219,228,217,157,37,52,205,252,165,135,167,116, + 216,172,231,222,189,84,97,0,8,106,108,88,47,114,48,116,0,0,0,0, + 100,0,9,116,105,109,101,115,116,97,109,112,98,93,205,0,44>>, + + %% Send dummy session ticket to trigger sending of pre_shared_key and + %% psk_key_exchange_modes extensions. + Client = ssl_test_lib:start_client([{node, ClientNode}, {port, Port}, + {host, Hostname}, + {from, self()}, + {mfa, {ssl_test_lib, send_recv_result_active, []}}, + {options, ssl_test_lib:ssl_options([{handshake, hello}, + {session_tickets, enabled}, + {use_ticket, [DummyTicket]}, + {versions, ['tlsv1.3', + 'tlsv1.2', + 'tlsv1.1', + 'tlsv1']}, + {verify, verify_peer} | ClientOpts + ], + Config)}, + {continue_options, proplists:delete(reuseaddr, ClientOpts)}]), + + ssl_test_lib:check_result(Server, ok, Client, ok), + + ssl_test_lib:close(Server), + ssl_test_lib:close(Client). %%------------------------------------------------------------------ handshake_continue_timeout() -> @@ -1538,6 +1610,60 @@ default_reject_anonymous(Config) when is_list(Config) -> ssl_test_lib:check_server_alert(Server, Client, insufficient_security). %%------------------------------------------------------------------- +cb_info() -> + [{doc,"Test that we can set cb_info."}]. + +cb_info(Config) when is_list(Config) -> + ClientOpts = ssl_test_lib:ssl_options(client_rsa_opts, Config), + ServerOpts = ssl_test_lib:ssl_options(server_rsa_opts, Config), + Protocol = proplists:get_value(protocol, Config, tls), + CbInfo = + case Protocol of + tls -> + {cb_info, {gen_tcp, tcp, tcp_closed, tcp_error}}; + dtls -> + {cb_info, {gen_udp, udp, udp_closed, udp_error}} + end, + {ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config), + Server = ssl_test_lib:start_server([{node, ServerNode}, {port, 0}, + {from, self()}, + {mfa, {ssl_test_lib, send_recv_result_active, []}}, + {options, [CbInfo | ServerOpts]}]), + Port = ssl_test_lib:inet_port(Server), + + Client = ssl_test_lib:start_client([{node, ClientNode}, {port, Port}, + {host, Hostname}, + {from, self()}, + {mfa, {ssl_test_lib, send_recv_result_active, []}}, + {options, [CbInfo | ClientOpts]}]), + + ssl_test_lib:check_result(Server, ok, Client, ok). + +%%------------------------------------------------------------------- +log_alert() -> + [{doc,"Test that we can set log_alert."}]. + +log_alert(Config) when is_list(Config) -> + ClientOpts = ssl_test_lib:ssl_options(client_rsa_opts, Config), + ServerOpts = ssl_test_lib:ssl_options(server_rsa_opts, Config), + + {ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config), + Server = ssl_test_lib:start_server([{node, ServerNode}, {port, 0}, + {from, self()}, + {mfa, {ssl_test_lib, send_recv_result_active, []}}, + {options, [{log_alert, false} | ServerOpts]}]), + Port = ssl_test_lib:inet_port(Server), + + Client = ssl_test_lib:start_client([{node, ClientNode}, {port, Port}, + {host, Hostname}, + {from, self()}, + {mfa, {ssl_test_lib, send_recv_result_active, []}}, + {options, [{log_alert, false} | ClientOpts]}]), + + ssl_test_lib:check_result(Server, ok, Client, ok). + + +%%------------------------------------------------------------------- %% Note that these test only test that the options are valid to set. As application data %% is a stream you can not test that the send acctually splits it up as when it arrives %% again at the user layer it may be concatenated. But COVER can show that the split up diff --git a/lib/ssl/test/ssl_crl_SUITE.erl b/lib/ssl/test/ssl_crl_SUITE.erl index b2fd3874a8..47d4b04d90 100644 --- a/lib/ssl/test/ssl_crl_SUITE.erl +++ b/lib/ssl/test/ssl_crl_SUITE.erl @@ -42,7 +42,8 @@ groups() -> {check_true, [], [{group, v2_crl}, {group, v1_crl}, {group, idp_crl}, - {group, crl_hash_dir}]}, + {group, crl_hash_dir}, + {group, crl_verify_crldp_crlissuer}]}, {check_peer, [], [{group, v2_crl}, {group, v1_crl}, {group, idp_crl}, @@ -54,7 +55,8 @@ groups() -> {v2_crl, [], basic_tests()}, {v1_crl, [], basic_tests()}, {idp_crl, [], basic_tests()}, - {crl_hash_dir, [], basic_tests() ++ crl_hash_dir_tests()}]. + {crl_hash_dir, [], basic_tests() ++ crl_hash_dir_tests()}, + {crl_verify_crldp_crlissuer, [], [crl_verify_valid]}]. basic_tests() -> [crl_verify_valid, crl_verify_revoked, crl_verify_no_crl]. @@ -108,8 +110,8 @@ init_per_group(Group, Config0) -> CertDir = filename:join(proplists:get_value(priv_dir, Config0), Group), {CertOpts, Config} = init_certs(CertDir, Group, Config0), {ok, _} = make_certs:all(DataDir, CertDir, CertOpts), - CrlCacheOpts = case Group of - crl_hash_dir -> + CrlCacheOpts = case need_hash_dir(Group) of + true -> CrlDir = filename:join(CertDir, "crls"), %% Copy CRLs to their hashed filenames. %% Find the hashes with 'openssl crl -noout -hash -in crl.pem'. @@ -462,8 +464,17 @@ is_idp(idp_crl) -> is_idp(_) -> false. +need_hash_dir(crl_hash_dir) -> + true; +need_hash_dir(crl_verify_crldp_crlissuer) -> + true; +need_hash_dir(_) -> + false. + init_certs(_,v1_crl, Config) -> {[{v2_crls, false}], Config}; +init_certs(_,crl_verify_crldp_crlissuer , Config) -> + {[{crldp_crlissuer, true}], Config}; init_certs(_, idp_crl, Config) -> Port = proplists:get_value(httpd_port, Config), {[{crl_port,Port}, diff --git a/lib/ssl/test/ssl_dist_SUITE.erl b/lib/ssl/test/ssl_dist_SUITE.erl index 7cfb2ac0c5..be3d7c2dfe 100644 --- a/lib/ssl/test/ssl_dist_SUITE.erl +++ b/lib/ssl/test/ssl_dist_SUITE.erl @@ -116,7 +116,6 @@ basic_test(NH1, NH2, _) -> Node1 = NH1#node_handle.nodename, Node2 = NH2#node_handle.nodename, pong = apply_on_ssl_node(NH1, fun () -> net_adm:ping(Node2) end), - [Node2] = apply_on_ssl_node(NH1, fun () -> nodes() end), [Node1] = apply_on_ssl_node(NH2, fun () -> nodes() end), diff --git a/lib/ssl/test/ssl_handshake_SUITE.erl b/lib/ssl/test/ssl_handshake_SUITE.erl index e4a2ebb583..dd84d732c1 100644 --- a/lib/ssl/test/ssl_handshake_SUITE.erl +++ b/lib/ssl/test/ssl_handshake_SUITE.erl @@ -28,6 +28,7 @@ -include("ssl_alert.hrl"). -include("ssl_handshake.hrl"). -include("ssl_internal.hrl"). +-include("ssl_record.hrl"). -include("tls_handshake.hrl"). -include_lib("public_key/include/public_key.hrl"). @@ -106,7 +107,7 @@ decode_hello_handshake(_Config) -> Version = {3, 0}, {Records, _Buffer} = tls_handshake:get_tls_handshake(Version, HelloPacket, <<>>, - ssl:options_to_map(#ssl_options{})), + default_options_map()), {Hello, _Data} = hd(Records), Extensions = Hello#server_hello.extensions, @@ -274,3 +275,7 @@ is_supported(Hash) -> Algos = crypto:supports(), Hashs = proplists:get_value(hashs, Algos), lists:member(Hash, Hashs). + +default_options_map() -> + Fun = fun (_Key, {Default, _}) -> Default end, + maps:map(Fun, ?RULES). diff --git a/lib/ssl/test/ssl_npn_SUITE.erl b/lib/ssl/test/ssl_npn_SUITE.erl index 8a76a3a82b..1f794075c1 100644 --- a/lib/ssl/test/ssl_npn_SUITE.erl +++ b/lib/ssl/test/ssl_npn_SUITE.erl @@ -68,7 +68,8 @@ init_per_suite(Config0) -> catch crypto:stop(), try crypto:start() of ok -> - ssl_test_lib:clean_start(), + ssl_test_lib:clean_start(), + ssl:clear_pem_cache(), Config = ssl_test_lib:make_rsa_cert(Config0), ssl_test_lib:cert_options(Config) catch _:_ -> diff --git a/lib/ssl/test/ssl_npn_hello_SUITE.erl b/lib/ssl/test/ssl_npn_hello_SUITE.erl index e098190ece..68c3962b2c 100644 --- a/lib/ssl/test/ssl_npn_hello_SUITE.erl +++ b/lib/ssl/test/ssl_npn_hello_SUITE.erl @@ -72,7 +72,7 @@ encode_and_decode_client_hello_test(Config) -> Version = ssl_test_lib:protocol_version(Config), {[{DecodedHandshakeMessage, _Raw}], _} = tls_handshake:get_tls_handshake(Version, list_to_binary(HandShakeData), <<>>, - ssl:options_to_map(#ssl_options{})), + default_options_map()), Extensions = DecodedHandshakeMessage#client_hello.extensions, #{next_protocol_negotiation := undefined} = Extensions. %%-------------------------------------------------------------------- @@ -81,7 +81,7 @@ encode_and_decode_npn_client_hello_test(Config) -> Version = ssl_test_lib:protocol_version(Config), {[{DecodedHandshakeMessage, _Raw}], _} = tls_handshake:get_tls_handshake(Version, list_to_binary(HandShakeData), <<>>, - ssl:options_to_map(#ssl_options{})), + default_options_map()), Extensions = DecodedHandshakeMessage#client_hello.extensions, #{next_protocol_negotiation := #next_protocol_negotiation{extension_data = <<>>}} = Extensions. %%-------------------------------------------------------------------- @@ -90,7 +90,7 @@ encode_and_decode_server_hello_test(Config) -> Version = ssl_test_lib:protocol_version(Config), {[{DecodedHandshakeMessage, _Raw}], _} = tls_handshake:get_tls_handshake(Version, list_to_binary(HandShakeData), <<>>, - ssl:options_to_map(#ssl_options{})), + default_options_map()), Extensions = DecodedHandshakeMessage#server_hello.extensions, #{next_protocol_negotiation := undefined} = Extensions. @@ -100,7 +100,7 @@ encode_and_decode_npn_server_hello_test(Config) -> Version = ssl_test_lib:protocol_version(Config), {[{DecodedHandshakeMessage, _Raw}], _} = tls_handshake:get_tls_handshake(Version, list_to_binary(HandShakeData), <<>>, - ssl:options_to_map(#ssl_options{})), + default_options_map()), Extensions = DecodedHandshakeMessage#server_hello.extensions, ct:log("~p ~n", [Extensions]), #{next_protocol_negotiation := #next_protocol_negotiation{extension_data = <<6, "spdy/2">>}} = Extensions. @@ -153,3 +153,7 @@ create_connection_states() -> current_read => #{secure_renegotiation => false } }. + +default_options_map() -> + Fun = fun (_Key, {Default, _}) -> Default end, + maps:map(Fun, ?RULES). diff --git a/lib/ssl/test/ssl_packet_SUITE.erl b/lib/ssl/test/ssl_packet_SUITE.erl index 6d26b2df33..b79a51f02a 100644 --- a/lib/ssl/test/ssl_packet_SUITE.erl +++ b/lib/ssl/test/ssl_packet_SUITE.erl @@ -860,11 +860,11 @@ server_http_decode(Socket, HttpResponse) -> Other4 -> exit({?LINE, Other4}) end, assert_packet_opt(Socket, http), - ok = ssl:send(Socket, HttpResponse), + spawn(fun() -> ssl:send(Socket, HttpResponse) end), ok. client_http_decode(Socket, HttpRequest) -> - ok = ssl:send(Socket, HttpRequest), + spawn(fun() -> ssl:send(Socket, HttpRequest) end), receive {ssl, Socket, {http_response, {1,1}, 200, "OK"}} -> ok; Other1 -> exit({?LINE, Other1}) @@ -922,7 +922,7 @@ packet_http_decode_list(Config) when is_list(Config) -> client_http_decode_list(Socket, HttpRequest) -> - ok = ssl:send(Socket, HttpRequest), + spawn(fun() -> ssl:send(Socket, HttpRequest) end), receive {ssl, Socket, {http_response, {1,1}, 200, "OK"}} -> ok; Other1 -> exit({?LINE, Other1}) @@ -1001,13 +1001,13 @@ server_http_bin_decode(Socket, HttpResponse, Count) when Count > 0 -> Other4 -> exit({?LINE, Other4}) end, assert_packet_opt(Socket, http_bin), - ok = ssl:send(Socket, HttpResponse), + spawn(fun() -> ssl:send(Socket, HttpResponse) end), server_http_bin_decode(Socket, HttpResponse, Count - 1); server_http_bin_decode(_, _, _) -> ok. client_http_bin_decode(Socket, HttpRequest, Count) when Count > 0 -> - ok = ssl:send(Socket, HttpRequest), + spawn(fun() -> ssl:send(Socket, HttpRequest) end), receive {ssl, Socket, {http_response, {1,1}, 200, <<"OK">>}} -> ok; Other1 -> exit({?LINE, Other1}) @@ -1085,7 +1085,7 @@ server_http_decode_error(Socket, HttpResponse) -> {ok, http_eoh} = ssl:recv(Socket, 0), assert_packet_opt(Socket, http), - ok = ssl:send(Socket, HttpResponse), + spawn(fun() -> ssl:send(Socket, HttpResponse) end), ok. %%-------------------------------------------------------------------- packet_httph_active() -> @@ -2009,13 +2009,14 @@ packet(Config, Data, Send, Recv, Quantity, Packet, Active) -> Server = ssl_test_lib:start_server([{node, ClientNode}, {port, 0}, {from, self()}, {mfa, {?MODULE, Send ,[Data, Quantity]}}, - {options, [{packet, Packet} | ServerOpts]}]), + {options, [{packet, Packet}, {nodelay, true}| ServerOpts]}]), Port = ssl_test_lib:inet_port(Server), Client = ssl_test_lib:start_client([{node, ServerNode}, {port, Port}, {host, Hostname}, {from, self()}, {mfa, {?MODULE, Recv, [Data, Quantity]}}, {options, [{active, Active}, + {nodelay, true}, {packet, Packet} | ClientOpts]}]), @@ -2048,7 +2049,7 @@ passive_recv_packet(Socket, _, 0) -> {other, Other, ssl:connection_information(Socket, [session_id, cipher_suite]), 0} end; passive_recv_packet(Socket, Data, N) -> - case ssl:recv(Socket, 0) of + case ssl:recv(Socket, 0, 5000) of {ok, Data} -> passive_recv_packet(Socket, Data, N-1); Other -> @@ -2108,18 +2109,10 @@ active_once_packet(Socket,_, 0) -> end; active_once_packet(Socket, Data, N) -> receive - {ssl, Socket, Byte} when length(Byte) == 1 -> - ssl:setopts(Socket, [{active, once}]), - receive - {ssl, Socket, _} -> - ssl:setopts(Socket, [{active, once}]), - active_once_packet(Socket, Data, N-1) - end; {ssl, Socket, Data} -> - ok - end, - ssl:setopts(Socket, [{active, once}]), - active_once_packet(Socket, Data, N-1). + ssl:setopts(Socket, [{active, once}]), + active_once_packet(Socket, Data, N-1) + end. active_raw(Socket, Data, N) -> active_raw(Socket, (length(Data) * N)). @@ -2140,11 +2133,6 @@ active_packet(Socket, _, 0) -> end; active_packet(Socket, Data, N) -> receive - {ssl, Socket, Byte} when length(Byte) == 1 -> - receive - {ssl, Socket, _} -> - active_packet(Socket, Data, N -1) - end; {ssl, Socket, Data} -> active_packet(Socket, Data, N -1); Other -> @@ -2164,7 +2152,8 @@ server_packet_decode(Socket, Packet) -> {ssl, Socket, Packet} -> ok; Other2 -> exit({?LINE, Other2}) end, - ok = ssl:send(Socket, Packet). + spawn(fun() -> ssl:send(Socket, Packet) end), + ok. client_packet_decode(Socket, Packet) when is_binary(Packet)-> <<P1:10/binary, P2/binary>> = Packet, @@ -2173,14 +2162,12 @@ client_packet_decode(Socket, [Head | Tail] = Packet) -> client_packet_decode(Socket, [Head], Tail, Packet). client_packet_decode(Socket, P1, P2, Packet) -> - ct:log("Packet: ~p ~n", [Packet]), - ok = ssl:send(Socket, P1), - ok = ssl:send(Socket, P2), + spawn(fun() -> ssl:send(Socket, P1), ssl:send(Socket, P2) end), receive {ssl, Socket, Packet} -> ok; Other1 -> exit({?LINE, Other1}) end, - ok = ssl:send(Socket, Packet), + spawn(fun() -> ssl:send(Socket, Packet) end), receive {ssl, Socket, Packet} -> ok; Other2 -> exit({?LINE, Other2}) @@ -2193,10 +2180,11 @@ server_header_decode_active(Socket, Packet, Result) -> {ssl, Socket, Other1} -> check_header_result(Result, Other1) end, - ok = ssl:send(Socket, Packet). + spawn(fun() -> ssl:send(Socket, Packet) end), + ok. client_header_decode_active(Socket, Packet, Result) -> - ok = ssl:send(Socket, Packet), + spawn(fun() -> ssl:send(Socket, Packet) end), receive {ssl, Socket, Result} -> ok; @@ -2211,11 +2199,11 @@ server_header_decode_passive(Socket, Packet, Result) -> {ok, Other} -> check_header_result(Result, Other) end, - ok = ssl:send(Socket, Packet). + spawn(fun() -> ssl:send(Socket, Packet) end), + ok. client_header_decode_passive(Socket, Packet, Result) -> - ok = ssl:send(Socket, Packet), - + spawn(fun() -> ssl:send(Socket, Packet) end), case ssl:recv(Socket, 0) of {ok, Result} -> ok; @@ -2253,7 +2241,8 @@ server_line_packet_decode(Socket, L1, L2, Packet) -> {ssl, Socket, L2} -> ok; Other2 -> exit({?LINE, Other2}) end, - ok = ssl:send(Socket, Packet). + spawn(fun() -> ssl:send(Socket, Packet) end), + ok. client_line_packet_decode(Socket, Packet) when is_binary(Packet)-> <<P1:10/binary, P2/binary>> = Packet, @@ -2264,8 +2253,7 @@ client_line_packet_decode(Socket, [Head | Tail] = Packet) -> client_line_packet_decode(Socket, [Head], Tail, L1 ++ "\n", L2 ++ "\n"). client_line_packet_decode(Socket, P1, P2, L1, L2) -> - ok = ssl:send(Socket, P1), - ok = ssl:send(Socket, P2), + spawn(fun() -> ssl:send(Socket, P1), ssl:send(Socket, P2) end), receive {ssl, Socket, L1} -> ok; Other1 -> exit({?LINE, Other1}) @@ -2305,11 +2293,11 @@ client_reject_packet_opt(Config, PacketOpt) -> send_switch_packet(SslSocket, Data, NextPacket) -> - ssl:send(SslSocket, Data), + spawn(fun() -> ssl:send(SslSocket, Data) end), receive {ssl, SslSocket, "Hello World"} -> ssl:setopts(SslSocket, [{packet, NextPacket}]), - ssl:send(SslSocket, Data), + spawn(fun() -> ssl:send(SslSocket, Data) end), receive {ssl, SslSocket, "Hello World"} -> ok @@ -2318,10 +2306,11 @@ send_switch_packet(SslSocket, Data, NextPacket) -> recv_switch_packet(SslSocket, Data, NextPacket) -> receive {ssl, SslSocket, "Hello World"} -> - ssl:send(SslSocket, Data), + spawn(fun() -> ssl:send(SslSocket, Data) end), ssl:setopts(SslSocket, [{packet, NextPacket}]), receive {ssl, SslSocket, "Hello World"} -> - ssl:send(SslSocket, Data) + spawn(fun() -> ssl:send(SslSocket, Data) end), + ok end end. diff --git a/lib/ssl/test/ssl_payload_SUITE.erl b/lib/ssl/test/ssl_payload_SUITE.erl index 2d0ffd03d7..91043408b7 100644 --- a/lib/ssl/test/ssl_payload_SUITE.erl +++ b/lib/ssl/test/ssl_payload_SUITE.erl @@ -776,7 +776,7 @@ echo_recv(_Socket, 0) -> ok; echo_recv(Socket, Size) -> {ok, Data} = ssl:recv(Socket, 0), - ok = ssl:send(Socket, Data), + spawn(fun() -> ssl:send(Socket, Data) end), echo_recv(Socket, Size - byte_size(Data)). @@ -785,7 +785,7 @@ echo_recv_chunk(_Socket, _, 0) -> ok; echo_recv_chunk(Socket, ChunkSize, Size) -> {ok, Data} = ssl:recv(Socket, ChunkSize), - ok = ssl:send(Socket, Data), + spawn(fun() -> ssl:send(Socket, Data) end), echo_recv_chunk(Socket, ChunkSize, Size - ChunkSize). @@ -795,7 +795,7 @@ echo_active_once(_Socket, 0) -> echo_active_once(Socket, Size) -> receive {ssl, Socket, Data} -> - ok = ssl:send(Socket, Data), + spawn(fun() -> ssl:send(Socket, Data) end), NewSize = Size - byte_size(Data), ssl:setopts(Socket, [{active, once}]), echo_active_once(Socket, NewSize) @@ -807,7 +807,7 @@ echo_active(_Socket, 0) -> echo_active(Socket, Size) -> receive {ssl, Socket, Data} -> - ok = ssl:send(Socket, Data), + spawn(fun() -> ssl:send(Socket, Data) end), echo_active(Socket, Size - byte_size(Data)) end. diff --git a/lib/ssl/test/ssl_session_cache_SUITE.erl b/lib/ssl/test/ssl_session_cache_SUITE.erl index 553c2d247b..f3b1c38d2e 100644 --- a/lib/ssl/test/ssl_session_cache_SUITE.erl +++ b/lib/ssl/test/ssl_session_cache_SUITE.erl @@ -225,15 +225,9 @@ session_cleanup(Config) when is_list(Config) -> %% Make sure session has expired and been cleaned up check_timer(SessionTimer), - ct:sleep(?DELAY *2), %% Delay time + some extra time - - {ServerDelayTimer, ClientDelayTimer} = get_delay_timers(), - - check_timer(ServerDelayTimer), - check_timer(ClientDelayTimer), - + ct:sleep(?SLEEP), %% Make sure clean has had time to run - + undefined = ssl_session_cache:lookup(ClientCache, {{Hostname, Port}, Id}), undefined = ssl_session_cache:lookup(ServerCache, {Port, Id}), @@ -524,24 +518,6 @@ check_timer(Timer) -> ct:sleep(Int), check_timer(Timer) end. - -get_delay_timers() -> - {status, _, _, StatusInfo} = sys:get_status(whereis(ssl_manager)), - [_, _,_, _, Prop] = StatusInfo, - State = ssl_test_lib:state(Prop), - case element(8, State) of - {undefined, undefined} -> - ct:sleep(?SLEEP), - get_delay_timers(); - {undefined, _} -> - ct:sleep(?SLEEP), - get_delay_timers(); - {_, undefined} -> - ct:sleep(?SLEEP), - get_delay_timers(); - DelayTimers -> - DelayTimers - end. wait_for_server() -> ct:sleep(100). diff --git a/lib/ssl/test/ssl_session_ticket_SUITE.erl b/lib/ssl/test/ssl_session_ticket_SUITE.erl new file mode 100644 index 0000000000..83e8e3214d --- /dev/null +++ b/lib/ssl/test/ssl_session_ticket_SUITE.erl @@ -0,0 +1,722 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2007-2019. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +%% +-module(ssl_session_ticket_SUITE). + +%% Note: This directive should only be used in test suites. +-compile(export_all). + +-include("tls_handshake.hrl"). + +-include_lib("common_test/include/ct.hrl"). + +-define(SLEEP, 500). + + +%%-------------------------------------------------------------------- +%% Common Test interface functions ----------------------------------- +%%-------------------------------------------------------------------- +all() -> + [ + {group, 'tlsv1.3'} + ]. + +groups() -> + [{'tlsv1.3', [], [{group, stateful}, {group, stateless}, {group, openssl_server}]}, + {openssl_server, [], [erlang_client_openssl_server_basic, + erlang_client_openssl_server_hrr, + erlang_client_openssl_server_hrr_multiple_tickets + ]}, + {stateful, [], session_tests()}, + {stateless, [], session_tests()}]. + +session_tests() -> + [erlang_client_erlang_server_basic, + openssl_client_erlang_server_basic, + erlang_client_erlang_server_hrr, + openssl_client_erlang_server_hrr, + erlang_client_erlang_server_multiple_tickets, + erlang_client_erlang_server_multiple_tickets_2hash]. + +init_per_suite(Config0) -> + catch crypto:stop(), + try crypto:start() of + ok -> + ssl_test_lib:clean_start(), + ssl_test_lib:make_rsa_cert(Config0) + catch _:_ -> + {skip, "Crypto did not start"} + end. + +end_per_suite(_Config) -> + ssl:stop(), + application:stop(crypto). + +init_per_group(stateful, Config) -> + [{server_ticket_mode, stateful} | proplists:delete(server_ticket_mode, Config)]; +init_per_group(stateless, Config) -> + [{server_ticket_mode, stateless} | proplists:delete(server_ticket_mode, Config)]; +init_per_group(GroupName, Config) -> + ssl_test_lib:clean_tls_version(Config), + case ssl_test_lib:is_tls_version(GroupName) andalso ssl_test_lib:sufficient_crypto_support(GroupName) of + true -> + ssl_test_lib:init_tls_version(GroupName, Config); + _ -> + case ssl_test_lib:sufficient_crypto_support(GroupName) of + true -> + ssl:start(), + Config; + false -> + {skip, "Missing crypto support"} + end + end. + +end_per_group(GroupName, Config) -> + case ssl_test_lib:is_tls_version(GroupName) of + true -> + ssl_test_lib:clean_tls_version(Config); + false -> + Config + end. + +init_per_testcase(_, Config) -> + ssl:stop(), + application:load(ssl), + ssl:start(), + ct:timetrap({seconds, 15}), + Config. + +end_per_testcase(_TestCase, Config) -> + Config. + +%%-------------------------------------------------------------------- +%% Test Cases -------------------------------------------------------- +%%-------------------------------------------------------------------- + + +erlang_client_erlang_server_basic() -> + [{doc,"Test session resumption with session tickets (erlang client - erlang server)"}]. +erlang_client_erlang_server_basic(Config) when is_list(Config) -> + ClientOpts0 = ssl_test_lib:ssl_options(client_rsa_verify_opts, Config), + ServerOpts0 = ssl_test_lib:ssl_options(server_rsa_verify_opts, Config), + {ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config), + ServerTicketMode = proplists:get_value(server_ticket_mode, Config), + + %% Configure session tickets + ClientOpts = [{session_tickets, auto}, {log_level, debug}, + {versions, ['tlsv1.2','tlsv1.3']}|ClientOpts0], + ServerOpts = [{session_tickets, ServerTicketMode}, {log_level, debug}, + {versions, ['tlsv1.2','tlsv1.3']}|ServerOpts0], + + Server0 = + ssl_test_lib:start_server([{node, ServerNode}, {port, 0}, + {from, self()}, + {mfa, {ssl_test_lib, + verify_active_session_resumption, + [false]}}, + {options, ServerOpts}]), + Port0 = ssl_test_lib:inet_port(Server0), + + %% Store ticket from first connection + Client0 = ssl_test_lib:start_client([{node, ClientNode}, + {port, Port0}, {host, Hostname}, + {mfa, {ssl_test_lib, %% Full handshake + verify_active_session_resumption, + [false]}}, + {from, self()}, {options, ClientOpts}]), + ssl_test_lib:check_result(Server0, ok, Client0, ok), + + Server0 ! {listen, {mfa, {ssl_test_lib, + verify_active_session_resumption, + [true]}}}, + + %% Wait for session ticket + ct:sleep(100), + + ssl_test_lib:close(Client0), + + %% Use ticket + Client1 = ssl_test_lib:start_client([{node, ClientNode}, + {port, Port0}, {host, Hostname}, + {mfa, {ssl_test_lib, %% Short handshake + verify_active_session_resumption, + [true]}}, + {from, self()}, {options, ClientOpts}]), + ssl_test_lib:check_result(Server0, ok, Client1, ok), + + process_flag(trap_exit, false), + ssl_test_lib:close(Server0), + ssl_test_lib:close(Client1). + + +erlang_client_openssl_server_basic() -> + [{doc,"Test session resumption with session tickets (erlang client - openssl server)"}]. +erlang_client_openssl_server_basic(Config) when is_list(Config) -> + process_flag(trap_exit, true), + ClientOpts0 = ssl_test_lib:ssl_options(client_rsa_verify_opts, Config), + ServerOpts = ssl_test_lib:ssl_options(server_rsa_verify_opts, Config), + {ClientNode, _, Hostname} = ssl_test_lib:run_where(Config), + + Version = 'tlsv1.3', + Port = ssl_test_lib:inet_port(node()), + CertFile = proplists:get_value(certfile, ServerOpts), + CACertFile = proplists:get_value(cacertfile, ServerOpts), + KeyFile = proplists:get_value(keyfile, ServerOpts), + + %% Configure session tickets + ClientOpts = [{session_tickets, auto}, {log_level, debug}, + {versions, ['tlsv1.2','tlsv1.3']}|ClientOpts0], + + Exe = "openssl", + Args = ["s_server", "-accept", integer_to_list(Port), ssl_test_lib:version_flag(Version), + "-cert", CertFile,"-key", KeyFile, "-CAfile", CACertFile, "-msg", "-debug"], + + OpensslPort = ssl_test_lib:portable_open_port(Exe, Args), + + ssl_test_lib:wait_for_openssl_server(Port, proplists:get_value(protocol, Config)), + + %% Store ticket from first connection + Client0 = ssl_test_lib:start_client([{node, ClientNode}, + {port, Port}, {host, Hostname}, + {mfa, {ssl_test_lib, + verify_active_session_resumption, + [false, no_reply]}}, + {from, self()}, {options, ClientOpts}]), + %% Wait for session ticket + ct:sleep(100), + + %% Close previous connection as s_server can only handle one at a time + ssl_test_lib:close(Client0), + + %% Use ticket + Client1 = ssl_test_lib:start_client([{node, ClientNode}, + {port, Port}, {host, Hostname}, + {mfa, {ssl_test_lib, + verify_active_session_resumption, + [true, no_reply]}}, + {from, self()}, + {options, ClientOpts}]), + process_flag(trap_exit, false), + + %% Clean close down! Server needs to be closed first !! + ssl_test_lib:close_port(OpensslPort), + ssl_test_lib:close(Client1). + + +openssl_client_erlang_server_basic() -> + [{doc,"Test session resumption with session tickets (openssl client - erlang server)"}]. +openssl_client_erlang_server_basic(Config) when is_list(Config) -> + ServerOpts0 = ssl_test_lib:ssl_options(server_rsa_verify_opts, Config), + {_, ServerNode, Hostname} = ssl_test_lib:run_where(Config), + TicketFile0 = filename:join([proplists:get_value(priv_dir, Config), "session_ticket0"]), + TicketFile1 = filename:join([proplists:get_value(priv_dir, Config), "session_ticket1"]), + ServerTicketMode = proplists:get_value(server_ticket_mode, Config), + + Data = "Hello world", + + %% Configure session tickets + ServerOpts = [{session_tickets, ServerTicketMode}, {log_level, debug}, + {versions, ['tlsv1.2','tlsv1.3']}|ServerOpts0], + + Server0 = + ssl_test_lib:start_server([{node, ServerNode}, {port, 0}, + {from, self()}, + {mfa, {ssl_test_lib, + verify_active_session_resumption, + [false]}}, + {options, ServerOpts}]), + + Version = 'tlsv1.3', + Port0 = ssl_test_lib:inet_port(Server0), + + Exe = "openssl", + Args0 = ["s_client", "-connect", ssl_test_lib:hostname_format(Hostname) + ++ ":" ++ integer_to_list(Port0), + ssl_test_lib:version_flag(Version), + "-sess_out", TicketFile0], + + OpenSslPort0 = ssl_test_lib:portable_open_port(Exe, Args0), + + true = port_command(OpenSslPort0, Data), + + ssl_test_lib:check_result(Server0, ok), + + Server0 ! {listen, {mfa, {ssl_test_lib, + verify_active_session_resumption, + [true]}}}, + + %% Wait for session ticket + ct:sleep(100), + + Args1 = ["s_client", "-connect", ssl_test_lib:hostname_format(Hostname) + ++ ":" ++ integer_to_list(Port0), + ssl_test_lib:version_flag(Version), + "-sess_in", TicketFile0, + "-sess_out", TicketFile1], + + OpenSslPort1 = ssl_test_lib:portable_open_port(Exe, Args1), + + true = port_command(OpenSslPort1, Data), + + ssl_test_lib:check_result(Server0, ok), + + %% Clean close down! Server needs to be closed first !! + ssl_test_lib:close(Server0), + ssl_test_lib:close_port(OpenSslPort0), + ssl_test_lib:close_port(OpenSslPort1). + + +erlang_client_erlang_server_hrr() -> + [{doc,"Test session resumption with session tickets and hello_retry_request (erlang client - erlang server)"}]. +erlang_client_erlang_server_hrr(Config) when is_list(Config) -> + ClientOpts0 = ssl_test_lib:ssl_options(client_rsa_verify_opts, Config), + ServerOpts0 = ssl_test_lib:ssl_options(server_rsa_verify_opts, Config), + {ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config), + ServerTicketMode = proplists:get_value(server_ticket_mode, Config), + + %% Configure session tickets + ClientOpts = [{session_tickets, auto}, {log_level, debug}, + {versions, ['tlsv1.2','tlsv1.3']}, + {supported_groups,[secp256r1, x25519]}|ClientOpts0], + ServerOpts = [{session_tickets, ServerTicketMode}, {log_level, debug}, + {versions, ['tlsv1.2','tlsv1.3']}, + {supported_groups, [x448, x25519]}|ServerOpts0], + + Server0 = + ssl_test_lib:start_server([{node, ServerNode}, {port, 0}, + {from, self()}, + {mfa, {ssl_test_lib, + verify_active_session_resumption, + [false]}}, + {options, ServerOpts}]), + Port0 = ssl_test_lib:inet_port(Server0), + + %% Store ticket from first connection + Client0 = ssl_test_lib:start_client([{node, ClientNode}, + {port, Port0}, {host, Hostname}, + {mfa, {ssl_test_lib, + verify_active_session_resumption, + [false]}}, + {from, self()}, {options, ClientOpts}]), + ssl_test_lib:check_result(Server0, ok, Client0, ok), + + Server0 ! {listen, {mfa, {ssl_test_lib, + verify_active_session_resumption, + [true]}}}, + + %% Wait for session ticket + ct:sleep(100), + + ssl_test_lib:close(Client0), + + %% Use ticket + Client1 = ssl_test_lib:start_client([{node, ClientNode}, + {port, Port0}, {host, Hostname}, + {mfa, {ssl_test_lib, + verify_active_session_resumption, + [true]}}, + {from, self()}, {options, ClientOpts}]), + ssl_test_lib:check_result(Server0, ok, Client1, ok), + + process_flag(trap_exit, false), + ssl_test_lib:close(Server0), + ssl_test_lib:close(Client1). + + +erlang_client_openssl_server_hrr() -> + [{doc,"Test session resumption with session tickets and hello_retry_request (erlang client - openssl server)"}]. +erlang_client_openssl_server_hrr(Config) when is_list(Config) -> + process_flag(trap_exit, true), + ClientOpts0 = ssl_test_lib:ssl_options(client_rsa_verify_opts, Config), + ServerOpts = ssl_test_lib:ssl_options(server_rsa_verify_opts, Config), + {ClientNode, _, Hostname} = ssl_test_lib:run_where(Config), + + Version = 'tlsv1.3', + Port = ssl_test_lib:inet_port(node()), + CertFile = proplists:get_value(certfile, ServerOpts), + CACertFile = proplists:get_value(cacertfile, ServerOpts), + KeyFile = proplists:get_value(keyfile, ServerOpts), + + %% Configure session tickets + ClientOpts = [{session_tickets, auto}, {log_level, debug}, + {versions, ['tlsv1.2','tlsv1.3']}, + {supported_groups,[secp256r1, x25519]}|ClientOpts0], + + Exe = "openssl", + Args = ["s_server", "-accept", integer_to_list(Port), ssl_test_lib:version_flag(Version), + "-cert", CertFile, + "-key", KeyFile, + "-CAfile", CACertFile, + "-groups", "X448:X25519", + "-msg", "-debug"], + + OpensslPort = ssl_test_lib:portable_open_port(Exe, Args), + + ssl_test_lib:wait_for_openssl_server(Port, proplists:get_value(protocol, Config)), + + %% Store ticket from first connection + Client0 = ssl_test_lib:start_client([{node, ClientNode}, + {port, Port}, {host, Hostname}, + {mfa, {ssl_test_lib, + verify_active_session_resumption, + [false, no_reply]}}, + {from, self()}, {options, ClientOpts}]), + %% Wait for session ticket + ct:sleep(100), + + %% Close previous connection as s_server can only handle one at a time + ssl_test_lib:close(Client0), + + %% Use ticket + Client1 = ssl_test_lib:start_client([{node, ClientNode}, + {port, Port}, {host, Hostname}, + {mfa, {ssl_test_lib, + verify_active_session_resumption, + [true, no_reply]}}, + {from, self()}, + {options, ClientOpts}]), + process_flag(trap_exit, false), + + %% Clean close down! Server needs to be closed first !! + ssl_test_lib:close_port(OpensslPort), + ssl_test_lib:close(Client1). + + +openssl_client_erlang_server_hrr() -> + [{doc,"Test session resumption with session tickets and hello_retry_request (openssl client - erlang server)"}]. +openssl_client_erlang_server_hrr(Config) when is_list(Config) -> + ServerOpts0 = ssl_test_lib:ssl_options(server_rsa_verify_opts, Config), + {_, ServerNode, Hostname} = ssl_test_lib:run_where(Config), + TicketFile0 = filename:join([proplists:get_value(priv_dir, Config), "session_ticket0"]), + TicketFile1 = filename:join([proplists:get_value(priv_dir, Config), "session_ticket1"]), + ServerTicketMode = proplists:get_value(server_ticket_mode, Config), + + Data = "Hello world", + + %% Configure session tickets + ServerOpts = [{session_tickets, ServerTicketMode}, {log_level, debug}, + {versions, ['tlsv1.2','tlsv1.3']}, + {supported_groups,[x448, x25519]}|ServerOpts0], + + Server0 = + ssl_test_lib:start_server([{node, ServerNode}, {port, 0}, + {from, self()}, + {mfa, {ssl_test_lib, + verify_active_session_resumption, + [false]}}, + {options, ServerOpts}]), + + Version = 'tlsv1.3', + Port0 = ssl_test_lib:inet_port(Server0), + + Exe = "openssl", + Args0 = ["s_client", "-connect", ssl_test_lib:hostname_format(Hostname) + ++ ":" ++ integer_to_list(Port0), + ssl_test_lib:version_flag(Version), + "-groups", "P-256:X25519", + "-sess_out", TicketFile0], + + OpenSslPort0 = ssl_test_lib:portable_open_port(Exe, Args0), + + true = port_command(OpenSslPort0, Data), + + ssl_test_lib:check_result(Server0, ok), + + Server0 ! {listen, {mfa, {ssl_test_lib, + verify_active_session_resumption, + [true]}}}, + + %% Wait for session ticket + ct:sleep(100), + + Args1 = ["s_client", "-connect", ssl_test_lib:hostname_format(Hostname) + ++ ":" ++ integer_to_list(Port0), + ssl_test_lib:version_flag(Version), + "-groups", "P-256:X25519", + "-sess_in", TicketFile0, + "-sess_out", TicketFile1], + + OpenSslPort1 = ssl_test_lib:portable_open_port(Exe, Args1), + + true = port_command(OpenSslPort1, Data), + + ssl_test_lib:check_result(Server0, ok), + + %% Clean close down! Server needs to be closed first !! + ssl_test_lib:close(Server0), + ssl_test_lib:close_port(OpenSslPort0), + ssl_test_lib:close_port(OpenSslPort1). + + +erlang_client_erlang_server_multiple_tickets() -> + [{doc,"Test session resumption with multiple session tickets (erlang client - erlang server)"}]. +erlang_client_erlang_server_multiple_tickets(Config) when is_list(Config) -> + ClientOpts0 = ssl_test_lib:ssl_options(client_rsa_verify_opts, Config), + ServerOpts0 = ssl_test_lib:ssl_options(server_rsa_verify_opts, Config), + {ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config), + ServerTicketMode = proplists:get_value(server_ticket_mode, Config), + + %% Configure session tickets + ClientOpts = [{session_tickets, enabled}, {log_level, debug}, + {versions, ['tlsv1.2','tlsv1.3']}|ClientOpts0], + ServerOpts = [{session_tickets, ServerTicketMode}, {log_level, debug}, + {versions, ['tlsv1.2','tlsv1.3']}|ServerOpts0], + + Server0 = + ssl_test_lib:start_server([{node, ServerNode}, {port, 0}, + {from, self()}, + {mfa, {ssl_test_lib, + verify_active_session_resumption, + [false]}}, + {options, ServerOpts}]), + Port0 = ssl_test_lib:inet_port(Server0), + + %% Store ticket from first connection + Client0 = ssl_test_lib:start_client([{node, ClientNode}, + {port, Port0}, {host, Hostname}, + {mfa, {ssl_test_lib, + verify_active_session_resumption, + [false, wait_reply, {tickets, 3}]}}, + {from, self()}, {options, ClientOpts}]), + + Tickets0 = ssl_test_lib:check_tickets(Client0), + + ct:pal("Received tickets: ~p~n", [Tickets0]), + + ssl_test_lib:check_result(Server0, ok), + + Server0 ! {listen, {mfa, {ssl_test_lib, + verify_active_session_resumption, + [true]}}}, + + ssl_test_lib:close(Client0), + + %% Use ticket + Client1 = ssl_test_lib:start_client([{node, ClientNode}, + {port, Port0}, {host, Hostname}, + {mfa, {ssl_test_lib, + verify_active_session_resumption, + [true, wait_reply, no_tickets]}}, + {from, self()}, + {options, [{use_ticket, Tickets0}|ClientOpts]}]), + + ssl_test_lib:check_result(Server0, ok, Client1, ok), + + process_flag(trap_exit, false), + ssl_test_lib:close(Server0), + ssl_test_lib:close(Client1). + + +erlang_client_openssl_server_hrr_multiple_tickets() -> + [{doc,"Test session resumption with multiple session tickets and hello_retry_request (erlang client - openssl server)"}]. +erlang_client_openssl_server_hrr_multiple_tickets(Config) when is_list(Config) -> + process_flag(trap_exit, true), + ClientOpts0 = ssl_test_lib:ssl_options(client_rsa_verify_opts, Config), + ServerOpts = ssl_test_lib:ssl_options(server_rsa_verify_opts, Config), + {ClientNode, _, Hostname} = ssl_test_lib:run_where(Config), + + Version = 'tlsv1.3', + Port = ssl_test_lib:inet_port(node()), + CertFile = proplists:get_value(certfile, ServerOpts), + CACertFile = proplists:get_value(cacertfile, ServerOpts), + KeyFile = proplists:get_value(keyfile, ServerOpts), + + %% Configure session tickets + ClientOpts = [{session_tickets, enabled}, {log_level, debug}, + {versions, ['tlsv1.2','tlsv1.3']}, + {supported_groups,[secp256r1, x25519]}|ClientOpts0], + + Exe = "openssl", + Args = ["s_server", "-accept", integer_to_list(Port), ssl_test_lib:version_flag(Version), + "-cert", CertFile, + "-key", KeyFile, + "-CAfile", CACertFile, + "-groups", "X448:X25519", + "-msg", "-debug"], + + OpensslPort = ssl_test_lib:portable_open_port(Exe, Args), + + ssl_test_lib:wait_for_openssl_server(Port, proplists:get_value(protocol, Config)), + + %% Store ticket from first connection + Client0 = ssl_test_lib:start_client([{node, ClientNode}, + {port, Port}, {host, Hostname}, + {mfa, {ssl_test_lib, + verify_active_session_resumption, + [false, no_reply, {tickets, 2}]}}, + {from, self()}, {options, ClientOpts}]), + + Tickets0 = ssl_test_lib:check_tickets(Client0), + + ct:pal("Received tickets: ~p~n", [Tickets0]), + + %% Close previous connection as s_server can only handle one at a time + ssl_test_lib:close(Client0), + + %% Use tickets + Client1 = ssl_test_lib:start_client([{node, ClientNode}, + {port, Port}, {host, Hostname}, + {mfa, {ssl_test_lib, + verify_active_session_resumption, + [true, no_reply, no_tickets]}}, + {from, self()}, + {options, [{use_ticket, Tickets0}|ClientOpts]}]), + + process_flag(trap_exit, false), + + %% Clean close down! Server needs to be closed first !! + ssl_test_lib:close_port(OpensslPort), + ssl_test_lib:close(Client1). + + +erlang_client_erlang_server_multiple_tickets_2hash() -> + [{doc,"Test session resumption with multiple session tickets with 2 different hash algorithms (erlang client - erlang server)"}]. +erlang_client_erlang_server_multiple_tickets_2hash(Config) when is_list(Config) -> + ClientOpts0 = ssl_test_lib:ssl_options(client_rsa_verify_opts, Config), + ServerOpts0 = ssl_test_lib:ssl_options(server_rsa_verify_opts, Config), + {ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config), + ServerTicketMode = proplists:get_value(server_ticket_mode, Config), + + %% Configure session tickets + ClientOpts = [{session_tickets, enabled}, {log_level, debug}, + {versions, ['tlsv1.2','tlsv1.3']}|ClientOpts0], + ServerOpts = [{session_tickets, ServerTicketMode}, {log_level, debug}, + {versions, ['tlsv1.2','tlsv1.3']}|ServerOpts0], + + Server0 = + ssl_test_lib:start_server([{node, ServerNode}, {port, 0}, + {from, self()}, + {mfa, {ssl_test_lib, + verify_active_session_resumption, + [false]}}, + {options, ServerOpts}]), + Port0 = ssl_test_lib:inet_port(Server0), + + %% Get tickets using sha256 + Client0 = ssl_test_lib:start_client([{node, ClientNode}, + {port, Port0}, {host, Hostname}, + {mfa, {ssl_test_lib, + verify_active_session_resumption, + [false, wait_reply, {tickets, 3}]}}, + {from, self()}, + {options, [{ciphers, [#{key_exchange => any, + cipher => aes_128_gcm, + mac => aead, + prf => sha256}]}| ClientOpts]}]), + + Tickets0 = ssl_test_lib:check_tickets(Client0), + + ct:pal("Received tickets: ~p~n", [Tickets0]), + + ssl_test_lib:check_result(Server0, ok), + + Server0 ! {listen, {mfa, {ssl_test_lib, + verify_active_session_resumption, + [false]}}}, + + ssl_test_lib:close(Client0), + + %% Get tickets using sha384 + Client1 = ssl_test_lib:start_client([{node, ClientNode}, + {port, Port0}, {host, Hostname}, + {mfa, {ssl_test_lib, + verify_active_session_resumption, + [false, wait_reply, {tickets, 3}]}}, + {from, self()}, + {options, [{ciphers, [#{key_exchange => any, + cipher => aes_256_gcm, + mac => aead, + prf => sha384}]}| ClientOpts]}]), + + Tickets1 = ssl_test_lib:check_tickets(Client1), + + ct:pal("Received tickets: ~p~n", [Tickets1]), + + ssl_test_lib:check_result(Server0, ok), + + Server0 ! {listen, {mfa, {ssl_test_lib, + verify_active_session_resumption, + [true]}}}, + + ssl_test_lib:close(Client1), + + %% Use tickets for handshake (server chooses TLS_AES_256_GCM_SHA384 cipher suite) + Client2 = ssl_test_lib:start_client([{node, ClientNode}, + {port, Port0}, {host, Hostname}, + {mfa, {ssl_test_lib, %% Short handshake + verify_active_session_resumption, + [true]}}, + {from, self()}, + {options, [{use_ticket, Tickets0 ++ Tickets1}|ClientOpts]}]), + ssl_test_lib:check_result(Server0, ok, Client2, ok), + + Server0 ! {listen, {mfa, {ssl_test_lib, + verify_active_session_resumption, + [true]}}}, + + ssl_test_lib:close(Client2), + + %% Use tickets for handshake (client chooses TLS_CHACHA20_POLY1305_SHA256 cipher suite) + Client3 = ssl_test_lib:start_client([{node, ClientNode}, + {port, Port0}, {host, Hostname}, + {mfa, {ssl_test_lib, %% Short handshake + verify_active_session_resumption, + [true]}}, + {from, self()}, + {options, [{ciphers, [#{key_exchange => any, + cipher => chacha20_poly1305, + mac => aead, + prf => sha256}]}, + {use_ticket, Tickets0 ++ Tickets1}|ClientOpts]}]), + ssl_test_lib:check_result(Server0, ok, Client3, ok), + + Server0 ! {listen, {mfa, {ssl_test_lib, + verify_active_session_resumption, + [false]}}}, + + ssl_test_lib:close(Client3), + + %% Use tickets (created using sha384) for handshake (client chooses + %% TLS_CHACHA20_POLY1305_SHA256 cipher suite). + %% Session resumption should fail as chosen cipher suite uses different hash algorithms + %% than those supplied by the selected tickets. + Client4 = ssl_test_lib:start_client([{node, ClientNode}, + {port, Port0}, {host, Hostname}, + {mfa, {ssl_test_lib, %% Short handshake + verify_active_session_resumption, + [false]}}, + {from, self()}, + {options, [{ciphers, [#{key_exchange => any, + cipher => chacha20_poly1305, + mac => aead, + prf => sha256}]}, + {use_ticket, Tickets1}|ClientOpts]}]), + ssl_test_lib:check_result(Server0, ok, Client4, ok), + + ssl_test_lib:close(Client4), + + process_flag(trap_exit, false), + ssl_test_lib:close(Server0). + + +%%-------------------------------------------------------------------- +%% Internal functions ------------------------------------------------ +%%-------------------------------------------------------------------- diff --git a/lib/ssl/test/ssl_test_lib.erl b/lib/ssl/test/ssl_test_lib.erl index cb528f185a..6218857e05 100644 --- a/lib/ssl/test/ssl_test_lib.erl +++ b/lib/ssl/test/ssl_test_lib.erl @@ -120,9 +120,7 @@ do_run_server(ListenSocket, AcceptSocket, Opts) -> ct:log("~p:~p~nServer closing ~p ~n", [?MODULE,?LINE, self()]), Result = Transport:close(AcceptSocket), Result1 = Transport:close(ListenSocket), - ct:log("~p:~p~nResult ~p : ~p ~n", [?MODULE,?LINE, Result, Result1]); - {ssl_closed, _} -> - ok + ct:log("~p:~p~nResult ~p : ~p ~n", [?MODULE,?LINE, Result, Result1]) end. %%% To enable to test with s_client -reconnect @@ -537,13 +535,17 @@ check_server_alert(Pid, Alert) -> check_server_txt(STxt), ok; {Pid, {error, closed}} -> - ok + ok; + {Pid, {ok, _}} -> + ct:fail("Successful connection during negative test.") end. check_server_alert(Server, Client, Alert) -> receive {Server, {error, {tls_alert, {Alert, STxt}}}} -> check_server_txt(STxt), - check_client_alert(Client, Alert) + check_client_alert(Client, Alert); + {Server, {ok, _}} -> + ct:fail("Successful connection during negative test.") end. check_client_alert(Pid, Alert) -> receive @@ -554,7 +556,9 @@ check_client_alert(Pid, Alert) -> check_client_txt(CTxt), ok; {Pid, {error, closed}} -> - ok + ok; + {Pid, {ok, _}} -> + ct:fail("Successful connection during negative test.") end. check_client_alert(Server, Client, Alert) -> receive @@ -565,7 +569,9 @@ check_client_alert(Server, Client, Alert) -> check_client_txt(CTxt), ok; {Client, {error, closed}} -> - ok + ok; + {Client, {ok, _}} -> + ct:fail("Successful connection during negative test.") end. check_server_txt("TLS server" ++ _) -> ok; @@ -1832,6 +1838,7 @@ state([{data,[{"StateData", State}]} | _]) -> %% gen_fsm state([_ | Rest]) -> state(Rest). +%% TODO: DTLS considered tls version in this use maybe rename is_tls_version('dtlsv1.2') -> true; is_tls_version('dtlsv1') -> @@ -1849,6 +1856,13 @@ is_tls_version('sslv3') -> is_tls_version(_) -> false. +is_dtls_version('dtlsv1.2') -> + true; +is_dtls_version('dtlsv1') -> + true; +is_dtls_version(_) -> + false. + init_tls_version(Version, Config) when Version == 'dtlsv1.2'; Version == 'dtlsv1' -> ssl:stop(), @@ -1936,6 +1950,69 @@ send_recv_result_active_once(Socket) -> ssl:send(Socket, Data), active_once_recv_list(Socket, length(Data)). +verify_active_session_resumption(Socket, SessionResumption) -> + verify_active_session_resumption(Socket, SessionResumption, wait_reply, no_tickets). +%% +verify_active_session_resumption(Socket, SessionResumption, WaitReply) -> + verify_active_session_resumption(Socket, SessionResumption, WaitReply, no_tickets). +%% +verify_active_session_resumption(Socket, SessionResumption, WaitForReply, TicketOption) -> + case ssl:connection_information(Socket, [session_resumption]) of + {ok, [{session_resumption, SessionResumption}]} -> + Msg = boolean_to_log_msg(SessionResumption), + ct:log("~p:~p~nSession resumption verified! (expected ~p, got ~p)!", + [?MODULE, ?LINE, Msg, Msg]); + {ok, [{session_resumption, Got0}]} -> + Expected = boolean_to_log_msg(SessionResumption), + Got = boolean_to_log_msg(Got0), + ct:fail("~p:~p~nFailed to verify session resumption! (expected ~p, got ~p)", + [?MODULE, ?LINE, Expected, Got]) + end, + + Data = "Hello world", + ssl:send(Socket, Data), + case WaitForReply of + wait_reply -> + Data = active_recv(Socket, length(Data)); + no_reply -> + ok; + Else1 -> + ct:fail("~p:~p~nFaulty parameter: ~p", [?MODULE, ?LINE, Else1]) + end, + case TicketOption of + {tickets, N} -> + receive_tickets(N); + no_tickets -> + ok; + Else2 -> + ct:fail("~p:~p~nFaulty parameter: ~p", [?MODULE, ?LINE, Else2]) + end. + +boolean_to_log_msg(true) -> + "OK"; +boolean_to_log_msg(false) -> + "FAIL". + +receive_tickets(N) -> + receive_tickets(N, []). +%% +receive_tickets(0, Acc) -> + Acc; +receive_tickets(N, Acc) -> + receive + {ssl, session_ticket, {_, Ticket}} -> + receive_tickets(N - 1, [Ticket|Acc]) + end. + +check_tickets(Client) -> + receive + {Client, Tickets} -> + Tickets + after + 5000 -> + ct:fail("~p:~p~nNo tickets received!", [?MODULE, ?LINE]) + end. + active_recv(Socket, N) -> active_recv(Socket, N, []). @@ -2219,7 +2296,7 @@ openssl_allows_client_renegotaite(Config) -> case os:cmd("openssl version") of "OpenSSL 1.1" ++ _ -> {skip, "OpenSSL does not allow client renegotiation"}; - "LibreSSL 2" ++ _ -> + "LibreSSL" ++ _ -> {skip, "LibreSSL does not allow client renegotiation"}; _ -> Config @@ -2795,6 +2872,18 @@ openssl_sane_dtls_alpn() -> case os:cmd("openssl version") of "OpenSSL 1.1.0g" ++ _ -> false; + "OpenSSL 1.1.1 " ++ _ -> + false; + "OpenSSL 1.1.1a" ++ _ -> + false; + _-> + openssl_sane_dtls() + end. + +openssl_sane_dtls_session_reuse() -> + case os:cmd("openssl version") of + "OpenSSL 1.1.1 " ++ _ -> + false; "OpenSSL 1.1.1a" ++ _ -> false; _-> diff --git a/lib/ssl/test/tls_1_3_record_SUITE.erl b/lib/ssl/test/tls_1_3_record_SUITE.erl index 5df8853b1b..26d7694e16 100644 --- a/lib/ssl/test/tls_1_3_record_SUITE.erl +++ b/lib/ssl/test/tls_1_3_record_SUITE.erl @@ -26,13 +26,15 @@ -include_lib("common_test/include/ct.hrl"). -include_lib("ssl/src/tls_record.hrl"). -include_lib("ssl/src/tls_handshake.hrl"). +-include_lib("ssl/src/tls_handshake_1_3.hrl"). -include_lib("ssl/src/ssl_cipher.hrl"). -include_lib("ssl/src/ssl_internal.hrl"). all() -> [encode_decode, finished_verify_data, - '1_RTT_handshake']. + '1_RTT_handshake', + '0_RTT_handshake']. init_per_suite(Config) -> catch crypto:stop(), @@ -79,7 +81,7 @@ encode_decode(_Config) -> <<128,229,186,211,62,127,182,20,62,166,233,23,135,64,121, 3,104,251,214,161,253,31,3,2,232,37,8,221,189,72,64,218, 121,41,112,148,254,34,68,164,228,60,161,201,132,55,56, - 157>>}, + 157>>}, undefined, undefined, <<92,24,205,75,244,60,136,212,250,32,214,20,37,3,213,87,61,207, 147,61,168,145,177,118,160,153,33,53,48,108,191,174>>, @@ -105,7 +107,7 @@ encode_decode(_Config) -> <<128,229,186,211,62,127,182,20,62,166,233,23,135,64,121, 3,104,251,214,161,253,31,3,2,232,37,8,221,189,72,64,218, 121,41,112,148,254,34,68,164,228,60,161,201,132,55,56, - 157>>}, + 157>>}, undefined, undefined, <<92,24,205,75,244,60,136,212,250,32,214,20,37,3,213,87,61,207, 147,61,168,145,177,118,160,153,33,53,48,108,191,174>>, @@ -735,6 +737,22 @@ encode_decode(_Config) -> SAPTrafficSecret = tls_v1:server_application_traffic_secret_0(HKDFAlgo, {master_secret, MasterSecret}, CHSF), + %% Resumption master secret from '0-RTT' + %% + %% {server} generate resumption secret "tls13 resumption": + %% + %% PRK (32 octets): 7d f2 35 f2 03 1d 2a 05 12 87 d0 2b 02 41 b0 bf + %% da f8 6c c8 56 23 1f 2d 5a ba 46 c4 34 ec 19 6c + %% + RMS = hexstr2bin("7d f2 35 f2 03 1d 2a 05 12 87 d0 2b 02 41 b0 bf + da f8 6c c8 56 23 1f 2d 5a ba 46 c4 34 ec 19 6c"), + + %% Verify calculation of resumption master secret that is used to create + %% the pre shared key in '0-RTT'. + Temp = tls_v1:resumption_master_secret(HKDFAlgo, {master_secret, MasterSecret}, CHSF), + erlang:display({rms, RMS}), + erlang:display({new_rms, Temp}), + %% {server} derive secret "tls13 exp master": %% %% PRK (32 octets): 18 df 06 84 3d 13 a0 8b f2 a4 49 84 4c 5f 8a 47 @@ -783,7 +801,7 @@ encode_decode(_Config) -> %% PRK = SAPTrafficsecret %% key info = WriteKeyInfo - %% iv info = WrtieIVInfo + %% iv info = WriteIVInfo SWKey = hexstr2bin("9f 02 28 3b 6c 9c 07 ef c2 6b b9 f2 ac 92 e3 56"), @@ -815,7 +833,449 @@ encode_decode(_Config) -> SRIV = hexstr2bin("5b d3 c7 1b 83 6e 0b 76 bb 73 26 5f"), - {SRKey, SRIV} = tls_v1:calculate_traffic_keys(HKDFAlgo, Cipher, CHSTrafficSecret). + {SRKey, SRIV} = tls_v1:calculate_traffic_keys(HKDFAlgo, Cipher, CHSTrafficSecret), + + %% {client} calculate finished "tls13 finished": + %% + %% PRK (32 octets): b3 ed db 12 6e 06 7f 35 a7 80 b3 ab f4 5e 2d 8f + %% 3b 1a 95 07 38 f5 2e 96 00 74 6a 0e 27 a5 5a 21 + %% + %% hash (0 octets): (empty) + %% + %% info (18 octets): 00 20 0e 74 6c 73 31 33 20 66 69 6e 69 73 68 65 + %% 64 00 + %% + %% expanded (32 octets): b8 0a d0 10 15 fb 2f 0b d6 5f f7 d4 da 5d + %% 6b f8 3f 84 82 1d 1f 87 fd c7 d3 c7 5b 5a 7b 42 d9 c4 + %% + %% finished (32 octets): a8 ec 43 6d 67 76 34 ae 52 5a c1 fc eb e1 + %% 1a 03 9e c1 76 94 fa c6 e9 85 27 b6 42 f2 ed d5 ce 61 + + %% PRK = CHSTrafficsecret + %% info = FInfo + CFExpanded = + hexstr2bin("b8 0a d0 10 15 fb 2f 0b d6 5f f7 d4 da 5d + 6b f8 3f 84 82 1d 1f 87 fd c7 d3 c7 5b 5a 7b 42 d9 c4"), + + CFinishedVerifyData = + hexstr2bin("a8 ec 43 6d 67 76 34 ae 52 5a c1 fc eb e1 + 1a 03 9e c1 76 94 fa c6 e9 85 27 b6 42 f2 ed d5 ce 61"), + + MessageHistory1 = [FinishedHSBin, + CertificateVerify, + Certificate, + EncryptedExtensions, + ServerHello, + ClientHello], + + CFExpanded = tls_v1:finished_key(CHSTrafficSecret, HKDFAlgo), + CFinishedVerifyData = tls_v1:finished_verify_data(CFExpanded, HKDFAlgo, MessageHistory1), + + %% {client} construct a Finished handshake message: + %% + %% Finished (36 octets): 14 00 00 20 a8 ec 43 6d 67 76 34 ae 52 5a + %% c1 fc eb e1 1a 03 9e c1 76 94 fa c6 e9 85 27 b6 42 f2 ed d5 ce + %% 61 + CFinishedBin = + hexstr2bin("14 00 00 20 a8 ec 43 6d 67 76 34 ae 52 5a + c1 fc eb e1 1a 03 9e c1 76 94 fa c6 e9 85 27 b6 42 f2 ed d5 ce + 61"), + + CFinished = #finished{verify_data = CFinishedVerifyData}, + + CFinishedIOList = tls_handshake:encode_handshake(CFinished, {3,4}), + CFinishedBin = iolist_to_binary(CFinishedIOList), + + %% {client} derive write traffic keys for application data: + %% + %% PRK (32 octets): 9e 40 64 6c e7 9a 7f 9d c0 5a f8 88 9b ce 65 52 + %% 87 5a fa 0b 06 df 00 87 f7 92 eb b7 c1 75 04 a5 + %% + %% key info (13 octets): 00 10 09 74 6c 73 31 33 20 6b 65 79 00 + %% + %% key expanded (16 octets): 17 42 2d da 59 6e d5 d9 ac d8 90 e3 c6 + %% 3f 50 51 + %% + %% iv info (12 octets): 00 0c 08 74 6c 73 31 33 20 69 76 00 + %% + %% iv expanded (12 octets): 5b 78 92 3d ee 08 57 90 33 e5 23 d9 + + %% PRK = CAPTrafficsecret + %% key info = WriteKeyInfo + %% iv info = WriteIVInfo + + CWKey = + hexstr2bin("17 42 2d da 59 6e d5 d9 ac d8 90 e3 c6 3f 50 51"), + + CWIV = + hexstr2bin("5b 78 92 3d ee 08 57 90 33 e5 23 d9"), + + {CWKey, CWIV} = tls_v1:calculate_traffic_keys(HKDFAlgo, Cipher, CAPTrafficSecret), + + %% {client} derive secret "tls13 res master": + %% + %% PRK (32 octets): 18 df 06 84 3d 13 a0 8b f2 a4 49 84 4c 5f 8a 47 + %% 80 01 bc 4d 4c 62 79 84 d5 a4 1d a8 d0 40 29 19 + %% + %% hash (32 octets): 20 91 45 a9 6e e8 e2 a1 22 ff 81 00 47 cc 95 26 + %% 84 65 8d 60 49 e8 64 29 42 6d b8 7c 54 ad 14 3d + %% + %% info (52 octets): 00 20 10 74 6c 73 31 33 20 72 65 73 20 6d 61 73 + %% 74 65 72 20 20 91 45 a9 6e e8 e2 a1 22 ff 81 00 47 cc 95 26 84 + %% 65 8d 60 49 e8 64 29 42 6d b8 7c 54 ad 14 3d + %% + %% expanded (32 octets): 7d f2 35 f2 03 1d 2a 05 12 87 d0 2b 02 41 + %% b0 bf da f8 6c c8 56 23 1f 2d 5a ba 46 c4 34 ec 19 6c + + %% PRK = MasterSecret + + CRMHash = hexstr2bin("20 91 45 a9 6e e8 e2 a1 22 ff 81 00 47 cc 95 26 + 84 65 8d 60 49 e8 64 29 42 6d b8 7c 54 ad 14 3d"), + + CRMInfo = hexstr2bin(" 00 20 10 74 6c 73 31 33 20 72 65 73 20 6d 61 73 + 74 65 72 20 20 91 45 a9 6e e8 e2 a1 22 ff 81 00 47 cc 95 26 84 + 65 8d 60 49 e8 64 29 42 6d b8 7c 54 ad 14 3d"), + + ResumptionMasterSecret = hexstr2bin("7d f2 35 f2 03 1d 2a 05 12 87 d0 2b 02 41 + b0 bf da f8 6c c8 56 23 1f 2d 5a ba 46 c4 34 ec 19 6c"), + + CHCF = <<ClientHello/binary, + ServerHello/binary, + EncryptedExtensions/binary, + Certificate/binary, + CertificateVerify/binary, + FinishedHSBin/binary, + CFinishedBin/binary>>, + + MessageHistory3 = [ClientHello, + ServerHello, + EncryptedExtensions, + Certificate, + CertificateVerify, + FinishedHSBin, + CFinishedBin + ], + + CRMHash = crypto:hash(HKDFAlgo, CHCF), + + CRMInfo = + tls_v1:create_info(<<"res master">>, CRMHash, ssl_cipher:hash_size(HKDFAlgo)), + + ResumptionMasterSecret = + tls_v1:resumption_master_secret(HKDFAlgo, {master_secret, MasterSecret}, MessageHistory3), + ok. + +%%-------------------------------------------------------------------- +'0_RTT_handshake'() -> + [{doc,"Test TLS 1.3 0-RTT Handshake"}]. + +'0_RTT_handshake'(_Config) -> + HKDFAlgo = sha256, + + %% {server} generate resumption secret "tls13 resumption": + %% + %% PRK (32 octets): 7d f2 35 f2 03 1d 2a 05 12 87 d0 2b 02 41 b0 bf + %% da f8 6c c8 56 23 1f 2d 5a ba 46 c4 34 ec 19 6c + %% + %% hash (2 octets): 00 00 + %% + %% info (22 octets): 00 20 10 74 6c 73 31 33 20 72 65 73 75 6d 70 74 + %% 69 6f 6e 02 00 00 + %% + %% expanded (32 octets): 4e cd 0e b6 ec 3b 4d 87 f5 d6 02 8f 92 2c + %% a4 c5 85 1a 27 7f d4 13 11 c9 e6 2d 2c 94 92 e1 c4 f3 + %% + %% {server} construct a NewSessionTicket handshake message: + %% + %% NewSessionTicket (205 octets): 04 00 00 c9 00 00 00 1e fa d6 aa + %% c5 02 00 00 00 b2 2c 03 5d 82 93 59 ee 5f f7 af 4e c9 00 00 00 + %% 00 26 2a 64 94 dc 48 6d 2c 8a 34 cb 33 fa 90 bf 1b 00 70 ad 3c + %% 49 88 83 c9 36 7c 09 a2 be 78 5a bc 55 cd 22 60 97 a3 a9 82 11 + %% 72 83 f8 2a 03 a1 43 ef d3 ff 5d d3 6d 64 e8 61 be 7f d6 1d 28 + %% 27 db 27 9c ce 14 50 77 d4 54 a3 66 4d 4e 6d a4 d2 9e e0 37 25 + %% a6 a4 da fc d0 fc 67 d2 ae a7 05 29 51 3e 3d a2 67 7f a5 90 6c + %% 5b 3f 7d 8f 92 f2 28 bd a4 0d da 72 14 70 f9 fb f2 97 b5 ae a6 + %% 17 64 6f ac 5c 03 27 2e 97 07 27 c6 21 a7 91 41 ef 5f 7d e6 50 + %% 5e 5b fb c3 88 e9 33 43 69 40 93 93 4a e4 d3 57 00 08 00 2a 00 + %% 04 00 00 04 00 + ResPRK = + hexstr2bin("7d f2 35 f2 03 1d 2a 05 12 87 d0 2b 02 41 b0 bf + da f8 6c c8 56 23 1f 2d 5a ba 46 c4 34 ec 19 6c"), + + _ResHash = hexstr2bin("00 00"), + + _ResInfo = hexstr2bin("00 20 10 74 6c 73 31 33 20 72 65 73 75 6d 70 74 + 69 6f 6e 02 00 00"), + + ResExpanded = + hexstr2bin("4e cd 0e b6 ec 3b 4d 87 f5 d6 02 8f 92 2c + a4 c5 85 1a 27 7f d4 13 11 c9 e6 2d 2c 94 92 e1 c4 f3"), + + NewSessionTicket = + hexstr2bin("04 00 00 c9 00 00 00 1e fa d6 aa + c5 02 00 00 00 b2 2c 03 5d 82 93 59 ee 5f f7 af 4e c9 00 00 00 + 00 26 2a 64 94 dc 48 6d 2c 8a 34 cb 33 fa 90 bf 1b 00 70 ad 3c + 49 88 83 c9 36 7c 09 a2 be 78 5a bc 55 cd 22 60 97 a3 a9 82 11 + 72 83 f8 2a 03 a1 43 ef d3 ff 5d d3 6d 64 e8 61 be 7f d6 1d 28 + 27 db 27 9c ce 14 50 77 d4 54 a3 66 4d 4e 6d a4 d2 9e e0 37 25 + a6 a4 da fc d0 fc 67 d2 ae a7 05 29 51 3e 3d a2 67 7f a5 90 6c + 5b 3f 7d 8f 92 f2 28 bd a4 0d da 72 14 70 f9 fb f2 97 b5 ae a6 + 17 64 6f ac 5c 03 27 2e 97 07 27 c6 21 a7 91 41 ef 5f 7d e6 50 + 5e 5b fb c3 88 e9 33 43 69 40 93 93 4a e4 d3 57 00 08 00 2a 00 + 04 00 00 04 00"), + <<?BYTE(NWT), ?UINT24(_), TicketBody/binary>> = NewSessionTicket, + #new_session_ticket{ + ticket_lifetime = _LifeTime, + ticket_age_add = _AgeAdd, + ticket_nonce = Nonce, + ticket = Ticket, + extensions = _Extensions + } = tls_handshake:decode_handshake({3,4}, NWT, TicketBody), + + %% ResPRK = resumption master secret + ResExpanded = tls_v1:pre_shared_key(ResPRK, Nonce, HKDFAlgo), + + %% {client} create an ephemeral x25519 key pair: + %% + %% private key (32 octets): bf f9 11 88 28 38 46 dd 6a 21 34 ef 71 + %% 80 ca 2b 0b 14 fb 10 dc e7 07 b5 09 8c 0d dd c8 13 b2 df + %% + %% public key (32 octets): e4 ff b6 8a c0 5f 8d 96 c9 9d a2 66 98 34 + %% 6c 6b e1 64 82 ba dd da fe 05 1a 66 b4 f1 8d 66 8f 0b + %% + %% {client} extract secret "early": + %% + %% salt: 0 (all zero octets) + %% + %% IKM (32 octets): 4e cd 0e b6 ec 3b 4d 87 f5 d6 02 8f 92 2c a4 c5 + %% 85 1a 27 7f d4 13 11 c9 e6 2d 2c 94 92 e1 c4 f3 + %% + %% secret (32 octets): 9b 21 88 e9 b2 fc 6d 64 d7 1d c3 29 90 0e 20 + %% bb 41 91 50 00 f6 78 aa 83 9c bb 79 7c b7 d8 33 2c + %% + + PSK = hexstr2bin("4e cd 0e b6 ec 3b 4d 87 f5 d6 02 8f 92 2c a4 c5 + 85 1a 27 7f d4 13 11 c9 e6 2d 2c 94 92 e1 c4 f3"), + PSK = ResExpanded, + EarlySecret = hexstr2bin("9b 21 88 e9 b2 fc 6d 64 d7 1d c3 29 90 0e 20 + bb 41 91 50 00 f6 78 aa 83 9c bb 79 7c b7 d8 33 2c"), + + {early_secret, EarlySecret} = tls_v1:key_schedule(early_secret, HKDFAlgo, {psk, PSK}), + + %% {client} construct a ClientHello handshake message: + %% + %% ClientHello (477 octets): 01 00 01 fc 03 03 1b c3 ce b6 bb e3 9c + %% ff 93 83 55 b5 a5 0a db 6d b2 1b 7a 6a f6 49 d7 b4 bc 41 9d 78 + %% 76 48 7d 95 00 00 06 13 01 13 03 13 02 01 00 01 cd 00 00 00 0b + %% 00 09 00 00 06 73 65 72 76 65 72 ff 01 00 01 00 00 0a 00 14 00 + %% 12 00 1d 00 17 00 18 00 19 01 00 01 01 01 02 01 03 01 04 00 33 + %% 00 26 00 24 00 1d 00 20 e4 ff b6 8a c0 5f 8d 96 c9 9d a2 66 98 + %% 34 6c 6b e1 64 82 ba dd da fe 05 1a 66 b4 f1 8d 66 8f 0b 00 2a + %% 00 00 00 2b 00 03 02 03 04 00 0d 00 20 00 1e 04 03 05 03 06 03 + %% 02 03 08 04 08 05 08 06 04 01 05 01 06 01 02 01 04 02 05 02 06 + %% 02 02 02 00 2d 00 02 01 01 00 1c 00 02 40 01 00 15 00 57 00 00 + %% 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + %% 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + %% 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + %% 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + %% 00 00 29 00 dd 00 b8 00 b2 2c 03 5d 82 93 59 ee 5f f7 af 4e c9 + %% 00 00 00 00 26 2a 64 94 dc 48 6d 2c 8a 34 cb 33 fa 90 bf 1b 00 + %% 70 ad 3c 49 88 83 c9 36 7c 09 a2 be 78 5a bc 55 cd 22 60 97 a3 + %% a9 82 11 72 83 f8 2a 03 a1 43 ef d3 ff 5d d3 6d 64 e8 61 be 7f + %% d6 1d 28 27 db 27 9c ce 14 50 77 d4 54 a3 66 4d 4e 6d a4 d2 9e + %% e0 37 25 a6 a4 da fc d0 fc 67 d2 ae a7 05 29 51 3e 3d a2 67 7f + %% a5 90 6c 5b 3f 7d 8f 92 f2 28 bd a4 0d da 72 14 70 f9 fb f2 97 + %% b5 ae a6 17 64 6f ac 5c 03 27 2e 97 07 27 c6 21 a7 91 41 ef 5f + %% 7d e6 50 5e 5b fb c3 88 e9 33 43 69 40 93 93 4a e4 d3 57 fa d6 + %% aa cb + %% + ClientHello = + hexstr2bin("01 00 01 fc 03 03 1b c3 ce b6 bb e3 9c + ff 93 83 55 b5 a5 0a db 6d b2 1b 7a 6a f6 49 d7 b4 bc 41 9d 78 + 76 48 7d 95 00 00 06 13 01 13 03 13 02 01 00 01 cd 00 00 00 0b + 00 09 00 00 06 73 65 72 76 65 72 ff 01 00 01 00 00 0a 00 14 00 + 12 00 1d 00 17 00 18 00 19 01 00 01 01 01 02 01 03 01 04 00 33 + 00 26 00 24 00 1d 00 20 e4 ff b6 8a c0 5f 8d 96 c9 9d a2 66 98 + 34 6c 6b e1 64 82 ba dd da fe 05 1a 66 b4 f1 8d 66 8f 0b 00 2a + 00 00 00 2b 00 03 02 03 04 00 0d 00 20 00 1e 04 03 05 03 06 03 + 02 03 08 04 08 05 08 06 04 01 05 01 06 01 02 01 04 02 05 02 06 + 02 02 02 00 2d 00 02 01 01 00 1c 00 02 40 01 00 15 00 57 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 29 00 dd 00 b8 00 b2 2c 03 5d 82 93 59 ee 5f f7 af 4e c9 + 00 00 00 00 26 2a 64 94 dc 48 6d 2c 8a 34 cb 33 fa 90 bf 1b 00 + 70 ad 3c 49 88 83 c9 36 7c 09 a2 be 78 5a bc 55 cd 22 60 97 a3 + a9 82 11 72 83 f8 2a 03 a1 43 ef d3 ff 5d d3 6d 64 e8 61 be 7f + d6 1d 28 27 db 27 9c ce 14 50 77 d4 54 a3 66 4d 4e 6d a4 d2 9e + e0 37 25 a6 a4 da fc d0 fc 67 d2 ae a7 05 29 51 3e 3d a2 67 7f + a5 90 6c 5b 3f 7d 8f 92 f2 28 bd a4 0d da 72 14 70 f9 fb f2 97 + b5 ae a6 17 64 6f ac 5c 03 27 2e 97 07 27 c6 21 a7 91 41 ef 5f + 7d e6 50 5e 5b fb c3 88 e9 33 43 69 40 93 93 4a e4 d3 57 fa d6 + aa cb"), + + %% {client} calculate PSK binder: + %% + %% ClientHello prefix (477 octets): 01 00 01 fc 03 03 1b c3 ce b6 bb + %% e3 9c ff 93 83 55 b5 a5 0a db 6d b2 1b 7a 6a f6 49 d7 b4 bc 41 + %% 9d 78 76 48 7d 95 00 00 06 13 01 13 03 13 02 01 00 01 cd 00 00 + %% 00 0b 00 09 00 00 06 73 65 72 76 65 72 ff 01 00 01 00 00 0a 00 + %% 14 00 12 00 1d 00 17 00 18 00 19 01 00 01 01 01 02 01 03 01 04 + %% 00 33 00 26 00 24 00 1d 00 20 e4 ff b6 8a c0 5f 8d 96 c9 9d a2 + %% 66 98 34 6c 6b e1 64 82 ba dd da fe 05 1a 66 b4 f1 8d 66 8f 0b + %% 00 2a 00 00 00 2b 00 03 02 03 04 00 0d 00 20 00 1e 04 03 05 03 + %% 06 03 02 03 08 04 08 05 08 06 04 01 05 01 06 01 02 01 04 02 05 + %% 02 06 02 02 02 00 2d 00 02 01 01 00 1c 00 02 40 01 00 15 00 57 + %% 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + %% 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + %% 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + %% 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + %% 00 00 00 00 29 00 dd 00 b8 00 b2 2c 03 5d 82 93 59 ee 5f f7 af + %% 4e c9 00 00 00 00 26 2a 64 94 dc 48 6d 2c 8a 34 cb 33 fa 90 bf + %% 1b 00 70 ad 3c 49 88 83 c9 36 7c 09 a2 be 78 5a bc 55 cd 22 60 + %% 97 a3 a9 82 11 72 83 f8 2a 03 a1 43 ef d3 ff 5d d3 6d 64 e8 61 + %% be 7f d6 1d 28 27 db 27 9c ce 14 50 77 d4 54 a3 66 4d 4e 6d a4 + %% d2 9e e0 37 25 a6 a4 da fc d0 fc 67 d2 ae a7 05 29 51 3e 3d a2 + %% 67 7f a5 90 6c 5b 3f 7d 8f 92 f2 28 bd a4 0d da 72 14 70 f9 fb + %% f2 97 b5 ae a6 17 64 6f ac 5c 03 27 2e 97 07 27 c6 21 a7 91 41 + %% ef 5f 7d e6 50 5e 5b fb c3 88 e9 33 43 69 40 93 93 4a e4 d3 57 + %% fa d6 aa cb + %% + %% binder hash (32 octets): 63 22 4b 2e 45 73 f2 d3 45 4c a8 4b 9d + %% 00 9a 04 f6 be 9e 05 71 1a 83 96 47 3a ef a0 1e 92 4a 14 + %% + %% PRK (32 octets): 69 fe 13 1a 3b ba d5 d6 3c 64 ee bc c3 0e 39 5b + %% 9d 81 07 72 6a 13 d0 74 e3 89 db c8 a4 e4 72 56 + %% + %% hash (0 octets): (empty) + %% + %% info (18 octets): 00 20 0e 74 6c 73 31 33 20 66 69 6e 69 73 68 65 + %% 64 00 + %% + %% expanded (32 octets): 55 88 67 3e 72 cb 59 c8 7d 22 0c af fe 94 + %% f2 de a9 a3 b1 60 9f 7d 50 e9 0a 48 22 7d b9 ed 7e aa + %% + %% finished (32 octets): 3a dd 4f b2 d8 fd f8 22 a0 ca 3c f7 67 8e + %% f5 e8 8d ae 99 01 41 c5 92 4d 57 bb 6f a3 1b 9e 5f 9d + BinderHash = + hexstr2bin("63 22 4b 2e 45 73 f2 d3 45 4c a8 4b 9d + 00 9a 04 f6 be 9e 05 71 1a 83 96 47 3a ef a0 1e 92 4a 14"), + + %% Part of derive_secret/4 + BinderHash = crypto:hash(HKDFAlgo, [ClientHello]), + + PRK = hexstr2bin("69 fe 13 1a 3b ba d5 d6 3c 64 ee bc c3 0e 39 5b + 9d 81 07 72 6a 13 d0 74 e3 89 db c8 a4 e4 72 56"), + + PRK = tls_v1:resumption_binder_key(HKDFAlgo, {early_secret, EarlySecret}), + + Expanded = + hexstr2bin("55 88 67 3e 72 cb 59 c8 7d 22 0c af fe 94 + f2 de a9 a3 b1 60 9f 7d 50 e9 0a 48 22 7d b9 ed 7e aa"), + + Expanded = tls_v1:finished_key(PRK, HKDFAlgo), + + Finished = hexstr2bin("3a dd 4f b2 d8 fd f8 22 a0 ca 3c f7 67 8e + f5 e8 8d ae 99 01 41 c5 92 4d 57 bb 6f a3 1b 9e 5f 9d"), + + Finished = tls_v1:finished_verify_data(Expanded, HKDFAlgo, [ClientHello]), + + %% {client} send handshake record: + %% + %% payload (512 octets): 01 00 01 fc 03 03 1b c3 ce b6 bb e3 9c ff + %% 93 83 55 b5 a5 0a db 6d b2 1b 7a 6a f6 49 d7 b4 bc 41 9d 78 76 + %% 48 7d 95 00 00 06 13 01 13 03 13 02 01 00 01 cd 00 00 00 0b 00 + %% 09 00 00 06 73 65 72 76 65 72 ff 01 00 01 00 00 0a 00 14 00 12 + %% 00 1d 00 17 00 18 00 19 01 00 01 01 01 02 01 03 01 04 00 33 00 + %% 26 00 24 00 1d 00 20 e4 ff b6 8a c0 5f 8d 96 c9 9d a2 66 98 34 + %% 6c 6b e1 64 82 ba dd da fe 05 1a 66 b4 f1 8d 66 8f 0b 00 2a 00 + %% 00 00 2b 00 03 02 03 04 00 0d 00 20 00 1e 04 03 05 03 06 03 02 + %% 03 08 04 08 05 08 06 04 01 05 01 06 01 02 01 04 02 05 02 06 02 + %% 02 02 00 2d 00 02 01 01 00 1c 00 02 40 01 00 15 00 57 00 00 00 + %% 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + %% 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + %% 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + %% 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + %% 00 29 00 dd 00 b8 00 b2 2c 03 5d 82 93 59 ee 5f f7 af 4e c9 00 + %% 00 00 00 26 2a 64 94 dc 48 6d 2c 8a 34 cb 33 fa 90 bf 1b 00 70 + %% ad 3c 49 88 83 c9 36 7c 09 a2 be 78 5a bc 55 cd 22 60 97 a3 a9 + %% 82 11 72 83 f8 2a 03 a1 43 ef d3 ff 5d d3 6d 64 e8 61 be 7f d6 + %% 1d 28 27 db 27 9c ce 14 50 77 d4 54 a3 66 4d 4e 6d a4 d2 9e e0 + %% 37 25 a6 a4 da fc d0 fc 67 d2 ae a7 05 29 51 3e 3d a2 67 7f a5 + %% 90 6c 5b 3f 7d 8f 92 f2 28 bd a4 0d da 72 14 70 f9 fb f2 97 b5 + %% ae a6 17 64 6f ac 5c 03 27 2e 97 07 27 c6 21 a7 91 41 ef 5f 7d + %% e6 50 5e 5b fb c3 88 e9 33 43 69 40 93 93 4a e4 d3 57 fa d6 aa + %% cb 00 21 20 3a dd 4f b2 d8 fd f8 22 a0 ca 3c f7 67 8e f5 e8 8d + %% ae 99 01 41 c5 92 4d 57 bb 6f a3 1b 9e 5f 9d + %% + %% complete record (517 octets): 16 03 01 02 00 01 00 01 fc 03 03 1b + %% c3 ce b6 bb e3 9c ff 93 83 55 b5 a5 0a db 6d b2 1b 7a 6a f6 49 + %% d7 b4 bc 41 9d 78 76 48 7d 95 00 00 06 13 01 13 03 13 02 01 00 + %% 01 cd 00 00 00 0b 00 09 00 00 06 73 65 72 76 65 72 ff 01 00 01 + %% 00 00 0a 00 14 00 12 00 1d 00 17 00 18 00 19 01 00 01 01 01 02 + %% 01 03 01 04 00 33 00 26 00 24 00 1d 00 20 e4 ff b6 8a c0 5f 8d + %% 96 c9 9d a2 66 98 34 6c 6b e1 64 82 ba dd da fe 05 1a 66 b4 f1 + %% 8d 66 8f 0b 00 2a 00 00 00 2b 00 03 02 03 04 00 0d 00 20 00 1e + %% 04 03 05 03 06 03 02 03 08 04 08 05 08 06 04 01 05 01 06 01 02 + %% 01 04 02 05 02 06 02 02 02 00 2d 00 02 01 01 00 1c 00 02 40 01 + %% 00 15 00 57 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + %% 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + %% 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + %% 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + %% 00 00 00 00 00 00 00 00 29 00 dd 00 b8 00 b2 2c 03 5d 82 93 59 + %% ee 5f f7 af 4e c9 00 00 00 00 26 2a 64 94 dc 48 6d 2c 8a 34 cb + %% 33 fa 90 bf 1b 00 70 ad 3c 49 88 83 c9 36 7c 09 a2 be 78 5a bc + %% 55 cd 22 60 97 a3 a9 82 11 72 83 f8 2a 03 a1 43 ef d3 ff 5d d3 + %% 6d 64 e8 61 be 7f d6 1d 28 27 db 27 9c ce 14 50 77 d4 54 a3 66 + %% 4d 4e 6d a4 d2 9e e0 37 25 a6 a4 da fc d0 fc 67 d2 ae a7 05 29 + %% 51 3e 3d a2 67 7f a5 90 6c 5b 3f 7d 8f 92 f2 28 bd a4 0d da 72 + %% 14 70 f9 fb f2 97 b5 ae a6 17 64 6f ac 5c 03 27 2e 97 07 27 c6 + %% 21 a7 91 41 ef 5f 7d e6 50 5e 5b fb c3 88 e9 33 43 69 40 93 93 + %% 4a e4 d3 57 fa d6 aa cb 00 21 20 3a dd 4f b2 d8 fd f8 22 a0 ca + %% 3c f7 67 8e f5 e8 8d ae 99 01 41 c5 92 4d 57 bb 6f a3 1b 9e 5f + %% 9d + ClientHelloRecord = + hexstr2bin("01 00 01 fc 03 03 1b c3 ce b6 bb e3 9c ff + 93 83 55 b5 a5 0a db 6d b2 1b 7a 6a f6 49 d7 b4 bc 41 9d 78 76 + 48 7d 95 00 00 06 13 01 13 03 13 02 01 00 01 cd 00 00 00 0b 00 + 09 00 00 06 73 65 72 76 65 72 ff 01 00 01 00 00 0a 00 14 00 12 + 00 1d 00 17 00 18 00 19 01 00 01 01 01 02 01 03 01 04 00 33 00 + 26 00 24 00 1d 00 20 e4 ff b6 8a c0 5f 8d 96 c9 9d a2 66 98 34 + 6c 6b e1 64 82 ba dd da fe 05 1a 66 b4 f1 8d 66 8f 0b 00 2a 00 + 00 00 2b 00 03 02 03 04 00 0d 00 20 00 1e 04 03 05 03 06 03 02 + 03 08 04 08 05 08 06 04 01 05 01 06 01 02 01 04 02 05 02 06 02 + 02 02 00 2d 00 02 01 01 00 1c 00 02 40 01 00 15 00 57 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 29 00 dd 00 b8 00 b2 2c 03 5d 82 93 59 ee 5f f7 af 4e c9 00 + 00 00 00 26 2a 64 94 dc 48 6d 2c 8a 34 cb 33 fa 90 bf 1b 00 70 + ad 3c 49 88 83 c9 36 7c 09 a2 be 78 5a bc 55 cd 22 60 97 a3 a9 + 82 11 72 83 f8 2a 03 a1 43 ef d3 ff 5d d3 6d 64 e8 61 be 7f d6 + 1d 28 27 db 27 9c ce 14 50 77 d4 54 a3 66 4d 4e 6d a4 d2 9e e0 + 37 25 a6 a4 da fc d0 fc 67 d2 ae a7 05 29 51 3e 3d a2 67 7f a5 + 90 6c 5b 3f 7d 8f 92 f2 28 bd a4 0d da 72 14 70 f9 fb f2 97 b5 + ae a6 17 64 6f ac 5c 03 27 2e 97 07 27 c6 21 a7 91 41 ef 5f 7d + e6 50 5e 5b fb c3 88 e9 33 43 69 40 93 93 4a e4 d3 57 fa d6 aa + cb 00 21 20 3a dd 4f b2 d8 fd f8 22 a0 ca 3c f7 67 8e f5 e8 8d + ae 99 01 41 c5 92 4d 57 bb 6f a3 1b 9e 5f 9d"), + + <<?BYTE(CH), ?UINT24(_Length), ClientHelloBody/binary>> = ClientHelloRecord, + #client_hello{extensions = #{pre_shared_key := PreSharedKey}} = + tls_handshake:decode_handshake({3,4}, CH, ClientHelloBody), + + #pre_shared_key_client_hello{ + offered_psks = #offered_psks{ + identities = [Identity], + binders = [_Binders]}} = PreSharedKey, + + #psk_identity{ + identity = Ticket} = Identity, + + ok. + %%-------------------------------------------------------------------- finished_verify_data() -> diff --git a/lib/ssl/test/tls_1_3_version_SUITE.erl b/lib/ssl/test/tls_1_3_version_SUITE.erl index f0b224d4e5..e0ac53e0f9 100644 --- a/lib/ssl/test/tls_1_3_version_SUITE.erl +++ b/lib/ssl/test/tls_1_3_version_SUITE.erl @@ -50,8 +50,14 @@ cert_groups() -> tests() -> [tls13_client_tls12_server, tls13_client_with_ext_tls12_server, - tls12_client_tls13_server]. - + tls12_client_tls13_server, + tls_client_tls10_server, + tls_client_tls11_server, + tls_client_tls12_server, + tls10_client_tls_server, + tls11_client_tls_server, + tls12_client_tls_server]. + init_per_suite(Config) -> catch crypto:stop(), try crypto:start() of @@ -150,4 +156,63 @@ tls12_client_tls13_server(Config) when is_list(Config) -> ServerOpts = [{versions, ['tlsv1.3', 'tlsv1.2']} | ssl_test_lib:ssl_options(server_cert_opts, Config)], ssl_test_lib:basic_test(ClientOpts, ServerOpts, Config). - + +tls_client_tls10_server() -> + [{doc,"Test that a TLS 1.0-1.3 client can connect to a TLS 1.0 server."}]. +tls_client_tls10_server(Config) when is_list(Config) -> + ClientOpts = [{versions, + ['tlsv1', 'tlsv1.1', 'tlsv1.2', 'tlsv1.3']} | + ssl_test_lib:ssl_options(client_cert_opts, Config)], + ServerOpts = [{versions, + ['tlsv1']} | ssl_test_lib:ssl_options(server_cert_opts, Config)], + ssl_test_lib:basic_test(ClientOpts, ServerOpts, Config). + +tls_client_tls11_server() -> + [{doc,"Test that a TLS 1.0-1.3 client can connect to a TLS 1.1 server."}]. +tls_client_tls11_server(Config) when is_list(Config) -> + ClientOpts = [{versions, + ['tlsv1', 'tlsv1.1', 'tlsv1.2', 'tlsv1.3']} | + ssl_test_lib:ssl_options(client_cert_opts, Config)], + ServerOpts = [{versions, + ['tlsv1.1']} | ssl_test_lib:ssl_options(server_cert_opts, Config)], + ssl_test_lib:basic_test(ClientOpts, ServerOpts, Config). + +tls_client_tls12_server() -> + [{doc,"Test that a TLS 1.0-1.3 client can connect to a TLS 1.2 server."}]. +tls_client_tls12_server(Config) when is_list(Config) -> + ClientOpts = [{versions, + ['tlsv1', 'tlsv1.1', 'tlsv1.2', 'tlsv1.3']} | + ssl_test_lib:ssl_options(client_cert_opts, Config)], + ServerOpts = [{versions, + ['tlsv1.2']} | ssl_test_lib:ssl_options(server_cert_opts, Config)], + ssl_test_lib:basic_test(ClientOpts, ServerOpts, Config). + +tls10_client_tls_server() -> + [{doc,"Test that a TLS 1.0 client can connect to a TLS 1.0-1.3 server."}]. +tls10_client_tls_server(Config) when is_list(Config) -> + ClientOpts = [{versions, + ['tlsv1']} | ssl_test_lib:ssl_options(client_cert_opts, Config)], + ServerOpts = [{versions, + ['tlsv1','tlsv1.1', 'tlsv1.2', 'tlsv1.3']} | + ssl_test_lib:ssl_options(server_cert_opts, Config)], + ssl_test_lib:basic_test(ClientOpts, ServerOpts, Config). + +tls11_client_tls_server() -> + [{doc,"Test that a TLS 1.1 client can connect to a TLS 1.0-1.3 server."}]. +tls11_client_tls_server(Config) when is_list(Config) -> + ClientOpts = [{versions, + ['tlsv1.1']} | ssl_test_lib:ssl_options(client_cert_opts, Config)], + ServerOpts = [{versions, + ['tlsv1','tlsv1.1', 'tlsv1.2', 'tlsv1.3']} | + ssl_test_lib:ssl_options(server_cert_opts, Config)], + ssl_test_lib:basic_test(ClientOpts, ServerOpts, Config). + +tls12_client_tls_server() -> + [{doc,"Test that a TLS 1.2 client can connect to a TLS 1.0-1.3 server."}]. +tls12_client_tls_server(Config) when is_list(Config) -> + ClientOpts = [{versions, + ['tlsv1.2']} | ssl_test_lib:ssl_options(client_cert_opts, Config)], + ServerOpts = [{versions, + ['tlsv1','tlsv1.1', 'tlsv1.2', 'tlsv1.3']} | + ssl_test_lib:ssl_options(server_cert_opts, Config)], + ssl_test_lib:basic_test(ClientOpts, ServerOpts, Config). |