summaryrefslogtreecommitdiff
path: root/lib/ssl/src/tls_server_session_ticket.erl
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ssl/src/tls_server_session_ticket.erl')
-rw-r--r--lib/ssl/src/tls_server_session_ticket.erl241
1 files changed, 201 insertions, 40 deletions
diff --git a/lib/ssl/src/tls_server_session_ticket.erl b/lib/ssl/src/tls_server_session_ticket.erl
index f6b91404fb..a2e5c327a0 100644
--- a/lib/ssl/src/tls_server_session_ticket.erl
+++ b/lib/ssl/src/tls_server_session_ticket.erl
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2007-2022. All Rights Reserved.
+%% Copyright Ericsson AB 2007-2023. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -31,8 +31,8 @@
-include("ssl_cipher.hrl").
%% API
--export([start_link/6,
- new/3,
+-export([start_link/7,
+ new/4,
use/4
]).
@@ -40,6 +40,9 @@
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3, format_status/2]).
+%% Tracing
+-export([handle_trace/3]).
+
-define(SERVER, ?MODULE).
-record(state, {
@@ -54,15 +57,24 @@
%%%===================================================================
%%% API
%%%===================================================================
--spec start_link(term(), atom(), integer(), integer(), integer(), tuple()) -> {ok, Pid :: pid()} |
+-spec start_link(term(), Mode, integer(), integer(), integer(), tuple(), Seed) ->
+ {ok, Pid :: pid()} |
{error, Error :: {already_started, pid()}} |
{error, Error :: term()} |
- ignore.
-start_link(Listener, Mode, Lifetime, TicketStoreSize, MaxEarlyDataSize, AntiReplay) ->
- gen_server:start_link(?MODULE, [Listener, Mode, Lifetime, TicketStoreSize, MaxEarlyDataSize, AntiReplay], []).
-
-new(Pid, Prf, MasterSecret) ->
- gen_server:call(Pid, {new_session_ticket, Prf, MasterSecret}, infinity).
+ ignore
+ when Mode :: stateful | stateless | stateful_with_cert | stateless_with_cert,
+ Seed :: undefined | binary().
+start_link(Listener, Mode1, Lifetime, TicketStoreSize, MaxEarlyDataSize, AntiReplay, Seed) ->
+ Mode = case Mode1 of
+ stateful_with_cert -> stateful;
+ stateless_with_cert -> stateless;
+ _ -> Mode1
+ end,
+ gen_server:start_link(?MODULE, [Listener, Mode, Lifetime, TicketStoreSize,
+ MaxEarlyDataSize, AntiReplay, Seed], []).
+
+new(Pid, Prf, MasterSecret, PeerCert) ->
+ gen_server:call(Pid, {new_session_ticket, Prf, MasterSecret, PeerCert}, infinity).
use(Pid, Identifiers, Prf, HandshakeHist) ->
gen_server:call(Pid, {use_ticket, Identifiers, Prf, HandshakeHist},
@@ -76,12 +88,12 @@ use(Pid, Identifiers, Prf, HandshakeHist) ->
init([Listener | Args]) ->
process_flag(trap_exit, true),
Monitor = inet:monitor(Listener),
- State = inital_state(Args),
+ State = initial_state(Args),
{ok, State#state{listen_monitor = Monitor}}.
-spec handle_call(Request :: term(), From :: {pid(), term()}, State :: term()) ->
{reply, Reply :: term(), NewState :: term()} .
-handle_call({new_session_ticket, Prf, MasterSecret}, _From,
+handle_call({new_session_ticket, Prf, MasterSecret, PeerCert}, _From,
#state{nonce = Nonce,
lifetime = LifeTime,
max_early_data_size = MaxEarlyDataSize,
@@ -89,14 +101,14 @@ handle_call({new_session_ticket, Prf, MasterSecret}, _From,
Id = stateful_psk_ticket_id(IdGen),
PSK = tls_v1:pre_shared_key(MasterSecret, ticket_nonce(Nonce), Prf),
SessionTicket = new_session_ticket(Id, Nonce, LifeTime, MaxEarlyDataSize),
- State = stateful_ticket_store(Id, SessionTicket, Prf, PSK, State0),
+ State = stateful_ticket_store(Id, SessionTicket, Prf, PSK, PeerCert, State0),
{reply, SessionTicket, State};
-handle_call({new_session_ticket, Prf, MasterSecret}, _From,
+handle_call({new_session_ticket, Prf, MasterSecret, PeerCert}, _From,
#state{nonce = Nonce,
stateless = #{}} = State) ->
BaseSessionTicket = new_session_ticket_base(State),
SessionTicket = generate_stateless_ticket(BaseSessionTicket, Prf,
- MasterSecret, State),
+ MasterSecret, PeerCert, State),
{reply, SessionTicket, State#state{nonce = Nonce+1}};
handle_call({use_ticket, Identifiers, Prf, HandshakeHist}, _From,
#state{stateful = #{}} = State0) ->
@@ -118,10 +130,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) ->
@@ -144,29 +159,28 @@ code_change(_OldVsn, State, _Extra) ->
Status :: list()) -> Status :: term().
format_status(_Opt, Status) ->
Status.
+
%%%===================================================================
%%% Internal functions
%%%===================================================================
-
-inital_state([stateless, Lifetime, _, MaxEarlyDataSize, undefined]) ->
+initial_state([stateless, Lifetime, _, MaxEarlyDataSize, undefined, Seed]) ->
#state{nonce = 0,
- stateless = #{seed => {crypto:strong_rand_bytes(16),
- crypto:strong_rand_bytes(32)},
+ stateless = #{seed => stateless_seed(Seed),
window => undefined},
lifetime = Lifetime,
max_early_data_size = MaxEarlyDataSize
};
-inital_state([stateless, Lifetime, _, MaxEarlyDataSize, {Window, K, M}]) ->
+initial_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),
- seed => {crypto:strong_rand_bytes(16),
- crypto:strong_rand_bytes(32)},
+ warm_up_windows_remaining => warm_up_windows(Seed),
+ seed => stateless_seed(Seed),
window => Window},
lifetime = Lifetime,
max_early_data_size = MaxEarlyDataSize
};
-inital_state([stateful, Lifetime, TicketStoreSize, MaxEarlyDataSize|_]) ->
+initial_state([stateful, Lifetime, TicketStoreSize, MaxEarlyDataSize|_]) ->
%% statfeful servers replay
%% protection is that it saves
%% all valid tickets
@@ -229,18 +243,18 @@ validate_binder(Binder, HandshakeHist, PSK, Prf, AlertDetail) ->
stateful_store() ->
gb_trees:empty().
-stateful_ticket_store(Ref, NewSessionTicket, Hash, Psk,
+stateful_ticket_store(Ref, NewSessionTicket, Hash, Psk, PeerCert,
#state{nonce = Nonce,
stateful = #{db := Tree0,
max := Max,
ref_index := Index0} = Stateful}
= State0) ->
Id = {erlang:monotonic_time(), erlang:unique_integer([monotonic])},
- StatefulTicket = {NewSessionTicket, Hash, Psk},
+ StatefulTicket = {NewSessionTicket, Hash, Psk, PeerCert},
case gb_trees:size(Tree0) of
Max ->
%% Trow away oldest ticket
- {_, {#new_session_ticket{ticket = OldRef},_,_}, Tree1}
+ {_, {#new_session_ticket{ticket = OldRef},_,_,_}, Tree1}
= gb_trees:take_smallest(Tree0),
Tree = gb_trees:insert(Id, StatefulTicket, Tree1),
Index = maps:without([OldRef], Index0),
@@ -272,8 +286,8 @@ stateful_use([#psk_identity{identity = Ref} | Refs], [Binder | Binders],
HandshakeHist, Tree0) of
true ->
RefIndex = maps:without([Ref], RefIndex0),
- {{_,_, PSK}, Tree} = gb_trees:take(Key, Tree0),
- {{ok, {Index, PSK}},
+ {{_,_, PSK, PeerCert}, Tree} = gb_trees:take(Key, Tree0),
+ {{ok, {Index, PSK, PeerCert}},
State#state{stateful = Stateful#{db => Tree,
ref_index => RefIndex}}};
false ->
@@ -291,7 +305,7 @@ stateful_usable_ticket(Key, Prf, Binder, HandshakeHist, Tree) ->
case gb_trees:lookup(Key, Tree) of
none ->
false;
- {value, {NewSessionTicket, Prf, PSK}} ->
+ {value, {NewSessionTicket, Prf, PSK, _PeerCert}} ->
case stateful_living_ticket(Key, NewSessionTicket) of
true ->
validate_binder(Binder, HandshakeHist, PSK, Prf, stateful);
@@ -323,7 +337,7 @@ stateful_psk_ticket_id(Key) ->
generate_stateless_ticket(#new_session_ticket{ticket_nonce = Nonce,
ticket_age_add = TicketAgeAdd,
ticket_lifetime = Lifetime}
- = Ticket, Prf, MasterSecret,
+ = Ticket, Prf, MasterSecret, PeerCert,
#state{stateless = #{seed := {IV, Shard}}}) ->
PSK = tls_v1:pre_shared_key(MasterSecret, Nonce, Prf),
Timestamp = erlang:system_time(second),
@@ -332,7 +346,8 @@ generate_stateless_ticket(#new_session_ticket{ticket_nonce = Nonce,
pre_shared_key = PSK,
ticket_age_add = TicketAgeAdd,
lifetime = Lifetime,
- timestamp = Timestamp
+ timestamp = Timestamp,
+ certificate = PeerCert
}, Shard, IV),
Ticket#new_session_ticket{ticket = Encrypted}.
@@ -351,11 +366,12 @@ stateless_use([#psk_identity{identity = Encrypted,
window := Window}} = State) ->
case ssl_cipher:decrypt_ticket(Encrypted, Shard, IV) of
#stateless_ticket{hash = Prf,
- pre_shared_key = PSK} = Ticket ->
+ pre_shared_key = PSK,
+ certificate = PeerCert} = Ticket ->
case stateless_usable_ticket(Ticket, ObfAge, Binder,
HandshakeHist, Window) of
true ->
- stateless_anti_replay(Index, PSK, Binder, State);
+ stateless_anti_replay(Index, PSK, Binder, PeerCert, State);
false ->
stateless_use(Ids, Binders, Prf, HandshakeHist,
Index+1, State);
@@ -382,19 +398,62 @@ stateless_usable_ticket(#stateless_ticket{hash = Prf,
stateless_living_ticket(0, _, _, _, _) ->
true;
+%% If `anti_replay` is not enabled, then a ticket is considered to be living
+%% if it has not exceeded its lifetime.
+%%
+%% If `anti_replay` is enabled, we must additionally perform a freshness check
+%% as is outlined in section 8.3 Freshness Checks - RFC 8446
stateless_living_ticket(ObfAge, TicketAgeAdd, Lifetime, Timestamp, Window) ->
- ReportedAge = ObfAge - TicketAgeAdd,
+ %% RealAge is the server's view of the age of the ticket in seconds.
RealAge = erlang:system_time(second) - Timestamp,
+
+ %% ReportedAge is the client's view of the age of the ticket in milliseconds.
+ ReportedAge = ObfAge - TicketAgeAdd,
+
+ %% DeltaAge is the difference of the client's view of the age of the ticket
+ %% and the server's view of the age of the ticket in seconds.
+ DeltaAge = abs(RealAge - (ReportedAge / 1000)),
+
+ %% We ensure that both the client's view of the age of the ticket and the
+ %% server's view of the age of the ticket do not exceed the lifetime specified.
(ReportedAge =< Lifetime * 1000)
andalso (RealAge =< Lifetime)
- andalso (in_window(RealAge, Window)).
+ andalso (in_window(DeltaAge, Window)).
in_window(_, undefined) ->
true;
+%% RFC 8446 - section 8.2 Client Hello Recording
+%% describes an anti-replay implementation that can use bounded memory
+%% by storing a unique value from a ClientHello (in our case the PSK binder)
+%% withing a given time window.
+%%
+%% In order implement this, when a ClientHello is received, the server
+%% must ensure that a ClientHello has been sent relatively recently.
+%% We do this by ensuring that the client and server view of the age
+%% of the ticket is not larger than our recording window.
+%%
+%% In the case of an attempted replay attack, there are 2 possible
+%% outcomes:
+%% - A ClientHello is replayed within the recording window
+%% * The ticket looks valid, `in_window` returns true
+%% so we proceed to check the unique value
+%% * The unique value (PSK Binder) is stored in the bloom filter
+%% and we reject the ticket.
+%%
+%% - A ClientHello is replayed outside the recording window
+%% * We reject the ticket as `in_window` returns false.
in_window(Age, Window) when is_integer(Window) ->
Age =< Window.
-stateless_anti_replay(Index, PSK, Binder,
+stateless_anti_replay(_Index, _PSK, _Binder, _PeerCert,
+ #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, PeerCert,
#state{stateless = #{bloom_filter := BloomFilter0}
= Stateless} = State) ->
case tls_bloom_filter:contains(BloomFilter0, Binder) of
@@ -403,8 +462,110 @@ stateless_anti_replay(Index, PSK, Binder,
{{ok, undefined}, State};
false ->
BloomFilter = tls_bloom_filter:add_elem(BloomFilter0, Binder),
- {{ok, {Index, PSK}},
+ {{ok, {Index, PSK, PeerCert}},
State#state{stateless = Stateless#{bloom_filter => BloomFilter}}}
end;
-stateless_anti_replay(Index, PSK, _, State) ->
- {{ok, {Index, PSK}}, State}.
+stateless_anti_replay(Index, PSK, _Binder, PeerCert, State) ->
+ {{ok, {Index, PSK, PeerCert}}, State}.
+
+-spec stateless_seed(Seed :: undefined | binary()) ->
+ {IV :: binary(), Shard :: binary()}.
+stateless_seed(undefined) ->
+ {crypto:strong_rand_bytes(16), crypto:strong_rand_bytes(32)};
+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.
+
+%%%################################################################
+%%%#
+%%%# Tracing
+%%%#
+handle_trace(rle, {call, {?MODULE, init, [[ListenSocket, Mode, Lifetime,
+ StoreSize | _T]]}}, Stack) ->
+ {io_lib:format("(*server) ([ListenSocket = ~w Mode = ~w Lifetime = ~w "
+ "StoreSize = ~w, ...])",
+ [ListenSocket, Mode, Lifetime, StoreSize]),
+ [{role, server} | Stack]};
+handle_trace(ssn,
+ {call, {?MODULE, terminate, [Reason, _State]}}, Stack) ->
+ {io_lib:format("(Reason ~w)", [Reason]), Stack};
+handle_trace(ssn,
+ {call, {?MODULE, handle_call,
+ [CallTuple, _From, _State]}}, Stack) ->
+ {io_lib:format("(Call = ~w)", [element(1, CallTuple)]), Stack};
+handle_trace(ssn,
+ {call, {?MODULE,handle_call,
+ [{Call = use_ticket,
+ {offered_psks,
+ [{psk_identity, PskIdentity, _ObfAge}],
+ [Binder]},
+ _Prf,
+ _HandshakeHist}, _From, _State]}}, Stack) ->
+ {io_lib:format("(Call = ~w PskIdentity = ~W Binder = ~W)",
+ [Call, PskIdentity, 5, Binder, 5]), Stack};
+handle_trace(ssn,
+ {call, {?MODULE, validate_binder,
+ [Binder, _HandshakeHist, PSK, _Prf, _AlertDetail]}}, Stack) ->
+ {io_lib:format("(Binder = ~W PSK = ~W)", [Binder, 5, PSK, 5]), Stack};
+handle_trace(ssn,
+ {call, {?MODULE, initial_state,
+ [[Mode, _Lifetime, _StoreSize,_MaxEarlyDataSize,
+ Window, Seed]]}}, Stack) ->
+ {io_lib:format("(Mode = ~w Window = ~w Seed = ~W)", [Mode, Window, Seed, 5]), Stack};
+handle_trace(ssn,
+ {call, {?MODULE, generate_stateless_ticket,
+ [_BaseTicket, _Prf, MasterSecret, _State]}}, Stack) ->
+ {io_lib:format("(MasterSecret = ~W)", [MasterSecret, 5]), Stack};
+handle_trace(ssn,
+ {call, {?MODULE, stateless_use,
+ [[{psk_identity, Encrypted, _ObfAge} | _],
+ [Binder | _],
+ _Prf, _HandshakeHist, _Index, _State]}}, Stack) ->
+ {io_lib:format("(Encrypted = ~W Binder = ~W)", [Encrypted, 5, Binder, 5]), Stack};
+handle_trace(ssn,
+ {call, {?MODULE, in_window, [RealAge, Window]}}, Stack) ->
+ {io_lib:format("(RealAge = ~w Window = ~w)",
+ [RealAge, Window]), Stack};
+handle_trace(ssn,
+ {call, {?MODULE, stateless_usable_ticket,
+ [{stateless_ticket, _Prf, _PreSharedKey, _TicketAgeAdd,
+ _Lifetime, _Timestamp}, _ObfAge, Binder,
+ _HandshakeHist, Window]}}, Stack) ->
+ {io_lib:format("(Binder = ~W Window = ~w)", [Binder, 5, Window]), Stack};
+handle_trace(ssn,
+ {call, {?MODULE, stateless_anti_replay,
+ [_Index, PSK, _Binder, _State]}}, Stack) ->
+ {io_lib:format("(PSK = ~W)", [PSK, 5]), Stack};
+handle_trace(ssn,
+ {return_from, {?MODULE, stateless_use, 6},
+ {{ok, {_Index, PSK}}, _State}}, Stack) ->
+ {io_lib:format("PSK = ~W", [PSK, 5]), Stack};
+handle_trace(ssn,
+ {return_from, {?MODULE, generate_stateless_ticket, 4},
+ {new_session_ticket, _LifeTime, _AgeAdd, _Nonce, Ticket,
+ _Extensions}}, Stack) ->
+ {io_lib:format("Ticket = ~W", [Ticket, 5]), Stack};
+handle_trace(ssn,
+ {return_from, {?MODULE, initial_state, 1},
+ {state, _Stateless = #{seed := {IV, Shard}, window := Window},
+ _Stateful = undefined, _Nonce, _Lifetime ,
+ _MaxEarlyDataSize, ListenMonitor}}, Stack) ->
+ {io_lib:format("IV = ~W Shard = ~W Window = ~w ListenMonitor = ~w",
+ [IV, 5, Shard, 5, Window, ListenMonitor]), Stack};
+handle_trace(ssn,
+ {return_from, {?MODULE, stateless_anti_replay, 4},
+ {{ok, {_Index, PSK}}, _State}}, Stack) ->
+ {io_lib:format("ticket OK ~W", [PSK, 5]), Stack};
+handle_trace(ssn,
+ {return_from, {?MODULE, stateless_anti_replay, 4},
+ Return}, Stack) ->
+ {io_lib:format("ticket REJECTED ~W", [Return, 5]), Stack}.