summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorIngela Anderton Andin <ingela@erlang.org>2019-11-21 11:06:47 +0100
committerIngela Anderton Andin <ingela@erlang.org>2019-11-21 11:06:47 +0100
commit4d105778fefc48e132248890b80e6eac999798eb (patch)
tree31243d85d7585724be2efd68c1858cdb0bb1e4c0 /lib
parent81d9f5a1c66ced5be94246dcc14f8902ce476c86 (diff)
parentcfad165ba34b3d50ecb0b89783752045dcd76081 (diff)
downloaderlang-4d105778fefc48e132248890b80e6eac999798eb.tar.gz
Merge branch 'ingela/ssl/stateful-session-ticket/OTP-16238' into maint
* ingela/ssl/stateful-session-ticket/OTP-16238: ssl: Unify statful and staless session ticket handling ssl: Add statfull tickets
Diffstat (limited to 'lib')
-rw-r--r--lib/ssl/src/tls_handshake_1_3.erl237
-rw-r--r--lib/ssl/src/tls_server_session_ticket.erl307
-rw-r--r--lib/ssl/src/tls_socket.erl12
-rw-r--r--lib/ssl/test/ssl_session_ticket_SUITE.erl40
4 files changed, 323 insertions, 273 deletions
diff --git a/lib/ssl/src/tls_handshake_1_3.erl b/lib/ssl/src/tls_handshake_1_3.erl
index 4cede4f99f..119ed75d36 100644
--- a/lib/ssl/src/tls_handshake_1_3.erl
+++ b/lib/ssl/src/tls_handshake_1_3.erl
@@ -54,6 +54,7 @@
maybe_add_binders/4,
maybe_automatic_session_resumption/1]).
+-export([is_valid_binder/4]).
%% crypto:hash(sha256, "HelloRetryRequest").
-define(HELLO_RETRY_REQUEST_RANDOM, <<207,33,173,116,229,154,97,17,
@@ -364,6 +365,18 @@ decode_handshake(?KEY_UPDATE, <<?BYTE(Update)>>) ->
decode_handshake(Tag, HandshakeMsg) ->
ssl_handshake:decode_handshake({3,4}, Tag, HandshakeMsg).
+is_valid_binder(Binder, HHistory, PSK, Hash) ->
+ case HHistory of
+ [ClientHello2, HRR, MessageHash|_] ->
+ Truncated = truncate_client_hello(ClientHello2),
+ FinishedKey = calculate_finished_key(PSK, Hash),
+ Binder == calculate_binder(FinishedKey, Hash, [MessageHash, HRR, Truncated]);
+ [ClientHello1|_] ->
+ Truncated = truncate_client_hello(ClientHello1),
+ FinishedKey = calculate_finished_key(PSK, Hash),
+ Binder == calculate_binder(FinishedKey, Hash, Truncated)
+ end.
+
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
@@ -549,11 +562,6 @@ do_start(#client_hello{cipher_suites = ClientCiphers,
%% the client.
Cipher = Maybe(select_cipher_suite(HonorCipherOrder, ClientCiphers, ServerCiphers)),
- State1 = maybe_seed_session_tickets(OfferedPSKs, State0),
-
- %% Exclude any incompatible PSKs.
- PSK = Maybe(handle_pre_shared_key(State1, OfferedPSKs, Cipher)),
-
Groups = Maybe(select_common_groups(ServerGroups, ClientGroups)),
Maybe(validate_client_key_share(ClientGroups, ClientShares)),
@@ -574,7 +582,7 @@ do_start(#client_hello{cipher_suites = ClientCiphers,
%% Generate server_share
KeyShare = ssl_cipher:generate_server_share(Group),
- State2 = update_start_state(State1,
+ State1 = update_start_state(State0,
#{cipher => Cipher,
key_share => KeyShare,
session_id => SessionId,
@@ -589,9 +597,14 @@ do_start(#client_hello{cipher_suites = ClientCiphers,
%% message if it is able to find an acceptable set of parameters but the
%% ClientHello does not contain sufficient information to proceed with
%% the handshake.
- NextStateTuple = Maybe(send_hello_retry_request(State2, ClientPubKey, KeyShare, SessionId)),
- Maybe(session_resumption(NextStateTuple, PSK))
-
+ case Maybe(send_hello_retry_request(State1, ClientPubKey, KeyShare, SessionId)) of
+ {_, start} = NextStateTuple ->
+ NextStateTuple;
+ {_, negotiated} = NextStateTuple ->
+ %% Exclude any incompatible PSKs.
+ PSK = Maybe(handle_pre_shared_key(State1, OfferedPSKs, Cipher)),
+ Maybe(session_resumption(NextStateTuple, PSK))
+ end
catch
{Ref, {insufficient_security, no_suitable_groups}} ->
?ALERT_REC(?FATAL, ?INSUFFICIENT_SECURITY, no_suitable_groups);
@@ -1107,9 +1120,6 @@ send_hello_retry_request(State0, _, _, _) ->
%% Suitable key found.
{ok, {State0, negotiated}}.
-
-session_resumption({State, start}, _) ->
- {ok, {State, start}};
session_resumption({#state{ssl_options = #{session_tickets := disabled}} = State, negotiated}, _) ->
{ok, {State, negotiated}};
session_resumption({#state{ssl_options = #{session_tickets := Tickets}} = State, negotiated}, undefined)
@@ -1163,56 +1173,18 @@ maybe_send_certificate_verify(#state{session = #session{sign_alg = SignatureSche
maybe_send_session_ticket(#state{ssl_options = #{session_tickets := disabled}} = State, _) ->
%% Do nothing!
State;
-maybe_send_session_ticket(#state{ssl_options = #{session_tickets := SessionTickets}} = State, 0)
- when SessionTickets =/= disabled ->
+maybe_send_session_ticket(State, 0) ->
State;
-maybe_send_session_ticket(#state{ssl_options = #{session_tickets := stateful},
+maybe_send_session_ticket(#state{connection_states = ConnectionStates,
static_env = #static_env{trackers = Trackers}} = State0, N) ->
Tracker = proplists:get_value(session_tickets_tracker, Trackers),
- Ticket = tls_server_session_ticket:new(Tracker),
- {State, _} = tls_connection:send_handshake(Ticket, State0),
- maybe_send_session_ticket(State, N - 1);
-maybe_send_session_ticket(#state{
- ssl_options = #{session_tickets := stateless},
- handshake_env =
- #handshake_env{ticket_seed =
- {BaseTicket, {IV, Shard} = Seed}} = HsEnv
- } = State0, N) ->
- Ticket = generate_statless_ticket(BaseTicket, IV, Shard, State0),
- {State, _} = tls_connection:send_handshake(Ticket, State0),
- %% Remove first "BaseTicket" when used
- maybe_send_session_ticket(State#state{handshake_env = HsEnv#handshake_env{ticket_seed = Seed}}, N - 1);
-maybe_send_session_ticket(#state{ssl_options = #{session_tickets := stateless},
- handshake_env = #handshake_env{ticket_seed = {IV, Shard}},
- static_env = #static_env{trackers = Trackers}
- } = State0, N) ->
- Tracker = proplists:get_value(session_tickets_tracker, Trackers),
- BaseTicket = tls_server_session_ticket:new(Tracker),
- Ticket = generate_statless_ticket(BaseTicket, IV, Shard, State0),
- {State, _} = tls_connection:send_handshake(Ticket, State0),
- maybe_send_session_ticket(State, N - 1).
-
-
-%% Generate ticket field of NewSessionTicket.
-generate_statless_ticket(#new_session_ticket{ticket_nonce = Nonce, ticket_age_add = TicketAgeAdd,
- ticket_lifetime = Lifetime} = Ticket, IV, Shard,
- #state{connection_states = ConnectionStates}) ->
#{security_parameters := SecParamsR} =
ssl_record:current_connection_state(ConnectionStates, read),
#security_parameters{prf_algorithm = HKDF,
- resumption_master_secret = RMS} = SecParamsR,
-
- PSK = tls_v1:pre_shared_key(RMS, Nonce, HKDF),
- Timestamp = erlang:system_time(second),
- Encrypted = ssl_cipher:encrypt_ticket(#stateless_ticket{
- hash = HKDF,
- pre_shared_key = PSK,
- ticket_age_add = TicketAgeAdd,
- lifetime = Lifetime,
- timestamp = Timestamp
- }, Shard, IV),
- Ticket#new_session_ticket{ticket = Encrypted}.
-
+ resumption_master_secret = RMS} = SecParamsR,
+ Ticket = tls_server_session_ticket:new(Tracker, HKDF, RMS),
+ {State, _} = tls_connection:send_handshake(Ticket, State0),
+ maybe_send_session_ticket(State, N - 1).
process_certificate_request(#certificate_request_1_3{},
#state{session = #session{own_certificate = undefined}} = State) ->
@@ -2120,79 +2092,6 @@ get_offered_psks(Extensions) ->
end.
-decode_pre_shared_keys(Shard, IV, PSKs, BloomFilter) ->
- #offered_psks{
- identities = Identities,
- binders = Binders
- } = PSKs,
- decode_pre_shared_keys(Shard, IV, Identities, Binders, 0, BloomFilter, []).
-%%
-decode_pre_shared_keys(_, _, [], [], _, _, Acc) ->
- lists:reverse(Acc);
-decode_pre_shared_keys(Shard, IV, [I|Identities], [B|Binders], Index, BloomFilter, Acc) ->
- {Validity, PSK, Hash} = decode_identity(Shard, IV, I, BloomFilter),
- case Validity of
- valid ->
- decode_pre_shared_keys(Shard, IV, Identities, Binders, Index + 1, BloomFilter,
- [{PSK, Index, Hash, B}|Acc]);
- invalid ->
- decode_pre_shared_keys(Shard, IV, Identities, Binders, Index + 1, BloomFilter, Acc)
- end.
-
-
-decode_identity(Shard, IV, #psk_identity{
- identity = I,
- obfuscated_ticket_age = ObfAge}, BloomFilter) ->
- case ssl_cipher:decrypt_ticket(I, Shard, IV) of
- error ->
- %% Skip PSK if encrypted with an unknown key
- {invalid, undefined, undefined};
- #stateless_ticket{
- hash = Hash,
- pre_shared_key = PSK,
- ticket_age_add = TicketAgeAdd,
- lifetime = Lifetime,
- timestamp = Timestamp} ->
- Validity = check_ticket_validity(ObfAge, TicketAgeAdd, Lifetime, Timestamp, BloomFilter),
- {Validity, PSK, Hash}
- end.
-
-
-check_replay(_, _, undefined) ->
- anti_replay_disabled;
-check_replay(Tracker, Ticket, {_, _, _}) ->
- case tls_server_session_ticket:bloom_filter_contains(Tracker, Ticket) of
- false ->
- new_binder;
- true ->
- possible_replay
- end.
-
-
-%% For identities established externally, an obfuscated_ticket_age of 0 SHOULD be
-%% used, and servers MUST ignore the value.
-check_ticket_validity(0, _, _, _, _) ->
- valid;
-check_ticket_validity(ObfAge, TicketAgeAdd, Lifetime, Timestamp, BloomFilter) ->
- ReportedAge = ObfAge - TicketAgeAdd,
- RealAge = erlang:system_time(second) - Timestamp,
- case (ReportedAge > Lifetime) orelse
- (RealAge > Lifetime) orelse
- out_of_window(RealAge, BloomFilter) of
- true ->
- invalid;
- false ->
- valid
- end.
-
-
-%% 8.3. Freshness Checks
-out_of_window(_, undefined) ->
- false;
-out_of_window(Age, {Window, _, _}) ->
- Age > Window.
-
-
%% Prior to accepting PSK key establishment, the server MUST validate
%% the corresponding binder value (see Section 4.2.11.2 below). If this
%% value is not present or does not validate, the server MUST abort the
@@ -2206,64 +2105,13 @@ handle_pre_shared_key(_, undefined, _) ->
{ok, undefined};
handle_pre_shared_key(#state{ssl_options = #{session_tickets := disabled}}, _, _) ->
{ok, undefined};
-handle_pre_shared_key(#state{handshake_env = #handshake_env{ticket_seed = Seed},
- ssl_options = #{anti_replay := BloomFilter}} = State, PreSharedKeys, Cipher) ->
- {IV, Shard} =
- case Seed of
- {_, {IV0, Shard0}} ->
- {IV0, Shard0};
- IVS ->
- IVS
- end,
- PSKTuples = decode_pre_shared_keys(Shard, IV, PreSharedKeys, BloomFilter),
- case select_psk(PSKTuples, Cipher) of
- no_acceptable_psk ->
- {ok, undefined};
- PSKTuple ->
- validate_binder(State, PSKTuple, BloomFilter)
- end.
-
-select_psk([], _) ->
- no_acceptable_psk;
-select_psk([{PSK, Index, Hash, B}|T], Cipher) ->
- #{prf := CipherHash} = ssl_cipher_format:suite_bin_to_map(Cipher),
- case Hash of
- CipherHash ->
- {PSK, Index, Hash, B};
- _ ->
- select_psk(T, Cipher)
- end.
-
-validate_binder(#state{handshake_env = #handshake_env{tls_handshake_history = {HHistory, _}},
- static_env = #static_env{trackers = Trackers}},
- {PSK, Index, Hash, Binder}, BloomFilter) ->
+handle_pre_shared_key(#state{ssl_options = #{session_tickets := Tickets},
+ handshake_env = #handshake_env{tls_handshake_history = {HHistory, _}},
+ static_env = #static_env{trackers = Trackers}},
+ OfferedPreSharedKeys, Cipher) when Tickets =/= disabled ->
Tracker = proplists:get_value(session_tickets_tracker, Trackers),
- IsBinderValid =
- case HHistory of
- [ClientHello2, HRR, MessageHash|_] ->
- Truncated = truncate_client_hello(ClientHello2),
- FinishedKey = calculate_finished_key(PSK, Hash),
- Binder == calculate_binder(FinishedKey, Hash, [MessageHash, HRR, Truncated]);
- [ClientHello1|_] ->
- Truncated = truncate_client_hello(ClientHello1),
- FinishedKey = calculate_finished_key(PSK, Hash),
- Binder == calculate_binder(FinishedKey, Hash, Truncated)
- end,
- case IsBinderValid of
- true ->
- case check_replay(Tracker, Binder, BloomFilter) of
- anti_replay_disabled ->
- {ok, {Index, PSK}};
- new_binder ->
- tls_server_session_ticket:bloom_filter_add_elem(Tracker, Binder),
- {ok, {Index, PSK}};
- possible_replay ->
- %% Reject 0-RTT
- {ok, undefined}
- end;
- false ->
- {error, illegal_parameter}
- end.
+ #{prf := CipherHash} = ssl_cipher_format:suite_bin_to_map(Cipher),
+ tls_server_session_ticket:use(Tracker, OfferedPreSharedKeys, CipherHash, HHistory).
get_selected_group(#key_share_hello_retry_request{selected_group = SelectedGroup}) ->
SelectedGroup.
@@ -2405,23 +2253,6 @@ update_binders(#client_hello{extensions =
Extensions = Extensions0#{pre_shared_key => PreSharedKey},
Hello#client_hello{extensions = Extensions}.
-
-maybe_seed_session_tickets([], State) ->
- %% No PSK offered
- State;
-maybe_seed_session_tickets(_, #state{ssl_options = #{session_tickets := stateless},
- static_env = #static_env{trackers = Trackers},
- handshake_env = #handshake_env{ticket_seed = undefined} = HsEnv} = State0) ->
- %% First time fetch seed and first ticket to avoid unnecessary communication
- %% will be removed from state when sent.
- Tracker = proplists:get_value(session_tickets_tracker, Trackers),
- TicketSeed = tls_server_session_ticket:new_with_seed(Tracker),
- State0#state{handshake_env = HsEnv#handshake_env{ticket_seed = TicketSeed}};
-maybe_seed_session_tickets(_, #state{ssl_options = #{session_tickets := _}} = State) ->
- %% Stateful or disabled does not need a seed
- State.
-
-
%% Configure a suitable session ticket
maybe_automatic_session_resumption(#state{
ssl_options = #{versions := [Version|_],
diff --git a/lib/ssl/src/tls_server_session_ticket.erl b/lib/ssl/src/tls_server_session_ticket.erl
index 3b5c36d203..e30205f4b8 100644
--- a/lib/ssl/src/tls_server_session_ticket.erl
+++ b/lib/ssl/src/tls_server_session_ticket.erl
@@ -27,13 +27,14 @@
-include("tls_handshake_1_3.hrl").
-include("ssl_internal.hrl").
+-include("ssl_alert.hrl").
+-include("ssl_cipher.hrl").
%% API
-export([start_link/3,
- new/1,
- new_with_seed/1,
- bloom_filter_add_elem/2,
- bloom_filter_contains/2]).
+ new/3,
+ use/4
+ ]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
@@ -44,6 +45,7 @@
-record(state, {
stateless,
stateful,
+ nonce,
lifetime
}).
@@ -57,25 +59,18 @@
start_link(Mode, Lifetime, AntiReplay) ->
gen_server:start_link(?MODULE, [Mode, Lifetime, AntiReplay], []).
-new(Pid) ->
- gen_server:call(Pid, new_ticket, infinity).
-
-new_with_seed(Pid) ->
- gen_server:call(Pid, new_with_seed, infinity).
-
-bloom_filter_add_elem(Pid, Elem) ->
- gen_server:cast(Pid, {add_elem, Elem}).
-
-bloom_filter_contains(Pid, Elem) ->
- gen_server:call(Pid, {contains, Elem}, infinity).
+new(Pid, Prf, MasterSecret) ->
+ gen_server:call(Pid, {new_session_ticket, Prf, MasterSecret}, infinity).
+use(Pid, Identifiers, Prf, HandshakeHist) ->
+ gen_server:call(Pid, {use_ticket, Identifiers, Prf, HandshakeHist},
+ infinity).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
-spec init(Args :: term()) -> {ok, State :: term()}.
-
init(Args) ->
process_flag(trap_exit, true),
State = inital_state(Args),
@@ -83,34 +78,43 @@ init(Args) ->
-spec handle_call(Request :: term(), From :: {pid(), term()}, State :: term()) ->
{reply, Reply :: term(), NewState :: term()} .
-handle_call(new_ticket, _From, #state{stateless = #{nonce := Nonce} = Stateless} = State) ->
- Ticket = new_ticket(Nonce, State#state.lifetime),
- {reply, Ticket, State#state{stateless = Stateless#{nonce => Nonce + 1}}};
-handle_call(new_ticket, _From, State) ->
- Ticket = new_ticket(State),
- {reply, Ticket, State};
-handle_call(new_with_seed, _From, #state{stateless = #{nonce := Nonce, seed := Seed} = Stateless} = State) ->
- Ticket = new_ticket(Nonce, State#state.lifetime),
- {reply, {Ticket, Seed}, State#state{stateless = Stateless#{nonce => Nonce + 1}}};
-handle_call(new_with_seed, _From, #state{} = State) ->
- Ticket = new_ticket(State),
- {reply, {Ticket, no_seed}, State};
-handle_call({contains, Elem}, _From, #state{stateless = #{bloom_filter := BloomFilter}} = State) ->
- Reply = tls_bloom_filter:contains(BloomFilter, Elem),
- {reply, Reply, State}.
+handle_call({new_session_ticket, Prf, MasterSecret}, _From,
+ #state{nonce = Nonce,
+ lifetime = LifeTime,
+ stateful = #{}} = State0) ->
+ Id = stateful_psk_id(),
+ PSK = tls_v1:pre_shared_key(MasterSecret, ticket_nonce(Nonce), Prf),
+ SessionTicket = new_session_ticket(Id, Nonce, LifeTime),
+ State = stateful_ticket_store(Id, SessionTicket, Prf, PSK, State0),
+ {reply, SessionTicket, State};
+handle_call({new_session_ticket, Prf, MasterSecret}, _From,
+ #state{nonce = Nonce,
+ stateless = #{}} = State) ->
+ BaseSessionTicket = new_session_ticket_base(State),
+ SessionTicket = generate_statless_ticket(BaseSessionTicket, Prf,
+ MasterSecret, State),
+ {reply, SessionTicket, State#state{nonce = Nonce+1}};
+handle_call({use_ticket, Identifiers, Prf, HandshakeHist}, _From,
+ #state{stateful = #{}} = State0) ->
+ {Result, State} = stateful_use(Identifiers, Prf,
+ HandshakeHist, State0),
+ {reply, Result, State};
+handle_call({use_ticket, Identifiers, Prf, HandshakeHist}, _From,
+ #state{stateless = #{}} = State0) ->
+ {Result, State} = stateless_use(Identifiers, Prf,
+ HandshakeHist, State0),
+ {reply, Result, State}.
-spec handle_cast(Request :: term(), State :: term()) ->
- {noreply, NewState :: term()}.
-handle_cast({add_elem, Elem}, #state{stateless = #{bloom_filter := BloomFilter0} = Stateless} = State) ->
- BloomFilter = tls_bloom_filter:add_elem(BloomFilter0, Elem),
- {noreply, State#state{stateless = Stateless#{bloom_filter => BloomFilter}}};
+ {noreply, NewState :: term()}.
handle_cast(_Request, State) ->
{noreply, State}.
-spec handle_info(Info :: timeout() | term(), State :: term()) ->
- {noreply, NewState :: term()}.
-handle_info(rotate_bloom_filters, #state{stateless = #{bloom_filter := BloomFilter0,
- window := Window} = Stateless} = State) ->
+ {noreply, NewState :: term()}.
+handle_info(rotate_bloom_filters,
+ #state{stateless = #{bloom_filter := BloomFilter0,
+ 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}}};
@@ -125,7 +129,7 @@ terminate(_Reason, _State) ->
-spec code_change(OldVsn :: term() | {down, term()},
State :: term(),
Extra :: term()) -> {ok, NewState :: term()} |
- {error, Reason :: term()}.
+ {error, Reason :: term()}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
@@ -138,26 +142,32 @@ format_status(_Opt, Status) ->
%%% Internal functions
%%%===================================================================
-
inital_state([stateless, Lifetime, undefined]) ->
- #state{stateless = #{nonce => 0,
- seed => {crypto:strong_rand_bytes(16),
- crypto:strong_rand_bytes(32)}},
+ #state{nonce = 0,
+ stateless = #{seed => {crypto:strong_rand_bytes(16),
+ crypto:strong_rand_bytes(32)},
+ window => undefined},
lifetime = Lifetime
};
inital_state([stateless, Lifetime, {Window, K, M}]) ->
erlang:send_after(Window * 1000, self(), rotate_bloom_filters),
- #state{stateless = #{bloom_filter => tls_bloom_filter:new(K, M),
- nonce => 0,
+ #state{nonce = 0,
+ stateless = #{bloom_filter => tls_bloom_filter:new(K, M),
seed => {crypto:strong_rand_bytes(16),
crypto:strong_rand_bytes(32)},
- windows => Window},
+ window => Window},
lifetime = Lifetime
};
-inital_state([stateful, Lifetime]) ->
+inital_state([stateful, Lifetime|_]) ->
+ %% statfeful servers replay
+ %% protection is that it saves
+ %% all valid tickets
#state{lifetime = Lifetime,
- stateful = #{db => gb_trees:empty(),
- max => 1000}
+ nonce = 0,
+ stateful = #{db => stateful_store(),
+ max => 1000,
+ ref_index => #{}
+ }
}.
ticket_age_add() ->
@@ -175,14 +185,207 @@ ticket_age_add() ->
ticket_nonce(I) ->
<<?UINT64(I)>>.
-new_ticket(Nonce, Lifetime) ->
+new_session_ticket_base(#state{nonce = Nonce,
+ lifetime = Lifetime}) ->
+ new_session_ticket(undefined, Nonce, Lifetime).
+
+new_session_ticket(Id, Nonce, Lifetime) ->
TicketAgeAdd = ticket_age_add(),
#new_session_ticket{
+ ticket = Id,
ticket_lifetime = Lifetime,
ticket_age_add = TicketAgeAdd,
ticket_nonce = ticket_nonce(Nonce),
extensions = #{}
}.
-new_ticket(_) ->
- #new_session_ticket{}.
+
+validate_binder(Binder, HandshakeHist, PSK, Prf, AlertDetail) ->
+ case tls_handshake_1_3:is_valid_binder(Binder, HandshakeHist, PSK, Prf) of
+ true ->
+ true;
+ false ->
+ {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER, AlertDetail)}
+ end.
+
+%%%===================================================================
+%%% Stateful store
+%%%===================================================================
+
+stateful_store() ->
+ gb_trees:empty().
+
+stateful_ticket_store(Ref, NewSessionTicket, Hash, Psk,
+ #state{nonce = Nonce,
+ stateful = #{db := Tree0,
+ max := Max,
+ ref_index := Index0} = Stateful}
+ = State0) ->
+ Id = erlang:monotonic_time(),
+ StatefulTicket = {NewSessionTicket, Hash, Psk},
+ case gb_trees:size(Tree0) of
+ Max ->
+ %% Trow away oldes ticket
+ {_, {#new_session_ticket{ticket = OldRef},_,_}, Tree1}
+ = gb_trees:take_smallest(Tree0),
+ Tree = gb_trees:insert(Id, StatefulTicket, Tree1),
+ Index = maps:without([OldRef], Index0),
+ State0#state{nonce = Nonce+1, stateful =
+ Stateful#{db => Tree,
+ ref_index => Index#{Ref => Id}}};
+ _ ->
+ Tree = gb_trees:insert(Id, StatefulTicket, Tree0),
+ State0#state{nonce = Nonce+1, stateful =
+ Stateful#{db => Tree,
+ ref_index => Index0#{Ref => Id}}}
+ end.
+
+stateful_use(#offered_psks{
+ identities = Identities,
+ binders = Binders
+ }, Prf, HandshakeHist, State) ->
+ stateful_use(Identities, Binders, Prf, HandshakeHist, 0, State).
+
+stateful_use([], [], _, _, _, State) ->
+ {{ok, undefined}, State};
+stateful_use([#psk_identity{identity = Ref} | Refs], [Binder | Binders],
+ Prf, HandshakeHist, Index,
+ #state{stateful = #{db := Tree0,
+ ref_index := RefIndex0} = Stateful} = State) ->
+ try maps:get(Ref, RefIndex0) of
+ Key ->
+ case stateful_usable_ticket(Key, Prf, Binder,
+ HandshakeHist, Tree0) of
+ true ->
+ RefIndex = maps:without([Ref], RefIndex0),
+ {{_,_, PSK}, Tree} = gb_trees:take(Key, Tree0),
+ {{ok, {Index, PSK}},
+ State#state{stateful = Stateful#{db => Tree,
+ ref_index => RefIndex}}};
+ false ->
+ stateful_use(Refs, Binders, Prf,
+ HandshakeHist, Index + 1, State);
+ {error, _} = Error ->
+ {Error, State}
+ end
+ catch
+ _:{badkey, Ref} ->
+ stateful_use(Refs, Binders, Prf, HandshakeHist, Index + 1, State)
+ end.
+
+stateful_usable_ticket(Key, Prf, Binder, HandshakeHist, Tree) ->
+ case gb_trees:lookup(Key, Tree) of
+ none ->
+ false;
+ {value, {NewSessionTicket, Prf, PSK}} ->
+ case stateful_living_ticket(Key, NewSessionTicket) of
+ true ->
+ validate_binder(Binder, HandshakeHist, PSK, Prf, stateful);
+ _ ->
+ false
+ end;
+ _ ->
+ false
+ end.
+
+stateful_living_ticket(TimeStamp,
+ #new_session_ticket{ticket_lifetime = LifeTime}) ->
+ Now = erlang:monotonic_time(),
+ Lived = erlang:convert_time_unit(Now-TimeStamp, native, seconds),
+ Lived < LifeTime.
+
+
+stateful_psk_id() ->
+ term_to_binary(make_ref()).
+
+%%%===================================================================
+%%% Stateless ticket
+%%%===================================================================
+generate_statless_ticket(#new_session_ticket{ticket_nonce = Nonce,
+ ticket_age_add = TicketAgeAdd,
+ ticket_lifetime = Lifetime}
+ = Ticket, Prf, MasterSecret,
+ #state{stateless = #{seed := {IV, Shard}}}) ->
+ PSK = tls_v1:pre_shared_key(MasterSecret, Nonce, Prf),
+ Timestamp = erlang:system_time(second),
+ Encrypted = ssl_cipher:encrypt_ticket(#stateless_ticket{
+ hash = Prf,
+ pre_shared_key = PSK,
+ ticket_age_add = TicketAgeAdd,
+ lifetime = Lifetime,
+ timestamp = Timestamp
+ }, Shard, IV),
+ Ticket#new_session_ticket{ticket = Encrypted}.
+
+stateless_use(#offered_psks{
+ identities = Identities,
+ binders = Binders
+ }, Prf, HandshakeHist, State) ->
+ stateless_use(Identities, Binders, Prf, HandshakeHist, 0, State).
+
+stateless_use([], [], _, _, _, State) ->
+ {{ok, undefined}, State};
+stateless_use([#psk_identity{identity = Encrypted,
+ obfuscated_ticket_age = ObfAge} | Ids],
+ [Binder | Binders], Prf, HandshakeHist, Index,
+ #state{stateless = #{seed := {IV, Shard},
+ window := Window}} = State) ->
+ case ssl_cipher:decrypt_ticket(Encrypted, Shard, IV) of
+ #stateless_ticket{hash = Prf,
+ pre_shared_key = PSK} = Ticket ->
+ case statless_usable_ticket(Ticket, ObfAge, Binder,
+ HandshakeHist, Window) of
+ true ->
+ stateless_anti_replay(Index, PSK, Binder, State);
+ false ->
+ stateless_use(Ids, Binders, Prf, HandshakeHist,
+ Index+1, State);
+ {error, _} = Error ->
+ {Error, State}
+ end;
+ _ ->
+ stateless_use(Ids, Binders, Prf, HandshakeHist, Index+1, State)
+ end.
+
+statless_usable_ticket(#stateless_ticket{hash = Prf,
+ ticket_age_add = TicketAgeAdd,
+ lifetime = Lifetime,
+ timestamp = Timestamp,
+ pre_shared_key = PSK}, ObfAge,
+ Binder, HandshakeHist, Window) ->
+ case stateless_living_ticket(ObfAge, TicketAgeAdd, Lifetime,
+ Timestamp, Window) of
+ true ->
+ validate_binder(Binder, HandshakeHist, PSK, Prf, stateless);
+ false ->
+ false
+ end.
+
+stateless_living_ticket(0, _, _, _, _) ->
+ true;
+stateless_living_ticket(ObfAge, TicketAgeAdd, Lifetime, Timestamp, Window) ->
+ ReportedAge = ObfAge - TicketAgeAdd,
+ RealAge = erlang:system_time(second) - Timestamp,
+ (ReportedAge =< Lifetime)
+ andalso (RealAge =< Lifetime)
+ andalso (in_window(RealAge, Window)).
+
+in_window(_, undefined) ->
+ true;
+in_window(Age, {Window, _, _}) ->
+ Age =< Window.
+
+stateless_anti_replay(Index, PSK, Binder,
+ #state{stateless = #{bloom_filter := BloomFilter0}
+ = Stateless} = State) ->
+ case tls_bloom_filter:contains(BloomFilter0, Binder) of
+ true ->
+ %%possible_replay
+ {{ok, undefined}, State};
+ false ->
+ BloomFilter = tls_bloom_filter:add_elem(BloomFilter0, Binder),
+ {{ok, {Index, PSK}},
+ State#state{stateless = Stateless#{bloom_filter => BloomFilter}}}
+ end;
+stateless_anti_replay(Index, PSK, _, State) ->
+ {{ok, {Index, PSK}}, State}.
diff --git a/lib/ssl/src/tls_socket.erl b/lib/ssl/src/tls_socket.erl
index ca97e91688..031665bfe8 100644
--- a/lib/ssl/src/tls_socket.erl
+++ b/lib/ssl/src/tls_socket.erl
@@ -78,7 +78,7 @@ listen(Transport, Port, #config{transport_info = {Transport, _, _, _, _},
{ok, ListenSocket} ->
{ok, Tracker} = inherit_tracker(ListenSocket, EmOpts, SslOpts),
%% TODO not hard code
- {ok, SessionHandler} = session_tickets_tracker(stateless, 7200, SslOpts),
+ {ok, SessionHandler} = session_tickets_tracker(7200, SslOpts),
Trackers = [{option_tracker, Tracker}, {session_tickets_tracker, SessionHandler}],
Socket = #sslsocket{pid = {ListenSocket, Config#config{trackers = Trackers}}},
check_active_n(EmOpts, Socket),
@@ -249,10 +249,14 @@ inherit_tracker(ListenSocket, EmOpts, #{erl_dist := false} = SslOpts) ->
inherit_tracker(ListenSocket, EmOpts, #{erl_dist := true} = SslOpts) ->
ssl_listen_tracker_sup:start_child_dist([ListenSocket, EmOpts, SslOpts]).
-session_tickets_tracker(Mode, Lifetime, #{erl_dist := false,
- anti_replay := AntiReplay}) ->
+session_tickets_tracker(_, #{erl_dist := false,
+ session_tickets := disabled}) ->
+ {ok, disabled};
+session_tickets_tracker(Lifetime, #{erl_dist := false,
+ session_tickets := Mode,
+ anti_replay := AntiReplay}) ->
tls_server_session_ticket_sup:start_child([Mode, Lifetime, AntiReplay]);
-session_tickets_tracker(Mode, Lifetime, #{erl_dist := true}) ->
+session_tickets_tracker(Lifetime, #{erl_dist := true, session_tickets := Mode}) ->
tls_server_session_ticket_sup:start_child_dist([Mode, Lifetime]).
diff --git a/lib/ssl/test/ssl_session_ticket_SUITE.erl b/lib/ssl/test/ssl_session_ticket_SUITE.erl
index 78cae6fab3..83e8e3214d 100644
--- a/lib/ssl/test/ssl_session_ticket_SUITE.erl
+++ b/lib/ssl/test/ssl_session_ticket_SUITE.erl
@@ -40,17 +40,20 @@ all() ->
].
groups() ->
- [{'tlsv1.3', [], session_tests()}].
+ [{'tlsv1.3', [], [{group, stateful}, {group, stateless}, {group, openssl_server}]},
+ {openssl_server, [], [erlang_client_openssl_server_basic,
+ erlang_client_openssl_server_hrr,
+ erlang_client_openssl_server_hrr_multiple_tickets
+ ]},
+ {stateful, [], session_tests()},
+ {stateless, [], session_tests()}].
session_tests() ->
[erlang_client_erlang_server_basic,
- erlang_client_openssl_server_basic,
openssl_client_erlang_server_basic,
erlang_client_erlang_server_hrr,
- erlang_client_openssl_server_hrr,
openssl_client_erlang_server_hrr,
erlang_client_erlang_server_multiple_tickets,
- erlang_client_openssl_server_hrr_multiple_tickets,
erlang_client_erlang_server_multiple_tickets_2hash].
init_per_suite(Config0) ->
@@ -58,8 +61,7 @@ init_per_suite(Config0) ->
try crypto:start() of
ok ->
ssl_test_lib:clean_start(),
- Config = ssl_test_lib:make_rsa_cert(Config0),
- ssl_test_lib:make_dsa_cert(Config)
+ ssl_test_lib:make_rsa_cert(Config0)
catch _:_ ->
{skip, "Crypto did not start"}
end.
@@ -68,6 +70,10 @@ end_per_suite(_Config) ->
ssl:stop(),
application:stop(crypto).
+init_per_group(stateful, Config) ->
+ [{server_ticket_mode, stateful} | proplists:delete(server_ticket_mode, Config)];
+init_per_group(stateless, Config) ->
+ [{server_ticket_mode, stateless} | proplists:delete(server_ticket_mode, Config)];
init_per_group(GroupName, Config) ->
ssl_test_lib:clean_tls_version(Config),
case ssl_test_lib:is_tls_version(GroupName) andalso ssl_test_lib:sufficient_crypto_support(GroupName) of
@@ -112,11 +118,12 @@ erlang_client_erlang_server_basic(Config) when is_list(Config) ->
ClientOpts0 = ssl_test_lib:ssl_options(client_rsa_verify_opts, Config),
ServerOpts0 = ssl_test_lib:ssl_options(server_rsa_verify_opts, Config),
{ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config),
+ ServerTicketMode = proplists:get_value(server_ticket_mode, Config),
%% Configure session tickets
ClientOpts = [{session_tickets, auto}, {log_level, debug},
{versions, ['tlsv1.2','tlsv1.3']}|ClientOpts0],
- ServerOpts = [{session_tickets, stateless}, {log_level, debug},
+ ServerOpts = [{session_tickets, ServerTicketMode}, {log_level, debug},
{versions, ['tlsv1.2','tlsv1.3']}|ServerOpts0],
Server0 =
@@ -221,11 +228,12 @@ openssl_client_erlang_server_basic(Config) when is_list(Config) ->
{_, ServerNode, Hostname} = ssl_test_lib:run_where(Config),
TicketFile0 = filename:join([proplists:get_value(priv_dir, Config), "session_ticket0"]),
TicketFile1 = filename:join([proplists:get_value(priv_dir, Config), "session_ticket1"]),
+ ServerTicketMode = proplists:get_value(server_ticket_mode, Config),
Data = "Hello world",
%% Configure session tickets
- ServerOpts = [{session_tickets, stateless}, {log_level, debug},
+ ServerOpts = [{session_tickets, ServerTicketMode}, {log_level, debug},
{versions, ['tlsv1.2','tlsv1.3']}|ServerOpts0],
Server0 =
@@ -282,12 +290,13 @@ erlang_client_erlang_server_hrr(Config) when is_list(Config) ->
ClientOpts0 = ssl_test_lib:ssl_options(client_rsa_verify_opts, Config),
ServerOpts0 = ssl_test_lib:ssl_options(server_rsa_verify_opts, Config),
{ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config),
-
+ ServerTicketMode = proplists:get_value(server_ticket_mode, Config),
+
%% Configure session tickets
ClientOpts = [{session_tickets, auto}, {log_level, debug},
{versions, ['tlsv1.2','tlsv1.3']},
{supported_groups,[secp256r1, x25519]}|ClientOpts0],
- ServerOpts = [{session_tickets, stateless}, {log_level, debug},
+ ServerOpts = [{session_tickets, ServerTicketMode}, {log_level, debug},
{versions, ['tlsv1.2','tlsv1.3']},
{supported_groups, [x448, x25519]}|ServerOpts0],
@@ -398,11 +407,12 @@ openssl_client_erlang_server_hrr(Config) when is_list(Config) ->
{_, ServerNode, Hostname} = ssl_test_lib:run_where(Config),
TicketFile0 = filename:join([proplists:get_value(priv_dir, Config), "session_ticket0"]),
TicketFile1 = filename:join([proplists:get_value(priv_dir, Config), "session_ticket1"]),
+ ServerTicketMode = proplists:get_value(server_ticket_mode, Config),
Data = "Hello world",
%% Configure session tickets
- ServerOpts = [{session_tickets, stateless}, {log_level, debug},
+ ServerOpts = [{session_tickets, ServerTicketMode}, {log_level, debug},
{versions, ['tlsv1.2','tlsv1.3']},
{supported_groups,[x448, x25519]}|ServerOpts0],
@@ -462,11 +472,12 @@ erlang_client_erlang_server_multiple_tickets(Config) when is_list(Config) ->
ClientOpts0 = ssl_test_lib:ssl_options(client_rsa_verify_opts, Config),
ServerOpts0 = ssl_test_lib:ssl_options(server_rsa_verify_opts, Config),
{ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config),
+ ServerTicketMode = proplists:get_value(server_ticket_mode, Config),
%% Configure session tickets
ClientOpts = [{session_tickets, enabled}, {log_level, debug},
{versions, ['tlsv1.2','tlsv1.3']}|ClientOpts0],
- ServerOpts = [{session_tickets, stateless}, {log_level, debug},
+ ServerOpts = [{session_tickets, ServerTicketMode}, {log_level, debug},
{versions, ['tlsv1.2','tlsv1.3']}|ServerOpts0],
Server0 =
@@ -582,11 +593,12 @@ erlang_client_erlang_server_multiple_tickets_2hash(Config) when is_list(Config)
ClientOpts0 = ssl_test_lib:ssl_options(client_rsa_verify_opts, Config),
ServerOpts0 = ssl_test_lib:ssl_options(server_rsa_verify_opts, Config),
{ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config),
-
+ ServerTicketMode = proplists:get_value(server_ticket_mode, Config),
+
%% Configure session tickets
ClientOpts = [{session_tickets, enabled}, {log_level, debug},
{versions, ['tlsv1.2','tlsv1.3']}|ClientOpts0],
- ServerOpts = [{session_tickets, stateless}, {log_level, debug},
+ ServerOpts = [{session_tickets, ServerTicketMode}, {log_level, debug},
{versions, ['tlsv1.2','tlsv1.3']}|ServerOpts0],
Server0 =