File 0182-ssl-Correct-TLS-1-3-session-ticket-handling.patch of Package erlang

From 5c9e217df875e1583392f3668ea7033e6e577ce9 Mon Sep 17 00:00:00 2001
From: Ingela Anderton Andin <ingela@erlang.org>
Date: Fri, 12 Dec 2025 17:26:10 +0100
Subject: [PATCH] ssl: Correct TLS-1-3 session ticket handling

A documentation and option handling issue.

Closes #10464
---
 lib/ssl/doc/guides/using_ssl.md           |  2 +-
 lib/ssl/src/ssl.erl                       | 19 ++++++---
 lib/ssl/src/ssl_config.erl                | 50 ++++++++++++++++-------
 lib/ssl/test/ssl_api_SUITE.erl            | 31 +++++++++-----
 lib/ssl/test/ssl_session_ticket_SUITE.erl | 10 +++--
 5 files changed, 76 insertions(+), 36 deletions(-)

diff --git a/lib/ssl/doc/guides/using_ssl.md b/lib/ssl/doc/guides/using_ssl.md
index 112fdcab3b..40feda9219 100644
--- a/lib/ssl/doc/guides/using_ssl.md
+++ b/lib/ssl/doc/guides/using_ssl.md
@@ -727,7 +727,7 @@ tickets sent by the server.
 _Step 10 (client):_ Receive a new session ticket:
 
 ```erlang
-      Ticket = receive {ssl, session_ticket, {_, TicketData}} -> TicketData end.
+      Ticket = receive {ssl, session_ticket, Ticket0} -> Ticket0 end.
 ```
 
 _Step 11 (server):_ Accept a new connection on the server:
diff --git a/lib/ssl/src/ssl.erl b/lib/ssl/src/ssl.erl
index d0840815f4..6e025b55a8 100644
--- a/lib/ssl/src/ssl.erl
+++ b/lib/ssl/src/ssl.erl
@@ -162,11 +162,12 @@ Special Erlang node configuration for the application can be found in
               key/0,
               named_curve/0,
               old_cipher_suite/0,
-              prf_random/0, 
+              prf_random/0,
               protocol_extensions/0,
               protocol_version/0,
               reason/0,
               session_id/0,
+              session_ticket/0,
               sign_algo/0,
               sign_scheme/0,
               signature_algs/0,
@@ -1533,6 +1534,11 @@ different semantics for the client and server.
                               {customize_hostname_check, HostNameCheckOpts::list()} |
                               {certificate_authorities, boolean()} |
                               {stapling, Stapling:: staple | no_staple | map()}.
