Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions lib/ssl/doc/src/ssl.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1461,15 +1461,34 @@ fun(srp, Username :: binary(), UserState :: term()) ->
<datatype>
<name name="server_session_tickets"/>
<desc>
<p>Configures the session ticket functionality. Allowed values are <c>disabled</c>,
<c>stateful</c> and <c>stateless</c>.</p>
<p>If it is set to <c>stateful</c> or
<c>stateless</c>, session resumption with pre-shared keys is enabled and the server will
send stateful or stateless session tickets to the client after successful connections.</p>
<p>Configures the session ticket functionality. Allowed values are <c>disabled</c>,
<c>stateful</c>, <c>stateless</c>, <c>stateful_with_cert</c>, <c>stateless_with_cert</c>.</p>
<p>If it is not set to <c>disabled</c>,
session resumption with pre-shared keys is enabled and the server will
send stateful or stateless session tickets to the client after successful connections.</p>

<note><p>
Pre-shared key session ticket resumption does not include any certificate exchange,
hence the function <seemfa marker="ssl:ssl#peercert/1">ssl:peercert/1</seemfa> will not
be able to return the peer certificate as it is only communicated in the initial handshake.
The server options <c>stateful_with_cert</c> or <c>stateless_with_cert</c> may be used
to make a server associate the client certificate from the original handshake
with the tickets it issues.
</p></note>

<p>A stateful session ticket is a database reference to internal state information.
A stateless session ticket is a self-encrypted binary that contains both cryptographic keying
material and state data.
</p>

<warning><p>
If it is set to <c>stateful_with_cert</c> the client certificate
is stored with the internal state information, increasing memory consumption.
If it is set to <c>stateless_with_cert</c> the client certificate is
encoded in the self-encrypted binary that is sent to the client,
increasing the payload size.
</p></warning>

