%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2007-2021. 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. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %% %%---------------------------------------------------------------------- %% Purpose: Handle server side TLS-1.3 session ticket storage %%---------------------------------------------------------------------- -module(tls_server_session_ticket). -behaviour(gen_server). -include("tls_handshake_1_3.hrl"). -include("ssl_internal.hrl"). -include("ssl_alert.hrl"). -include("ssl_cipher.hrl"). %% API -export([start_link/6, new/3, use/4 ]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, format_status/2]). -define(SERVER, ?MODULE). -record(state, { stateless, stateful, nonce, lifetime, max_early_data_size, listen_monitor }). %%%=================================================================== %%% API %%%=================================================================== -spec start_link(term(), atom(), integer(), integer(), integer(), tuple()) -> {ok, Pid :: pid()} | {error, Error :: {already_started, pid()}} | {error, Error :: term()} | ignore. start_link(Listner, Mode, Lifetime, TicketStoreSize, MaxEarlyDataSize, AntiReplay) -> gen_server:start_link(?MODULE, [Listner, Mode, Lifetime, TicketStoreSize, MaxEarlyDataSize, AntiReplay], []). 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([Listener | Args]) -> process_flag(trap_exit, true), Monitor = inet:monitor(Listener), State = inital_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, #state{nonce = Nonce, lifetime = LifeTime, max_early_data_size = MaxEarlyDataSize, stateful = #{id_generator := IdGen}} = State0) -> 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), {reply, SessionTicket, State}; handle_call({new_session_ticket, Prf, MasterSecret}, _From, #state{nonce = Nonce, stateless = #{}} = State) -> BaseSessionTicket = new_session_ticket_base(State), SessionTicket = generate_stateless_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(_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) -> BloomFilter = tls_bloom_filter:rotate(BloomFilter0), erlang:send_after(Window * 1000, self(), rotate_bloom_filters), {noreply, State#state{stateless = Stateless#{bloom_filter => BloomFilter}}}; handle_info({'DOWN', Monitor, _, _, _}, #state{listen_monitor = Monitor} = State) -> {stop, normal, State}; handle_info(_Info, State) -> {noreply, State}. -spec terminate(Reason :: normal | shutdown | {shutdown, term()} | term(), State :: term()) -> any(). terminate(_Reason, _State) -> ok. -spec code_change(OldVsn :: term() | {down, term()}, State :: term(), Extra :: term()) -> {ok, NewState :: term()} | {error, Reason :: term()}. code_change(_OldVsn, State, _Extra) -> {ok, State}. -spec format_status(Opt :: normal | terminate, Status :: list()) -> Status :: term(). format_status(_Opt, Status) -> Status. %%%=================================================================== %%% Internal functions %%%=================================================================== inital_state([stateless, Lifetime, _, MaxEarlyDataSize, undefined]) -> #state{nonce = 0, stateless = #{seed => {crypto:strong_rand_bytes(16), crypto:strong_rand_bytes(32)}, window => undefined}, lifetime = Lifetime, max_early_data_size = MaxEarlyDataSize }; inital_state([stateless, Lifetime, _, MaxEarlyDataSize, {Window, K, M}]) -> 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)}, window => Window}, lifetime = Lifetime, max_early_data_size = MaxEarlyDataSize }; inital_state([stateful, Lifetime, TicketStoreSize, MaxEarlyDataSize|_]) -> %% statfeful servers replay %% protection is that it saves %% all valid tickets #state{lifetime = Lifetime, max_early_data_size = MaxEarlyDataSize, nonce = 0, stateful = #{db => stateful_store(), max => TicketStoreSize, ref_index => #{}, id_generator => crypto:strong_rand_bytes(16) } }. ticket_age_add() -> MaxTicketAge = 7 * 24 * 3600, IntMax = round(math:pow(2,32)) - 1, MaxAgeAdd = IntMax - MaxTicketAge, <> = crypto:strong_rand_bytes(4), case I > MaxAgeAdd of true -> I - MaxTicketAge; false -> I end. ticket_nonce(I) -> <>. new_session_ticket_base(#state{nonce = Nonce, lifetime = Lifetime, max_early_data_size = MaxEarlyDataSize}) -> new_session_ticket(undefined, Nonce, Lifetime, MaxEarlyDataSize). new_session_ticket(Id, Nonce, Lifetime, MaxEarlyDataSize) -> TicketAgeAdd = ticket_age_add(), Extensions = #{early_data => #early_data_indication_nst{ indication = MaxEarlyDataSize}}, #new_session_ticket{ ticket = Id, ticket_lifetime = Lifetime, ticket_age_add = TicketAgeAdd, ticket_nonce = ticket_nonce(Nonce), extensions = Extensions }. 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(), erlang:unique_integer([monotonic])}, 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_ticket_id(Key) -> Unique = erlang:unique_integer(), %% Obfuscate to avoid DoS attack possiblities %% that could invalidate tickets and render them %% unusable. This id should be unpredictable %% and unique but have no other cryptographic requirements. crypto:crypto_one_time(aes_128_ecb, Key, <>, true). %%%=================================================================== %%% Stateless ticket %%%=================================================================== generate_stateless_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 stateless_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. stateless_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) when is_integer(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}.