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

openSUSE Build Service is sponsored by