<note><p>This option is supported by TLS 1.3 and above. See also
<seeguide marker="ssl:using_ssl#session-tickets-and-session-resumption-in-tls-1.3">
SSL's Users Guide, Session Tickets and Session Resumption in TLS 1.3</seeguide>
Expand Down
15 changes: 11 additions & 4 deletions lib/ssl/src/ssl.erl
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@
-type log_alert() :: boolean().
-type logging_level() :: logger:level() | none | all.
-type client_session_tickets() :: disabled | manual | auto.
-type server_session_tickets() :: disabled | stateful | stateless.
-type server_session_tickets() :: disabled | stateful | stateless | stateful_with_cert | stateless_with_cert.
-type session_tickets() :: client_session_tickets() | server_session_tickets().
-type key_update_at() :: pos_integer().
-type bloom_filter_window_size() :: integer().
Expand Down Expand Up @@ -1899,17 +1899,24 @@ opt_tickets(UserOpts, #{versions := Versions} = Opts, #{role := client}) ->
assert_server_only(stateless_tickets_seed, UserOpts),
Opts#{session_tickets => SessionTickets, use_ticket => UseTicket, early_data => EarlyData};
opt_tickets(UserOpts, #{versions := Versions} = Opts, #{role := server}) ->
{_, SessionTickets} = get_opt_of(session_tickets, [disabled, stateful, stateless], disabled, UserOpts, Opts),
{_, SessionTickets} =
get_opt_of(session_tickets,
[disabled, stateful, stateless, stateful_with_cert, stateless_with_cert],
disabled,
UserOpts,
Opts),
assert_version_dep(SessionTickets =/= disabled, session_tickets, Versions, ['tlsv1.3']),

{_, EarlyData} = get_opt_of(early_data, [enabled, disabled], disabled, UserOpts, Opts),
option_incompatible(SessionTickets =:= disabled andalso EarlyData =:= enabled,
[early_data, {session_tickets, disabled}]),

Stateless = lists:member(SessionTickets, [stateless, stateless_with_cert]),

AntiReplay =
case get_opt(anti_replay, undefined, UserOpts, Opts) of
{_, undefined} -> undefined;
{_,AR} when SessionTickets =/= stateless ->
{_,AR} when not Stateless ->
option_incompatible([{anti_replay, AR}, {session_tickets, SessionTickets}]);
{_,'10k'} -> {10, 5, 72985}; %% n = 10000 p = 0.030003564 (1 in 33) m = 72985 (8.91KiB) k = 5
{_,'100k'} -> {10, 5, 729845}; %% n = 10000 p = 0.03000428 (1 in 33) m = 729845 (89.09KiB) k = 5
Expand All @@ -1918,7 +1925,7 @@ opt_tickets(UserOpts, #{versions := Versions} = Opts, #{role := server}) ->
end,

{_, STS} = get_opt_bin(stateless_tickets_seed, undefined, UserOpts, Opts),
option_incompatible(STS =/= undefined andalso SessionTickets =/= stateless,
option_incompatible(STS =/= undefined andalso not Stateless,
[stateless_tickets_seed, {session_tickets, SessionTickets}]),

assert_client_only(use_ticket, UserOpts),
Expand Down
26 changes: 22 additions & 4 deletions lib/ssl/src/ssl_cipher.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1306,10 +1306,22 @@ encrypt_ticket(#stateless_ticket{
pre_shared_key = PSK,
ticket_age_add = TicketAgeAdd,
lifetime = Lifetime,
timestamp = Timestamp
timestamp = Timestamp,
certificate = Certificate
}, Shard, IV) ->
Plaintext = <<(ssl_cipher:hash_algorithm(Hash)):8,PSK/binary,
Plaintext1 = <<(ssl_cipher:hash_algorithm(Hash)):8,PSK/binary,
?UINT64(TicketAgeAdd),?UINT32(Lifetime),?UINT32(Timestamp)>>,
CertificateLength = case Certificate of
undefined -> 0;
_ -> byte_size(Certificate)
end,
Plaintext = case CertificateLength of
0 ->
<<Plaintext1/binary,?UINT16(0)>>;
_ ->
<<Plaintext1/binary,?UINT16(CertificateLength),
Certificate/binary>>
end,
encrypt_ticket_data(Plaintext, Shard, IV).


Expand All @@ -1321,13 +1333,19 @@ decrypt_ticket(CipherFragment, Shard, IV) ->
<<?BYTE(HKDF),T/binary>> = Plaintext,
Hash = hash_algorithm(HKDF),
HashSize = hash_size(Hash),
<<PSK:HashSize/binary,?UINT64(TicketAgeAdd),?UINT32(Lifetime),?UINT32(Timestamp),_/binary>> = T,
<<PSK:HashSize/binary,?UINT64(TicketAgeAdd),?UINT32(Lifetime),?UINT32(Timestamp),
?UINT16(CertificateLength),Certificate1:CertificateLength/binary,_/binary>> = T,
Certificate = case CertificateLength of
0 -> undefined;
_ -> Certificate1
end,
#stateless_ticket{
hash = Hash,
pre_shared_key = PSK,
ticket_age_add = TicketAgeAdd,
lifetime = Lifetime,
timestamp = Timestamp
timestamp = Timestamp,
certificate = Certificate
}
end.

Expand Down
3 changes: 2 additions & 1 deletion lib/ssl/src/ssl_cipher.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
pre_shared_key,
ticket_age_add,
lifetime,
timestamp
timestamp,
certificate
}).

%%% SSL cipher protocol %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Expand Down
33 changes: 26 additions & 7 deletions lib/ssl/src/tls_handshake_1_3.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1316,22 +1316,27 @@ session_resumption({#state{ssl_options = #{session_tickets := Tickets}} = State,
{ok, {State, negotiated}};
session_resumption({#state{ssl_options = #{session_tickets := Tickets},
handshake_env = #handshake_env{
early_data_accepted = false}} = State0, negotiated}, PSK)
early_data_accepted = false}} = State0, negotiated}, PSK0)
when Tickets =/= disabled ->
State = handle_resumption(State0, ok),
State1 = handle_resumption(State0, ok),
{Index, PSK1, PeerCert} = PSK0,
PSK = {Index, PSK1},
State = maybe_store_peer_cert(State1, PeerCert),
{ok, {State, negotiated, PSK}};
session_resumption({#state{ssl_options = #{session_tickets := Tickets},
handshake_env = #handshake_env{
early_data_accepted = true}} = State0, negotiated}, PSK0)
when Tickets =/= disabled ->
State1 = handle_resumption(State0, ok),
%% TODO Refactor PSK-tuple {Index, PSK}, index might not be needed.
{_ , PSK} = PSK0,
State2 = calculate_client_early_traffic_secret(State1, PSK),
{Index, PSK1, PeerCert} = PSK0,
State2 = calculate_client_early_traffic_secret(State1, PSK1),
PSK = {Index, PSK1},
%% Set 0-RTT traffic keys for reading early_data
State3 = ssl_record:step_encryption_state_read(State2),
State = update_current_read(State3, true, true),
{ok, {State, negotiated, PSK0}}.
State4 = maybe_store_peer_cert(State3, PeerCert),
State = update_current_read(State4, true, true),
{ok, {State, negotiated, PSK}}.

