File 3772-Reject-stateless-tickets-until-2-WindowSize-has-pass.patch of Package erlang
From 489c3a3f86eab939d5e4b6bbb1a7c0f4f618a577 Mon Sep 17 00:00:00 2001
From: Anders Kiel Hovgaard <anders.hovgaard@motorolasolutions.com>
Date: Thu, 7 Jul 2022 11:40:39 +0200
Subject: [PATCH 2/2] Reject stateless tickets until 2*WindowSize has passed
As per RFC 8446 8.2 Client Hello Recording:
"When implementations are freshly started, they SHOULD reject 0-RTT as
long as any portion of their recording window overlaps the startup time.
Otherwise, they run the risk of accepting replays which were originally
sent during that period."
Before the `stateless_tickets_seed` option, when ticket encryption
secrets were generated upon socket creation, this wasn't necessary since
tickets from a previous instance of a server were not usable anyway.
The "warm-up" is only enabled when the `stateless_tickets_seed` option
is specified, such that this change doesn't affect legacy flows when the
options isn't specified.
Only enable warm up when seed option is specified
---
lib/ssl/src/tls_server_session_ticket.erl | 25 +++++++++++--
lib/ssl/test/ssl_session_ticket_SUITE.erl | 43 ++++++++++++++++++++++-
2 files changed, 65 insertions(+), 3 deletions(-)
diff --git a/lib/ssl/src/tls_server_session_ticket.erl b/lib/ssl/src/tls_server_session_ticket.erl
index 08f3b19111..609df77f1f 100644
--- a/lib/ssl/src/tls_server_session_ticket.erl
+++ b/lib/ssl/src/tls_server_session_ticket.erl
@@ -122,10 +122,13 @@ handle_cast(_Request, State) ->
{noreply, NewState :: term()}.
handle_info(rotate_bloom_filters,
#state{stateless = #{bloom_filter := BloomFilter0,
+ warm_up_windows_remaining := WarmUp0,
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}}};
+ WarmUp = max(WarmUp0 - 1, 0),
+ {noreply, State#state{stateless = Stateless#{bloom_filter => BloomFilter,
+ warm_up_windows_remaining => WarmUp}}};
handle_info({'DOWN', Monitor, _, _, _}, #state{listen_monitor = Monitor} = State) ->
{stop, normal, State};
handle_info(_Info, State) ->
@@ -163,6 +166,7 @@ inital_state([stateless, Lifetime, _, MaxEarlyDataSize, {Window, K, M}, Seed]) -
erlang:send_after(Window * 1000, self(), rotate_bloom_filters),
#state{nonce = 0,
stateless = #{bloom_filter => tls_bloom_filter:new(K, M),
+ warm_up_windows_remaining => warm_up_windows(Seed),
seed => stateless_seed(Seed),
window => Window},
lifetime = Lifetime,
@@ -431,7 +435,15 @@ in_window(_, undefined) ->
in_window(Age, Window) when is_integer(Window) ->
Age =< Window.
-stateless_anti_replay(Index, PSK, Binder,
+stateless_anti_replay(_Index, _PSK, _Binder,
+ #state{stateless = #{warm_up_windows_remaining := WarmUpRemaining}
+ } = State) when WarmUpRemaining > 0 ->
+ %% Reject all tickets during the warm-up period:
+ %% RFC 8446 8.2 Client Hello Recording
+ %% "When implementations are freshly started, they SHOULD reject 0-RTT as
+ %% long as any portion of their recording window overlaps the startup time."
+ {{ok, undefined}, State};
+stateless_anti_replay(Index, PSK, Binder,
#state{stateless = #{bloom_filter := BloomFilter0}
= Stateless} = State) ->
case tls_bloom_filter:contains(BloomFilter0, Binder) of
@@ -453,3 +465,12 @@ stateless_seed(undefined) ->
stateless_seed(Seed) ->
<<IV:16/binary, Shard:32/binary, _/binary>> = crypto:hash(sha512, Seed),
{IV, Shard}.
+
+-spec warm_up_windows(Seed :: undefined | binary()) -> 0 | 2.
+warm_up_windows(undefined) ->
+ 0;
+warm_up_windows(_) ->
+ %% When the encryption seed is specified, "warm up" the bloom filter for
+ %% 2*WindowSize to ensure tickets from a previous instance of the server
+ %% (before a restart) cannot be reused, if the ticket encryption seed is reused.
+ 2.
diff --git a/lib/ssl/test/ssl_session_ticket_SUITE.erl b/lib/ssl/test/ssl_session_ticket_SUITE.erl
index 6000117f6f..9bb57c6f8d 100644
--- a/lib/ssl/test/ssl_session_ticket_SUITE.erl
+++ b/lib/ssl/test/ssl_session_ticket_SUITE.erl
@@ -45,6 +45,8 @@
ticket_reuse_anti_replay/1,
ticket_reuse_anti_replay_server_restart/0,
ticket_reuse_anti_replay_server_restart/1,
+ ticket_reuse_anti_replay_server_restart_reused_seed/0,
+ ticket_reuse_anti_replay_server_restart_reused_seed/1,
basic_stateful_stateless/0,
basic_stateful_stateless/1,
basic_stateless_stateful/0,
@@ -107,6 +109,7 @@ groups() ->
ticketage_bigger_than_windowsize_anti_replay,
ticketage_out_of_lifetime_anti_replay, ticket_reuse_anti_replay,
ticket_reuse_anti_replay_server_restart,
+ ticket_reuse_anti_replay_server_restart_reused_seed,
stateless_multiple_servers]},
{mixed, [], mixed_tests()}].
@@ -328,6 +331,36 @@ ticket_reuse_anti_replay_server_restart(Config) when is_list(Config) ->
process_flag(trap_exit, false),
[ssl_test_lib:close(A) || A <- [Server0, Client2, Server1]].
+ticket_reuse_anti_replay_server_restart_reused_seed() ->
+ [{doc, "Verify 2 connection attempts with same stateless tickets "
+ "and server restart between, with the server using the same session "
+ "ticket encryption seed between restarts. Second attempt is expected to "
+ "fail as long as the Bloom filter window overlaps with startup time."
+ }].
+ticket_reuse_anti_replay_server_restart_reused_seed(Config) when is_list(Config) ->
+ WindowSize = 10,
+ Seed = crypto:strong_rand_bytes(32),
+ Config1 = [{server_ticket_seed, Seed} | Config],
+ {Server1 , Port1} = anti_replay_helper_start_server(Config1, WindowSize),
+ {ClientNode, _ServerNode, Hostname} = ssl_test_lib:run_where(Config),
+ ClientOpts0 = ssl_test_lib:ssl_options(client_rsa_verify_opts, Config),
+ ClientOpts1 = [{session_tickets, manual},
+ {versions, ['tlsv1.2','tlsv1.3']} | ClientOpts0],
+ Client1 = ssl_test_lib:start_client([{node, ClientNode},
+ {port, Port1}, {host, Hostname},
+ {mfa, {ssl_test_lib, %% full handshake
+ verify_active_session_resumption,
+ [false, wait_reply, {tickets, 1}]}},
+ {from, self()}, {options, ClientOpts1}]),
+ [Ticket] = ssl_test_lib:check_tickets(Client1),
+ ssl_test_lib:check_result(Server1, ok),
+ ClientOpts2 = [{use_ticket, [Ticket]} | ClientOpts1],
+ {Server2, Port2} = anti_replay_helper_start_server(Config1, WindowSize),
+ Client2 = anti_replay_helper_connect(Server2, Client1, Port2, ClientNode,
+ Hostname, ClientOpts2, 0, false, false),
+ process_flag(trap_exit, false),
+ [ssl_test_lib:close(A) || A <- [Server1, Client2, Server2]].
+
anti_replay_helper_init(Config, Mode, WindowSize) ->
DefaultLifetime = ssl_config:get_ticket_lifetime(),
anti_replay_helper_init(Config, Mode, WindowSize, DefaultLifetime).
@@ -363,9 +396,17 @@ anti_replay_helper_start_server(Config, WindowSize) ->
{_ClientNode, ServerNode, _Hostname} = ssl_test_lib:run_where(Config),
ServerOpts0 = ssl_test_lib:ssl_options(server_rsa_verify_opts, Config),
ServerTicketMode = proplists:get_value(server_ticket_mode, Config),
+ ServerTicketSeed =
+ case proplists:get_value(server_ticket_seed, Config) of
+ undefined ->
+ [];
+ Seed ->
+ [{stateless_tickets_seed, Seed}]
+ end,
ServerOpts = [{session_tickets, ServerTicketMode},
{anti_replay, {WindowSize, 5, 72985}},
- {versions, ['tlsv1.2','tlsv1.3']}|ServerOpts0],
+ {versions, ['tlsv1.2','tlsv1.3']}|ServerOpts0
+ ] ++ ServerTicketSeed,
Server =
ssl_test_lib:start_server([{node, ServerNode}, {port, 0},
--
2.35.3