diff options
Diffstat (limited to 'lib/ssl/src/tls_server_session_ticket.erl')
-rw-r--r-- | lib/ssl/src/tls_server_session_ticket.erl | 241 |
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}. |