%% Session resumption with early_data
maybe_send_certificate_request(#state{
Expand Down Expand Up @@ -1410,10 +1415,19 @@ maybe_send_session_ticket(#state{connection_states = ConnectionStates,
ssl_record:current_connection_state(ConnectionStates, read),
#security_parameters{prf_algorithm = HKDF,
resumption_master_secret = RMS} = SecParamsR,
Ticket = tls_server_session_ticket:new(Tracker, HKDF, RMS),
Ticket = new_session_ticket(Tracker, HKDF, RMS, State0),
{State, _} = Connection:send_handshake(Ticket, State0),
maybe_send_session_ticket(State, N - 1).

new_session_ticket(Tracker, HKDF, RMS, #state{ssl_options = #{session_tickets := stateful_with_cert},
session = #session{peer_certificate = PeerCert}}) ->
tls_server_session_ticket:new(Tracker, HKDF, RMS, PeerCert);
new_session_ticket(Tracker, HKDF, RMS, #state{ssl_options = #{session_tickets := stateless_with_cert},
session = #session{peer_certificate = PeerCert}}) ->
tls_server_session_ticket:new(Tracker, HKDF, RMS, PeerCert);
new_session_ticket(Tracker, HKDF, RMS, _) ->
tls_server_session_ticket:new(Tracker, HKDF, RMS, undefined).

create_change_cipher_spec(#state{ssl_options = #{log_level := LogLevel}}) ->
%% Dummy connection_states with NULL cipher
ConnectionStates =
Expand Down Expand Up @@ -1515,6 +1529,11 @@ validate_certificate_chain(CertEntries, CertDbHandle, CertDbRef,
ocsp_responder_certs => OcspResponderCerts}).


maybe_store_peer_cert(State, undefined) ->
State;
maybe_store_peer_cert(#state{session = Session} = State, PeerCert) ->
State#state{session = Session#session{peer_certificate = PeerCert}}.

store_peer_cert(#state{session = Session,
handshake_env = HsEnv} = State, PeerCert, PublicKeyInfo) ->
State#state{session = Session#session{peer_certificate = PeerCert},
Expand Down
55 changes: 31 additions & 24 deletions lib/ssl/src/tls_server_session_ticket.erl
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

%% API
-export([start_link/7,
new/3,
new/4,
use/4
]).

Expand All @@ -59,14 +59,19 @@
{error, Error :: {already_started, pid()}} |
{error, Error :: term()} |
ignore
when Mode :: stateless | stateful,
when Mode :: stateful | stateless | stateful_with_cert | stateless_with_cert,
Seed :: undefined | binary().
start_link(Listener, Mode, Lifetime, TicketStoreSize, MaxEarlyDataSize, AntiReplay, Seed) ->
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) ->
gen_server:call(Pid, {new_session_ticket, Prf, MasterSecret}, infinity).
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},
Expand All @@ -85,22 +90,22 @@ init([Listener | Args]) ->

-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,
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),
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) ->
Expand Down Expand Up @@ -235,18 +240,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),
Expand Down Expand Up @@ -278,8 +283,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 ->
Expand All @@ -297,7 +302,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);
Expand Down Expand Up @@ -329,7 +334,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),
Expand All @@ -338,7 +343,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}.

Expand All @@ -357,11 +363,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);
Expand Down Expand Up @@ -435,15 +442,15 @@ in_window(_, undefined) ->
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,
stateless_anti_replay(Index, PSK, Binder, PeerCert,
#state{stateless = #{bloom_filter := BloomFilter0}
= Stateless} = State) ->
case tls_bloom_filter:contains(BloomFilter0, Binder) of
Expand All @@ -452,11 +459,11 @@ 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()}.
Expand Down
Loading