diff options
author | Ingela Anderton Andin <ingela@erlang.org> | 2019-11-21 11:06:47 +0100 |
---|---|---|
committer | Ingela Anderton Andin <ingela@erlang.org> | 2019-11-21 11:06:47 +0100 |
commit | 4d105778fefc48e132248890b80e6eac999798eb (patch) | |
tree | 31243d85d7585724be2efd68c1858cdb0bb1e4c0 /lib | |
parent | 81d9f5a1c66ced5be94246dcc14f8902ce476c86 (diff) | |
parent | cfad165ba34b3d50ecb0b89783752045dcd76081 (diff) | |
download | erlang-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.erl | 237 | ||||
-rw-r--r-- | lib/ssl/src/tls_server_session_ticket.erl | 307 | ||||
-rw-r--r-- | lib/ssl/src/tls_socket.erl | 12 | ||||
-rw-r--r-- | lib/ssl/test/ssl_session_ticket_SUITE.erl | 40 |
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 = |