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