1%% 2%% %CopyrightBegin% 3%% 4%% Copyright Ericsson AB 2020-2020. All Rights Reserved. 5%% 6%% Licensed under the Apache License, Version 2.0 (the "License"); 7%% you may not use this file except in compliance with the License. 8%% You may obtain a copy of the License at 9%% 10%% http://www.apache.org/licenses/LICENSE-2.0 11%% 12%% Unless required by applicable law or agreed to in writing, software 13%% distributed under the License is distributed on an "AS IS" BASIS, 14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15%% See the License for the specific language governing permissions and 16%% limitations under the License. 17%% 18%% %CopyrightEnd% 19%% 20 21%%---------------------------------------------------------------------- 22%% Purpose: Handle server side pre TLS-1.3 reuse session storage. 23%% This implements the RFC session reuse and not the session ticket extension 24%% that inspired the RFC session tickets of TLS-1.3. 25%%---------------------------------------------------------------------- 26 27-module(ssl_server_session_cache). 28-behaviour(gen_server). 29 30-include_lib("kernel/include/logger.hrl"). 31-include("ssl_handshake.hrl"). 32-include("ssl_internal.hrl"). 33 34%% API 35-export([start_link/2, 36 new_session_id/1, 37 register_session/2, 38 reuse_session/2 39 ]). 40 41%% gen_server callbacks 42-export([init/1, 43 handle_call/3, 44 handle_cast/2, 45 handle_info/2, 46 terminate/2 47 %%code_change/3, 48 %%format_status/2 49 ]). 50 51-record(state, {store_cb, 52 lifetime, 53 db, 54 max, 55 session_order, 56 id_generator, 57 listner 58 }). 59 60%%%=================================================================== 61%%% API 62%%%=================================================================== 63 64-spec start_link(pid(), map()) -> {ok, Pid :: pid()} | 65 {error, Error :: {already_started, pid()}} | 66 {error, Error :: term()} | 67 ignore. 68start_link(ssl_unknown_listener = Listner, Map) -> 69 gen_server:start_link({local, Listner}, ?MODULE, [Listner, Map], []); 70start_link(Listner, Map) -> 71 gen_server:start_link(?MODULE, [Listner, Map], []). 72 73%%-------------------------------------------------------------------- 74-spec new_session_id(Pid::pid()) -> ssl:session_id(). 75%% 76%% Description: Creates a session id for the server. 77%%-------------------------------------------------------------------- 78new_session_id(Pid) -> 79 case call(Pid, new_session_id) of 80 {no_server, _} -> 81 crypto:strong_rand_bytes(32); 82 Result -> 83 Result 84 end. 85 86%%-------------------------------------------------------------------- 87-spec reuse_session(pid(), ssl:session_id()) -> #session{} | not_reusable. 88%% 89%% Description: Returns session to reuse 90%%-------------------------------------------------------------------- 91reuse_session(Pid, SessionId) -> 92 case call(Pid, {reuse_session, SessionId}) of 93 {no_server, _} -> 94 not_reusable; 95 Result -> 96 Result 97 end. 98%%-------------------------------------------------------------------- 99-spec register_session(pid(), term()) -> ok. 100%% 101%% Description: Makes a session available for reuse 102%%-------------------------------------------------------------------- 103register_session(Pid, Session) -> 104 gen_server:cast(Pid, {register_session, Session}). 105 106 107%%%=================================================================== 108%%% gen_server callbacks 109%%%=================================================================== 110-spec init(Args :: term()) -> {ok, State :: term()}. 111init([Listner, #{lifetime := Lifetime, 112 session_cb := Cb, 113 session_cb_init_args := InitArgs, 114 max := Max 115 }]) -> 116 process_flag(trap_exit, true), 117 Monitor = monitor_listener(Listner), 118 DbRef = init(Cb, [{role, server} | InitArgs]), 119 State = #state{store_cb = Cb, 120 lifetime = Lifetime, 121 db = DbRef, 122 max = Max, 123 session_order = gb_trees:empty(), 124 id_generator = crypto:strong_rand_bytes(16), 125 listner = Monitor 126 }, 127 {ok, State}. 128 129-spec handle_call(Request :: term(), From :: {pid(), term()}, State :: term()) -> 130 {reply, Reply :: term(), NewState :: term()} . 131handle_call(new_session_id, _From, #state{id_generator = IdGen} = State) -> 132 SessionId = session_id(IdGen), 133 {reply, SessionId, State}; 134handle_call({reuse_session, SessionId}, _From, #state{store_cb = Cb, 135 db = Store0, 136 lifetime = Lifetime, 137 session_order = Order0} = State0) -> 138 case lookup(Cb, Store0, SessionId) of 139 undefined -> 140 {reply, not_reusable, State0}; 141 #session{internal_id = InId} = Session -> 142 case ssl_session:valid_session(Session, Lifetime) of 143 true -> 144 {reply, Session, State0}; 145 false -> 146 {Store, Order} = invalidate_session(Cb, Store0, Order0, SessionId, InId), 147 {reply, not_reusable, State0#state{db = Store, session_order = Order}} 148 end 149 end. 150 151-spec handle_cast(Request :: term(), State :: term()) -> 152 {noreply, NewState :: term()}. 153handle_cast({register_session, #session{session_id = SessionId, time_stamp = TimeStamp} = Session0}, 154 #state{store_cb = Cb, 155 db = Store0, 156 max = Max, 157 lifetime = Lifetime, 158 session_order = Order0} 159 = State0) -> 160 InternalId = {TimeStamp, erlang:unique_integer([monotonic])}, 161 Session = Session0#session{internal_id = InternalId}, 162 State = case size(Cb, Store0) of 163 Max -> 164 %% Throw away oldest session table may not grow larger than max 165 {_, OldSessId, Order1} = gb_trees:take_smallest(Order0), 166 Store1 = delete(Cb, Store0, OldSessId), 167 %% Insert new session 168 Order = gb_trees:insert(InternalId, SessionId, Order1), 169 Store = update(Cb, Store1, SessionId, Session), 170 State0#state{db = Store, session_order = Order}; 171 Size when Size > 0 -> 172 {_, OldSessId, Order1} = gb_trees:take_smallest(Order0), 173 OldestSession = lookup(Cb, Store0, OldSessId), 174 case ssl_session:valid_session(OldestSession, Lifetime) of 175 true -> 176 Store = update(Cb, Store0, SessionId, Session#session{time_stamp = TimeStamp}), 177 State0#state{db = Store, 178 session_order = gb_trees:insert(InternalId, SessionId, Order0)}; 179 false -> 180 %% Throw away oldest session as it is not valid anymore 181 Store1 = delete(Cb, Store0, OldSessId), 182 Store = update(Cb, Store1, SessionId, Session#session{time_stamp = TimeStamp}), 183 State0#state{db = Store, 184 session_order = gb_trees:insert(InternalId, SessionId, Order1)} 185 end; 186 0 -> 187 Store = update(Cb, Store0, SessionId, Session#session{time_stamp = TimeStamp}), 188 State0#state{db = Store, 189 session_order = gb_trees:insert(InternalId, SessionId, Order0)} 190 end, 191 {noreply, State}. 192 193-spec handle_info(Info :: timeout() | term(), State :: term()) -> 194 {noreply, NewState :: term()}. 195handle_info({'DOWN', Monitor, _, _, _}, #state{listner = Monitor} = State) -> 196 {stop, normal, State}; 197handle_info(_, State) -> 198 {noreply, State}. 199 200terminate(_, _) -> 201 ok. 202 203%%%=================================================================== 204%%% Internal functions 205%%%=================================================================== 206call(Pid, Msg) -> 207 try gen_server:call(Pid, Msg, infinity) 208 catch 209 exit:Reason -> 210 {no_server, Reason} 211 end. 212 213session_id(Key) -> 214 Unique1 = erlang:unique_integer(), 215 Unique2 = erlang:unique_integer(), 216 %% Obfuscate to avoid DoS attack possiblities 217 %% This id should be unpredictable an 32 bytes 218 %% and unique but have no other cryptographic requirements. 219 Bin1 = crypto:crypto_one_time(aes_128_ecb, Key, <<Unique1:128>>, true), 220 Bin2 = crypto:crypto_one_time(aes_128_ecb, Key, <<Unique2:128>>, true), 221 <<Bin1/binary, Bin2/binary>>. 222 223invalidate_session(Cb, Store0, Order, SessionId, InternalId) -> 224 Store = delete(Cb, Store0, SessionId), 225 {Store, gb_trees:delete(InternalId, Order)}. 226 227init(Cb, Options) -> 228 Cb:init(Options). 229 230lookup(Cb, Cache, Key) -> 231 Cb:lookup(Cache, Key). 232 233update(ssl_server_session_cache_db = Cb, Cache, Key, Session) -> 234 Cb:update(Cache, Key, Session); 235update(Cb, Cache, Key, Session) -> 236 Cb:update(Cache, Key, Session), 237 Cache. 238 239delete(ssl_server_session_cache_db = Cb, Cache, Key) -> 240 Cb:delete(Cache, Key); 241delete(Cb, Cache, Key) -> 242 Cb:delete(Cache, Key), 243 Cache. 244 245size(Cb,Cache) -> 246 try Cb:size(Cache) of 247 Size -> 248 Size 249 catch 250 error:undef -> 251 Cb:foldl(fun(_, Acc) -> Acc + 1 end, 0, Cache) 252 end. 253 254monitor_listener(ssl_unknown_listener) -> 255 %% Backwards compatible Erlang node 256 %% global process. 257 undefined; 258monitor_listener(Listen) -> 259 inet:monitor(Listen). 260