+-doc(#{group => <<"Client Options">>}).
+-doc """
+
+""".
+-nominal session_ticket() :: #{sni := inet:hostname()}.
 
 -doc(#{group => <<"Client Options">>}).
 -doc """
@@ -1545,11 +1551,14 @@ Options only relevant for TLS-1.3.
   information to user process in a 3-tuple:
 
   ```erlang
-  {ssl, session_ticket, {SNI, TicketData}}
+  {ssl, session_ticket, `Ticket::`(`t:session_ticket/0`)}
   ```
 
-  where `SNI` is the ServerNameIndication and `TicketData` is the extended ticket
-  data that can be used in subsequent session resumptions.
+  where `Ticket` is a map with information about the created TLS-1.3 session ticket.
+  The only key that the user needs to consider is `sni` key to be able to
+  provide it as a value in the use_ticket option list of possible tickets
+  to use it to attempt session resumption to a server identified by the
+  server name indication in `manual` session ticket mode.
 
   If it is set to `auto`, the client automatically handles received tickets and
   tries to use them when making new TLS connections (session resumption with
@@ -1606,7 +1615,7 @@ Options only relevant for TLS-1.3.
 """.
 -type client_option_tls13() ::
         {session_tickets, SessionTickets:: disabled | manual | auto} |
-        {use_ticket, Tickets::[binary()]} |
+        {use_ticket, Tickets::[session_ticket()]} |
         {early_data, binary()} |
         {middlebox_comp_mode, MiddleBoxMode::boolean()}.
 
diff --git a/lib/ssl/src/ssl_config.erl b/lib/ssl/src/ssl_config.erl
index 41d13f7080..27c623df60 100644
--- a/lib/ssl/src/ssl_config.erl
+++ b/lib/ssl/src/ssl_config.erl
@@ -590,9 +590,9 @@ process_options(UserSslOpts, SslOpts0, Env) ->
     SslOpts1  = opt_protocol_versions(UserSslOptsMap, SslOpts0, Env),
     SslOpts2  = opt_verification(UserSslOptsMap, SslOpts1, Env),
     SslOpts3  = opt_certs(UserSslOptsMap, SslOpts2, Env),
-    SslOpts4  = opt_tickets(UserSslOptsMap, SslOpts3, Env),
-    SslOpts5  = opt_stapling(UserSslOptsMap, SslOpts4, Env),
-    SslOpts6  = opt_sni(UserSslOptsMap, SslOpts5, Env),
+    SslOpts4  = opt_sni(UserSslOptsMap, SslOpts3, Env),
+    SslOpts5  = opt_tickets(UserSslOptsMap, SslOpts4, Env),
+    SslOpts6  = opt_stapling(UserSslOptsMap, SslOpts5, Env),
     SslOpts7  = opt_signature_algs(UserSslOptsMap, SslOpts6, Env),
     SslOpts8  = opt_alpn(UserSslOptsMap, SslOpts7, Env),
     SslOpts9  = opt_mitigation(UserSslOptsMap, SslOpts8, Env),
@@ -942,7 +942,8 @@ opt_cacerts(UserOpts, #{verify := Verify, log_level := LogLevel, versions := Ver
                      {new, FileName} -> unambiguous_path(FileName);
                      {_, FileName} -> FileName
                  end,
-    option_incompatible(CaCertFile =:= <<>> andalso CaCerts =:= undefined andalso Verify =:= verify_peer,
+    option_incompatible(CaCertFile =:= <<>> andalso CaCerts =:= undefined andalso
+                        Verify =:= verify_peer,
                         [{verify, verify_peer}, {cacerts, undefined}]),
 
     {Where2, CA} = get_opt_bool(certificate_authorities, Role =:= server, UserOpts, Opts),
@@ -957,24 +958,33 @@ opt_cacerts(UserOpts, #{verify := Verify, log_level := LogLevel, versions := Ver
     Opts2 = set_opt_new(Where2, certificate_authorities, Role =:= server, CA, Opts1),
     Opts2#{cacerts => CaCerts}.
 
-opt_tickets(UserOpts, #{versions := Versions} = Opts, #{role := client}) ->
-    {_, SessionTickets} = get_opt_of(session_tickets, [disabled,manual,auto], disabled, UserOpts, Opts),
+opt_tickets(UserOpts, #{versions := Versions} = Opts,
+            #{role := client}) ->
+    {_, SessionTickets} = get_opt_of(session_tickets, [disabled,manual,auto], disabled,
+                                     UserOpts, Opts),
     assert_version_dep(SessionTickets =/= disabled, session_tickets, Versions, ['tlsv1.3']),
 
-    {_, UseTicket} = get_opt_list(use_ticket, undefined, UserOpts, Opts),
-    option_error(UseTicket =:= [], use_ticket, UseTicket),
-    option_incompatible(UseTicket =/= undefined andalso SessionTickets =/= manual,
-                        [{use_ticket, UseTicket}, {session_tickets, SessionTickets}]),
+    {_, UseTickets} = get_opt_list(use_ticket, undefined, UserOpts, Opts),
+    case (SessionTickets == manual) andalso UseTickets =/= undefined of
+        true ->
+            verify_use_tickets(UseTickets, maps:get(server_name_indication, Opts));
+        _ ->
+            ok
+    end,
+    option_error(UseTickets =:= [], use_ticket, UseTickets),
+    option_incompatible(UseTickets =/= undefined andalso SessionTickets =/= manual,
+                        [{use_ticket, UseTickets}, {session_tickets, SessionTickets}]),
 
     {_, EarlyData} = get_opt_bin(early_data, undefined, UserOpts, Opts),
     option_incompatible(is_binary(EarlyData) andalso SessionTickets =:= disabled,
                         [early_data, {session_tickets, disabled}]),
-    option_incompatible(is_binary(EarlyData) andalso SessionTickets =:= manual andalso UseTicket =:= undefined,
+    option_incompatible(is_binary(EarlyData) andalso SessionTickets =:= manual andalso
+                        UseTickets =:= undefined,
                         [early_data, {session_tickets, manual}, {use_ticket, undefined}]),
 
     assert_server_only(anti_replay, UserOpts),
     assert_server_only(stateless_tickets_seed, UserOpts),
-    Opts#{session_tickets => SessionTickets, use_ticket => UseTicket, early_data => EarlyData};
+    Opts#{session_tickets => SessionTickets, use_ticket => UseTickets, early_data => EarlyData};
 opt_tickets(UserOpts, #{versions := Versions} = Opts, #{role := server}) ->
     {_, SessionTickets} =
         get_opt_of(session_tickets,
@@ -995,8 +1005,10 @@ opt_tickets(UserOpts, #{versions := Versions} = Opts, #{role := server}) ->
             {_, undefined} -> undefined;
             {_,AR} when not Stateless ->
                 option_incompatible([{anti_replay, AR}, {session_tickets, SessionTickets}]);
-            {_,'10k'}  -> {10, 5, 72985};  %% n = 10000 p = 0.030003564 (1 in 33) m = 72985 (8.91KiB) k = 5
-            {_,'100k'} -> {10, 5, 729845}; %% n = 10000 p = 0.03000428 (1 in 33) m = 729845 (89.09KiB) k = 5
+            %% n = 10000 p = 0.030003564 (1 in 33) m = 72985 (8.91KiB) k = 5
+            {_,'10k'}  -> {10, 5, 72985};
+            %% n = 10000 p = 0.03000428 (1 in 33) m = 729845 (89.09KiB) k = 5
+            {_,'100k'} -> {10, 5, 729845};
             {_, {_,_,_} = AR} -> AR;
             {_, AR} -> option_error(anti_replay, AR)
         end,
@@ -1009,6 +1021,13 @@ opt_tickets(UserOpts, #{versions := Versions} = Opts, #{role := server}) ->
     Opts#{session_tickets => SessionTickets, early_data => EarlyData,
           anti_replay => AntiReplay, stateless_tickets_seed => STS}.
 
+verify_use_tickets([], _) ->
+    true;
+verify_use_tickets([#{sni := SNI} | Tickests], SNI) ->
+    verify_use_tickets(Tickests, SNI);
+verify_use_tickets([Ticket | _], SNI) ->
+    option_error(ticket_for_other_SNI, {Ticket, SNI}).
+
 opt_stapling(UserOpts, #{versions := _Versions} = Opts, #{role := client}) ->
     {Stapling, Nonce} =
         case get_opt(stapling, ?DEFAULT_STAPLING_OPT, UserOpts, Opts) of
@@ -1111,7 +1130,8 @@ valid_signature_algs_cert(#{versions := Versions} = Opts, UserOpts, TlsVersion)
         {_, Schemes} ->
             Schemes
     end.
-valid_signature_algs(AlgCertSchemes0, #{versions := Versions} = Opts, UserOpts, [TlsVersion| _] = TlsVsns) ->
+valid_signature_algs(AlgCertSchemes0, #{versions := Versions} = Opts, UserOpts,
+                     [TlsVersion| _] = TlsVsns) ->
     case get_opt_list(signature_algs, undefined, UserOpts, Opts) of
         {default, undefined}  ->
             %% Smooth upgrade path allow rsa_pkcs1_sha1 for signatures_algs_cert
diff --git a/lib/ssl/test/ssl_api_SUITE.erl b/lib/ssl/test/ssl_api_SUITE.erl
index 4648f12f57..edde9426b4 100644
--- a/lib/ssl/test/ssl_api_SUITE.erl
+++ b/lib/ssl/test/ssl_api_SUITE.erl
@@ -986,7 +986,8 @@ handshake_continue_tls13_client(Config) when is_list(Config) ->
            {mfa, {ssl_test_lib, send_recv_result_active, []}},
            {options, ssl_test_lib:ssl_options([{handshake, hello},
                                                {session_tickets, manual},
-                                               {use_ticket, [DummyTicket]},
+                                               {use_ticket, [#{sni => net_adm:localhost(),
+                                                               ticket => DummyTicket}]},
                                                {versions, ['tlsv1.3',
                                                            'tlsv1.2',
                                                            'tlsv1.1',
@@ -2217,7 +2218,7 @@ customize_defaults(Opts, Role, Host) ->
 -define(OK(EXP, Opts, Role), ?OK(EXP,Opts, Role, [])).
 -define(OK(EXP, Opts, Role, ShouldBeMissing),
         fun() ->
-                Host = "dummy.host.org",
+                Host = net_adm:localhost(),
                 {__DefOpts, __Opts} = customize_defaults(Opts, Role, Host),
                 try ssl_config:handle_options(__Opts, Role, Host) of
                     {ok, #config{ssl=EXP = __ALL}} ->
@@ -2252,7 +2253,7 @@ customize_defaults(Opts, Role, Host) ->
 
 -define(ERR(EXP, Opts, Role),
         fun() ->
-                Host = "dummy.host.org",
+                Host = net_adm:localhost(),
                 {__DefOpts, __Opts} = customize_defaults(Opts, Role, Host),
                 try ssl_config:handle_options(__Opts, Role, Host) of
                     Other ->
@@ -2286,7 +2287,7 @@ customize_defaults(Opts, Role, Host) ->
 
 -define(ERR_UPD(EXP, Opts, Role),
         fun() ->
-                Host = "dummy.host.org",
+                Host = net_adm:localhost(),
                 {__DefOpts, __Opts} = customize_defaults(Opts, Role, Host),
                 try ssl_config:handle_options(__Opts, Role, Host) of
                     {ok, #config{}} ->
@@ -2718,6 +2719,7 @@ options_dh(Config) -> %% dh dhfile
     ok.
 
 options_early_data(_Config) -> %% early_data, session_tickets and use_ticket
+     SNI = net_adm:localhost(),
     ?OK(#{early_data := undefined, session_tickets := disabled},
         [], client),
     ?OK(#{early_data := disabled, session_tickets := disabled, stateless_tickets_seed := undefined},
@@ -2725,8 +2727,11 @@ options_early_data(_Config) -> %% early_data, session_tickets and use_ticket
 
     ?OK(#{early_data := <<>>, session_tickets := auto},
         [{early_data, <<>>}, {session_tickets, auto}], client),
-    ?OK(#{early_data := <<>>, session_tickets := manual, use_ticket := [<<1>>]},
-        [{early_data, <<>>}, {session_tickets, manual}, {use_ticket, [<<1>>]}],
+    ?OK(#{early_data := <<>>, session_tickets := manual,
+          use_ticket := [#{sni := SNI,
+                           ticket := <<1>>}]},
+        [{early_data, <<>>}, {session_tickets, manual},
+         {use_ticket, [#{sni => SNI, ticket => <<1>>}]}],
         client),
 
     ?OK(#{early_data := enabled, stateless_tickets_seed := <<"foo">>},
@@ -2795,7 +2800,8 @@ options_eccs(_Config) ->
 
 options_verify(Config) ->  %% fail_if_no_peer_cert, verify, verify_fun, partial_chain
     Cert = proplists:get_value(cert, ssl_test_lib:ssl_options(server_rsa_der_opts, Config)),
-    {ok, #config{ssl = DefOpts = #{verify_fun := {DefVerify,_}}}} = ssl_config:handle_options([{verify, verify_none}], client, "dummy.host.org"),
+    {ok, #config{ssl = DefOpts = #{verify_fun := {DefVerify,_}}}} =
+        ssl_config:handle_options([{verify, verify_none}], client, net_adm:localhost()),
 
     ?OK(#{fail_if_no_peer_cert := false, verify := verify_none, verify_fun := {DefVerify, []}, partial_chain := _},
         [], server),
@@ -3070,10 +3076,11 @@ options_reuse_session(_Config) ->
     ok.
 
 options_sni(_Config) -> %% server_name_indication
-    ?OK(#{server_name_indication := "dummy.host.org"}, [], client),
+    SNI = net_adm:localhost(),
+    ?OK(#{server_name_indication := SNI}, [], client),
     ?OK(#{}, [], server, [server_name_indication]),
     ?OK(#{server_name_indication := disable}, [{server_name_indication, disable}], client),
-    ?OK(#{server_name_indication := "dummy.org"}, [{server_name_indication, "dummy.org"}], client),
+    ?OK(#{server_name_indication := SNI}, [{server_name_indication, SNI}], client),
 
     ?OK(#{sni_fun := _}, [], server, [sni_hosts]),
 
@@ -3408,13 +3415,15 @@ client_options_negative_early_data(Config) when is_list(Config) ->
                            [early_data, {session_tickets, manual}, {use_ticket, undefined}]}),
     start_client_negative(Config, [{versions, ['tlsv1.2', 'tlsv1.3']},
                                    {session_tickets, manual},
-                                   {use_ticket, [<<"ticket">>]},
+                                   {use_ticket, [#{sni=> net_adm:localhost(),
+                                                   ticket => <<"ticket">>}]},
                                    {early_data, "test"}],
                           {options, {early_data, "test"}}),
     %% All options are ok but there is no server
     start_client_negative(Config, [{versions, ['tlsv1.2', 'tlsv1.3']},
                                    {session_tickets, manual},
-                                   {use_ticket, [<<"ticket">>]},
+                                   {use_ticket, [#{sni=> net_adm:localhost(),
+                                                   ticket => <<"ticket">>}]},
                                    {early_data, <<"test">>}],
                           econnrefused),
 
diff --git a/lib/ssl/test/ssl_session_ticket_SUITE.erl b/lib/ssl/test/ssl_session_ticket_SUITE.erl
index 1e9c410b92..cd4ac0a447 100644
--- a/lib/ssl/test/ssl_session_ticket_SUITE.erl
+++ b/lib/ssl/test/ssl_session_ticket_SUITE.erl
@@ -532,12 +532,13 @@ basic_stateless_stateful_anti_replay(Config) when is_list(Config) ->
 basic_stateful_stateless_faulty_ticket() ->
     [{doc,"Test session resumption with session tickets (erlang client - erlang server)"}].
 basic_stateful_stateless_faulty_ticket(Config) when is_list(Config) ->
+    SNI = net_adm:localhost(),
     do_test_mixed(Config,
                   [{session_tickets, auto},
                    {versions, ['tlsv1.2','tlsv1.3']}],
                   [{session_tickets, manual},
-                   {use_ticket, [<<131,100,0,12,"faultyticket">>,
-                                 <<"faulty ticket">>]},
+                   {use_ticket, [#{sni => SNI,
+                                   ticket => <<"faultyticket">>}]},
                    {versions, ['tlsv1.2','tlsv1.3']}],
                   [{session_tickets, stateless},
                    {anti_replay, '10k'},
@@ -548,12 +549,13 @@ basic_stateful_stateless_faulty_ticket(Config) when is_list(Config) ->
 basic_stateless_stateful_faulty_ticket() ->
     [{doc,"Test session resumption with session tickets (erlang client - erlang server)"}].
 basic_stateless_stateful_faulty_ticket(Config) when is_list(Config) ->
+    SNI = net_adm:localhost(),
     do_test_mixed(Config,
                   [{session_tickets, auto},
                    {versions, ['tlsv1.2','tlsv1.3']}],
                   [{session_tickets, manual},
-                   {use_ticket, [<<"faulty ticket">>,
-                                 <<131,100,0,12,"faultyticket">>]},
+                   {use_ticket, [#{sni => SNI,
+                                   ticket => <<"faultyticket">>}]},
                    {versions, ['tlsv1.2','tlsv1.3']}],
                   [{session_tickets, stateless},
                    {anti_replay, '10k'},
-- 
2.51.0

openSUSE Build Service is sponsored by