File 4151-ssl-Convey-alert-information-to-passive-socket-opera.patch of Package erlang
From f9013fad5a1121535e882be9a3d4713637110ec7 Mon Sep 17 00:00:00 2001
From: Ingela Anderton Andin <ingela@erlang.org>
Date: Fri, 13 Sep 2024 14:22:06 +0200
Subject: [PATCH] ssl: Convey alert information to passive socket operations
recv and setopts
If a TLS-1.3 server fails client certification the alert might arrive
in the connection state and even after data has been sent. Make sure
the alert information will be available in error reason returned from
passive socket API functions recv and setopt.
Backport of 25f3c524809b6f2909d925205df2dc9532464c14
---
lib/ssl/src/ssl_connection.hrl | 3 +-
lib/ssl/src/ssl_gen_statem.erl | 62 ++++++++++++++----
lib/ssl/src/tls_gen_connection.erl | 7 ++
lib/ssl/test/tls_1_3_version_SUITE.erl | 90 +++++++++++++++++++++++++-
4 files changed, 146 insertions(+), 16 deletions(-)
diff --git a/lib/ssl/src/ssl_connection.hrl b/lib/ssl/src/ssl_connection.hrl
index c8295f339f..5efdccfdfd 100644
--- a/lib/ssl/src/ssl_connection.hrl
+++ b/lib/ssl/src/ssl_connection.hrl
@@ -31,6 +31,7 @@
-include("ssl_handshake.hrl").
-include("ssl_srp.hrl").
-include("ssl_cipher.hrl").
+-include("ssl_alert.hrl").
-include_lib("public_key/include/public_key.hrl").
-record(static_env, {
@@ -96,7 +97,7 @@
user_application :: {Monitor::reference(), User::pid()},
downgrade :: {NewController::pid(), From::gen_statem:from()} | 'undefined',
socket_terminated = false ::boolean(),
- socket_tls_closed = false ::boolean(),
+ socket_tls_closed = false ::boolean() | #alert{},
negotiated_version :: ssl_record:ssl_version() | 'undefined',
erl_dist_handle = undefined :: erlang:dist_handle() | 'undefined',
cert_key_alts = undefined :: #{eddsa => list(),
diff --git a/lib/ssl/src/ssl_gen_statem.erl b/lib/ssl/src/ssl_gen_statem.erl
index eed0025ad7..f795e7c24c 100644
--- a/lib/ssl/src/ssl_gen_statem.erl
+++ b/lib/ssl/src/ssl_gen_statem.erl
@@ -584,11 +584,10 @@ config_error(_Type, _Event, _State) ->
gen_statem:state_function_result().
%%--------------------------------------------------------------------
connection({call, RecvFrom}, {recv, N, Timeout},
- #state{static_env = #static_env{protocol_cb = Connection},
- socket_options =
+ #state{socket_options =
#socket_options{active = false}} = State0) ->
passive_receive(State0#state{bytes_to_read = N,
- start_or_recv_from = RecvFrom}, ?FUNCTION_NAME, Connection,
+ start_or_recv_from = RecvFrom}, ?FUNCTION_NAME,
[{{timeout, recv}, Timeout, timeout}]);
connection({call, From}, peer_certificate,
#state{session = #session{peer_certificate = Cert}} = State) ->
@@ -613,6 +612,18 @@ connection({call, From}, negotiated_protocol,
negotiated_protocol = undefined}} = State) ->
hibernate_after(?FUNCTION_NAME, State,
[{reply, From, {ok, SelectedProtocol}}]);
+connection({call, From},
+ {close,{_NewController, _Timeout}},
+ #state{static_env = #static_env{role = Role,
+ socket = Socket,
+ trackers = Trackers,
+ transport_cb = Transport,
+ protocol_cb = Connection},
+ connection_env = #connection_env{socket_tls_closed = #alert{} = Alert}
+ } = State) ->
+ Pids = Connection:pids(State),
+ alert_user(Pids, Transport, Trackers, Socket, From, Alert, Role, connection, Connection),
+ {stop, {shutdown, normal}, State};
connection({call, From},
{close,{NewController, Timeout}},
#state{connection_states = ConnectionStates,
@@ -663,9 +674,8 @@ connection(cast, {dist_handshake_complete, DHandle},
Connection:next_event(connection, Record, State);
connection(info, Msg, #state{static_env = #static_env{protocol_cb = Connection}} = State) ->
Connection:handle_info(Msg, ?FUNCTION_NAME, State);
-connection(internal, {recv, RecvFrom}, #state{start_or_recv_from = RecvFrom,
- static_env = #static_env{protocol_cb = Connection}} = State) ->
- passive_receive(State, ?FUNCTION_NAME, Connection, []);
+connection(internal, {recv, RecvFrom}, #state{start_or_recv_from = RecvFrom} = State) ->
+ passive_receive(State, ?FUNCTION_NAME, []);
connection(Type, Msg, State) ->
handle_common_event(Type, Msg, ?FUNCTION_NAME, State).
@@ -844,7 +854,7 @@ handle_info({ErrorTag, Socket, econnaborted}, StateName,
maybe_invalidate_session(Version, Type, Role, Host, Port, Session),
Pids = Connection:pids(State),
- alert_user(Pids, Transport, Trackers,Socket,
+ alert_user(Pids, Transport, Trackers, Socket,
StartFrom, ?ALERT_REC(?FATAL, ?CLOSE_NOTIFY), Role, StateName, Connection),
{stop, {shutdown, normal}, State};
@@ -925,10 +935,23 @@ read_application_data(Data,
user_data_buffer = {Front,BufferSize,Rear}}}
end
end.
+
+passive_receive(#state{static_env = #static_env{role = Role,
+ socket = Socket,
+ trackers = Trackers,
+ transport_cb = Transport,
+ protocol_cb = Connection},
+ start_or_recv_from = RecvFrom,
+ connection_env = #connection_env{socket_tls_closed = #alert{} = Alert}} = State,
+ StateName, _) ->
+ Pids = Connection:pids(State),
+ alert_user(Pids, Transport, Trackers, Socket, RecvFrom, Alert, Role, StateName, Connection),
+ {stop, {shutdown, normal}, State};
passive_receive(#state{user_data_buffer = {Front,BufferSize,Rear},
%% Assert! Erl distribution uses active sockets
+ static_env = #static_env{protocol_cb = Connection},
connection_env = #connection_env{erl_dist_handle = undefined}}
- = State0, StateName, Connection, StartTimerAction) ->
+ = State0, StateName, StartTimerAction) ->
case BufferSize of
0 ->
Connection:next_event(StateName, no_record, State0, StartTimerAction);
@@ -1396,14 +1419,27 @@ no_records(Extensions) ->
handle_active_option(false, connection = StateName, To, Reply, State) ->
hibernate_after(StateName, State, [{reply, To, Reply}]);
-handle_active_option(_, connection = StateName, To, Reply, #state{static_env = #static_env{role = Role},
- connection_env = #connection_env{socket_tls_closed = true},
- user_data_buffer = {_,0,_}} = State) ->
+handle_active_option(_, connection = StateName, To, Reply,
+ #state{static_env = #static_env{role = Role},
+ connection_env = #connection_env{socket_tls_closed = true},
+ user_data_buffer = {_,0,_}} = State) ->
Alert = ?ALERT_REC(?FATAL, ?CLOSE_NOTIFY, all_data_delivered),
handle_normal_shutdown(Alert#alert{role = Role}, StateName, State),
{stop_and_reply,{shutdown, peer_close}, [{reply, To, Reply}]};
-handle_active_option(_, connection = StateName0, To, Reply, #state{static_env = #static_env{protocol_cb = Connection},
- user_data_buffer = {_,0,_}} = State0) ->
+handle_active_option(_, connection = StateName, To, _Reply,
+ #state{static_env = #static_env{role = Role,
+ socket = Socket,
+ trackers = Trackers,
+ transport_cb = Transport,
+ protocol_cb = Connection},
+ connection_env = #connection_env{socket_tls_closed = Alert = #alert{}},
+ user_data_buffer = {_,0,_}} = State) ->
+ Pids = Connection:pids(State),
+ alert_user(Pids, Transport, Trackers, Socket, To, Alert, Role, StateName, Connection),
+ {stop, {shutdown, normal}, State};
+handle_active_option(_, connection = StateName0, To, Reply,
+ #state{static_env = #static_env{protocol_cb = Connection},
+ user_data_buffer = {_,0,_}} = State0) ->
case Connection:next_event(StateName0, no_record, State0) of
{next_state, StateName, State} ->
hibernate_after(StateName, State, [{reply, To, Reply}]);
diff --git a/lib/ssl/src/tls_gen_connection.erl b/lib/ssl/src/tls_gen_connection.erl
index 940666f104..bfdc8a4f2f 100644
--- a/lib/ssl/src/tls_gen_connection.erl
+++ b/lib/ssl/src/tls_gen_connection.erl
@@ -872,7 +872,14 @@ handle_alerts([#alert{level = ?WARNING, description = ?CLOSE_NOTIFY} | _Alerts],
{next_state, connection = StateName, #state{connection_env = CEnv,
socket_options = #socket_options{active = false},
start_or_recv_from = From} = State}) when From == undefined ->
+ %% Linger to allow recv and setopts to possibly fetch data not yet delivered to user to be fetched
{next_state, StateName, State#state{connection_env = CEnv#connection_env{socket_tls_closed = true}}};
+handle_alerts([#alert{level = ?FATAL} = Alert | _Alerts],
+ {next_state, connection = StateName, #state{connection_env = CEnv,
+ socket_options = #socket_options{active = false},
+ start_or_recv_from = From} = State}) when From == undefined ->
+ %% Linger to allow recv and setopts to retrieve alert reason
+ {next_state, StateName, State#state{connection_env = CEnv#connection_env{socket_tls_closed = Alert}}};
handle_alerts([Alert | Alerts], {next_state, StateName, State}) ->
handle_alerts(Alerts, ssl_gen_statem:handle_alert(Alert, StateName, State));
handle_alerts([Alert | Alerts], {next_state, StateName, State, _Actions}) ->
diff --git a/lib/ssl/test/tls_1_3_version_SUITE.erl b/lib/ssl/test/tls_1_3_version_SUITE.erl
index 8a3ff288f7..5b6b40305f 100644
--- a/lib/ssl/test/tls_1_3_version_SUITE.erl
+++ b/lib/ssl/test/tls_1_3_version_SUITE.erl
@@ -57,7 +57,11 @@
middle_box_client_tls_v2_session_reused/0,
middle_box_client_tls_v2_session_reused/1,
renegotiate_error/0,
- renegotiate_error/1
+ renegotiate_error/1,
+ client_cert_fail_alert_active/0,
+ client_cert_fail_alert_active/1,
+ client_cert_fail_alert_passive/0,
+ client_cert_fail_alert_passive/1
]).
@@ -90,7 +94,9 @@ tls_1_3_1_2_tests() ->
middle_box_tls13_client,
middle_box_tls12_enabled_client,
middle_box_client_tls_v2_session_reused,
- renegotiate_error
+ renegotiate_error,
+ client_cert_fail_alert_active,
+ client_cert_fail_alert_passive
].
legacy_tests() ->
[tls_client_tls10_server,
@@ -329,6 +335,60 @@ renegotiate_error(Config) when is_list(Config) ->
ct:fail(Reason)
end.
+
+client_cert_fail_alert_active() ->
+ [{doc, "Check that we receive alert message"}].
+client_cert_fail_alert_active(Config) when is_list(Config) ->
+ ssl:clear_pem_cache(),
+ {_ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config),
+ ClientOpts0 = ssl_test_lib:ssl_options(extra_client, client_cert_opts, Config),
+ ServerOpts0 = ssl_test_lib:ssl_options(extra_server, server_cert_opts, Config),
+ PrivDir = proplists:get_value(priv_dir, Config),
+ NewClientCertFile = filename:join(PrivDir, "client_invalid_cert.pem"),
+
+ create_bad_client_certfile(NewClientCertFile, ClientOpts0),
+
+ ClientOpts = [{active, true},
+ {verify, verify_peer},
+ {certfile, NewClientCertFile} | proplists:delete(certfile, ClientOpts0)],
+ ServerOpts = [{verify, verify_peer}, {fail_if_no_peer_cert, true}| ServerOpts0],
+ Server = ssl_test_lib:start_server([{node, ServerNode}, {port, 0},
+ {from, self()},
+ {mfa, {ssl_test_lib, no_result, []}},
+ {options, ServerOpts}]),
+ Port = ssl_test_lib:inet_port(Server),
+ {ok, Socket} = ssl:connect(Hostname, Port, ClientOpts),
+ receive
+ {Server, {error, {tls_alert, {unknown_ca, _}}}} ->
+ receive
+ {ssl_error, Socket, {tls_alert, {unknown_ca, _}}} ->
+ ok
+ after 500 ->
+ ct:fail(no_acticv_msg)
+ end
+ end.
+
+client_cert_fail_alert_passive() ->
+ [{doc, "Check that recv or setopts return alert"}].
+client_cert_fail_alert_passive(Config) when is_list(Config) ->
+ ssl:clear_pem_cache(),
+ {_, ServerNode, Hostname} = ssl_test_lib:run_where(Config),
+ ClientOpts0 = ssl_test_lib:ssl_options(extra_client, client_cert_opts, Config),
+ ServerOpts0 = ssl_test_lib:ssl_options(extra_server, server_cert_opts, Config),
+ PrivDir = proplists:get_value(priv_dir, Config),
+ NewClientCertFile = filename:join(PrivDir, "client_invalid_cert.pem"),
+
+ create_bad_client_certfile(NewClientCertFile, ClientOpts0),
+
+ ClientOpts = [{active, false},
+ {verify, verify_peer},
+ {certfile, NewClientCertFile} | proplists:delete(certfile, ClientOpts0)],
+ ServerOpts = [{verify, verify_peer}, {fail_if_no_peer_cert, true}| ServerOpts0],
+ alert_passive(ServerOpts, ClientOpts, recv,
+ ServerNode, Hostname),
+ alert_passive(ServerOpts, ClientOpts, setopts,
+ ServerNode, Hostname).
+
tls13_client_tls11_server() ->
[{doc,"Test that a TLS 1.3 client gets old server alert from TLS 1.0 server."}].
tls13_client_tls11_server(Config) when is_list(Config) ->
@@ -359,3 +419,31 @@ check_session_id(Socket, not_empty) ->
_ ->
ok
end.
+
+alert_passive(ServerOpts, ClientOpts, Function,
+ ServerNode, Hostname) ->
+ Server = ssl_test_lib:start_server([{node, ServerNode}, {port, 0},
+ {from, self()},
+ {mfa, {ssl_test_lib, no_result, []}},
+ {options, ServerOpts}]),
+ Port = ssl_test_lib:inet_port(Server),
+ {ok, Socket} = ssl:connect(Hostname, Port, ClientOpts),
+ ct:sleep(500),
+ case Function of
+ recv ->
+ {error, {tls_alert, {unknown_ca,_}}} = ssl:recv(Socket, 0);
+ setopts ->
+ {error, {tls_alert, {unknown_ca,_}}} = ssl:setopts(Socket, [{active, once}])
+ end.
+
+create_bad_client_certfile(NewClientCertFile, ClientOpts0) ->
+ KeyFile = proplists:get_value(keyfile, ClientOpts0),
+ [KeyEntry] = ssl_test_lib:pem_to_der(KeyFile),
+ Key = ssl_test_lib:public_key(public_key:pem_entry_decode(KeyEntry)),
+ ClientCertFile = proplists:get_value(certfile, ClientOpts0),
+
+ [{'Certificate', ClientDerCert, _}] = ssl_test_lib:pem_to_der(ClientCertFile),
+ ClientOTPCert = public_key:pkix_decode_cert(ClientDerCert, otp),
+ ClientOTPTbsCert = ClientOTPCert#'OTPCertificate'.tbsCertificate,
+ NewClientDerCert = public_key:pkix_sign(ClientOTPTbsCert, Key),
+ ssl_test_lib:der_to_pem(NewClientCertFile, [{'Certificate', NewClientDerCert, not_encrypted}]).
--
2.43.0