Skip to content

Commit

Permalink
ssl: Add callback to retrieve saved keylog when connection fails
Browse files Browse the repository at this point in the history
  • Loading branch information
IngelaAndin committed Jan 3, 2025
1 parent 6bf99d6 commit 0a9b377
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 111 deletions.
43 changes: 28 additions & 15 deletions lib/ssl/src/ssl.erl
Original file line number Diff line number Diff line change
Expand Up @@ -658,15 +658,24 @@ Options common to both client and server side.
on hello extensions before continuing or aborting the handshake by
calling `handshake_continue/3` or `handshake_cancel/1`.
- **`{keep_secrets, KeepSecrets}`** - Configures a TLS 1.3 connection for keylogging.
- **`{keep_secrets, KeepSecrets}`** - Configures a TLS connection for keylogging.
In order to retrieve keylog information on a TLS 1.3 connection, it must be
In order to retrieve keylog information on a TLS connection, it must be
configured in advance to keep `client_random` and various handshake secrets.
The `keep_secrets` functionality is disabled (`false`) by default.
Added in OTP 23.2.
`keep_secrets` can also be set to a fun of arity one, in which case
secrets will be keept and the fun will be called with a list of keylog
items in the case that the connection fails. The
reason for this is that when the connection fails calling
connection_information/2 is not possible due to that the connection
has been closed. See [NSS keylog](using_ssl.md#nss-keylog).
Added in OTP @OTP-19391@
- **`{max_handshake_size, HandshakeSize}`** - Limit the acceptable handshake packet size.
Used to limit the size of valid TLS handshake packets to avoid DoS
Expand Down Expand Up @@ -702,7 +711,7 @@ Options common to both client and server side.
{ciphers, cipher_suites()} |
{signature_algs, signature_algs()} |
{signature_algs_cert, [sign_scheme()]} |
{keep_secrets, KeepSecrets:: boolean()} |
{keep_secrets, KeepSecrets:: boolean() | function()} |
{max_handshake_size, HandshakeSize::pos_integer()} |
{versions, [protocol_version()]} |
{log_level, Level::logger:level() | none | all} |
Expand Down Expand Up @@ -2053,7 +2062,7 @@ TLS connection information that can be used for NSS key logging.
-type security_info() :: [{client_random, binary()} |
{server_random, binary()} |
{master_secret, binary()} |
{keylog, term()}].
{keylog, [io_lib:chars()]}].


-doc(#{title => <<"Info">>}).
Expand Down Expand Up @@ -2690,9 +2699,8 @@ defined.
Note that the values for `client_random`, `server_random`, `master_secret`, and `keylog`
affect the security of connection.
In order to retrieve `keylog` and other secret information from a TLS 1.3
connection, the `keep_secrets` option must be configured in advance and
set to `true`.
In order to retrieve `keylog` and other secret information from a TLS
connection, the `keep_secrets` option must be configured in advance.
> #### Note {: .info }
>
Expand Down Expand Up @@ -3809,7 +3817,7 @@ opt_protocol_versions(UserOpts, Opts, Env) ->

{_, LL} = get_opt_of(log_level, LogLevels, DefaultLevel, UserOpts, Opts),

Opts1 = set_opt_bool(keep_secrets, false, UserOpts, Opts),
Opts1 = opt_keep_secrets(UserOpts, Opts),

{DistW, Dist} = get_opt_bool(erl_dist, false, UserOpts, Opts1),
option_incompatible(PRC =:= dtls andalso Dist, [{protocol, PRC}, {erl_dist, Dist}]),
Expand Down Expand Up @@ -4623,6 +4631,18 @@ opt_process(UserOpts, Opts0, _Env) ->
%% Opts = Opts1#{receiver_spawn_opts => RSO, sender_spawn_opts => SSO},
set_opt_int(hibernate_after, 0, infinity, infinity, UserOpts, Opts2).

opt_keep_secrets(UserOpts, Opts) ->
case get_opt(keep_secrets, false, UserOpts, Opts) of
{new, Value} when is_boolean(Value) orelse is_function(Value) ->
Opts#{keep_secrets => Value};
{old, _} ->
Opts;
{default, _} -> %% Keep default implicit
Opts;
{_, Value} ->
option_error(keep_secrets, Value)
end.

%%%%

get_opt(Opt, Default, UserOpts, Opts) ->
Expand Down Expand Up @@ -4693,13 +4713,6 @@ get_opt_file(Opt, Default, UserOpts, Opts) ->
Res -> Res
end.

set_opt_bool(Opt, Default, UserOpts, Opts) ->
case maps:get(Opt, UserOpts, Default) of
Default -> Opts;
Value when is_boolean(Value) -> Opts#{Opt => Value};
Value -> option_error(Opt, Value)
end.

get_opt_map(Opt, Default, UserOpts, Opts) ->
case get_opt(Opt, Default, UserOpts, Opts) of
{new, Err} when not is_map(Err) -> option_error(Opt, Err);
Expand Down
190 changes: 114 additions & 76 deletions lib/ssl/src/ssl_gen_statem.erl
Original file line number Diff line number Diff line change
Expand Up @@ -967,16 +967,9 @@ read_application_data(Data,
end
end.

passive_receive(#state{static_env = #static_env{role = Role,
socket = Socket,
trackers = Trackers,
transport_cb = Transport,
protocol_cb = Connection},
recv = #recv{from = RecvFrom},
connection_env = #connection_env{socket_tls_closed = #alert{} = Alert}} = State,
passive_receive(#state{connection_env = #connection_env{socket_tls_closed = #alert{} = Alert}} = State,
StateName, _) ->
Pids = Connection:pids(State),
alert_user(Pids, Transport, Trackers, Socket, RecvFrom, Alert, Role, StateName, Connection),
handle_normal_shutdown(Alert, StateName, State),
{stop, {shutdown, normal}, State};
passive_receive(#state{user_data_buffer = {Front,BufferSize,Rear},
%% Assert! Erl distribution uses active sockets
Expand Down Expand Up @@ -1024,15 +1017,16 @@ handle_own_alert(Alert0, StateName,
#state{static_env = #static_env{role = Role,
protocol_cb = Connection},
ssl_options = #{log_level := LogLevel}} = State) ->
Alert = Alert0#alert{role = Role},
try %% Try to tell the other side
send_alert(Alert0, StateName, State)
send_alert(Alert, StateName, State)
catch _:_ -> %% Can crash if we are in a uninitialized state
ignore
end,
try %% Try to tell the local user
Alert = Alert0#alert{role = Role},
log_alert(LogLevel, Role, Connection:protocol_name(), StateName, Alert),
handle_normal_shutdown(Alert,StateName, State)
try
log_alert(LogLevel, Role, Connection:protocol_name(), StateName, Alert),
%% Try to tell the local user
handle_normal_shutdown(Alert, StateName, State)
catch _:_ ->
ok
end,
Expand All @@ -1047,6 +1041,7 @@ handle_normal_shutdown(Alert, StateName, #state{static_env = #static_env{role =
recv = #recv{from = StartFrom}
} = State) ->
Pids = Connection:pids(State),
maybe_keep_secrets_callback(Alert, StateName, State),
alert_user(Pids, Transport, Trackers, Socket, StartFrom, Alert, Role, StateName, Connection);

handle_normal_shutdown(Alert, StateName, #state{static_env = #static_env{role = Role,
Expand All @@ -1060,6 +1055,7 @@ handle_normal_shutdown(Alert, StateName, #state{static_env = #static_env{role =
recv = #recv{from = RecvFrom}
} = State) ->
Pids = Connection:pids(State),
maybe_keep_secrets_callback(Alert, StateName, State),
alert_user(Pids, Transport, Trackers, Socket, Type, Opts, Pid, RecvFrom, Alert, Role, StateName, Connection).

handle_alert(#alert{level = ?FATAL} = Alert, StateName, State) ->
Expand Down Expand Up @@ -1159,23 +1155,16 @@ handle_alert(Alert, StateName, State) ->

handle_fatal_alert(Alert0, StateName,
#state{static_env = #static_env{role = Role,
socket = Socket,
host = Host,
port = Port,
trackers = Trackers,
transport_cb = Transport,
protocol_cb = Connection},
connection_env = #connection_env{user_application = {_Mon, Pid}},
ssl_options = #{log_level := LogLevel},
recv = #recv{from = From},
session = Session,
socket_options = Opts} = State) ->
session = Session} = State) ->
invalidate_session(Role, Host, Port, Session),
Alert = Alert0#alert{role = opposite_role(Role)},
log_alert(LogLevel, Role, Connection:protocol_name(),
StateName, Alert),
Pids = Connection:pids(State),
alert_user(Pids, Transport, Trackers, Socket, StateName, Opts, Pid, From, Alert, Role, StateName, Connection),
handle_normal_shutdown(Alert, StateName, State),
{stop, {shutdown, normal}, State}.

handle_trusted_certs_db(#state{ssl_options =#{cacerts := []} = Opts})
Expand Down Expand Up @@ -1939,12 +1928,15 @@ security_info(#state{connection_states = ConnectionStates,
application_traffic_secret = AppTrafSecretRead,
client_early_data_secret = ServerEarlyData
}} = ReadState,
BaseSecurityInfo = [{client_random, ClientRand}, {server_random, ServerRand}, {master_secret, MasterSecret}],

BaseSecurityInfo = [{client_random, ClientRand},
{server_random, ServerRand}, {master_secret, MasterSecret}],
KeepSecrets = maps:get(keep_secrets, Opts, false),
if KeepSecrets =/= true ->
BaseSecurityInfo;
true ->
case KeepSecrets of
false ->
%% Need to include {keep_secrets, false} for maybe_add_keylog
%% to be able to run in user process context
[{keep_secrets, false} | BaseSecurityInfo];
Other when Other == true orelse is_function(Other) ->
#{security_parameters :=
#security_parameters{
application_traffic_secret = AppTrafSecretWrite0,
Expand Down Expand Up @@ -1976,7 +1968,7 @@ security_info(#state{connection_states = ConnectionStates,
server_handshake_traffic_secret := ServerHSTrafficSecret} ->
[{client_handshake_traffic_secret, ClientHSTrafficSecret},
{server_handshake_traffic_secret, ServerHSTrafficSecret}];
_ ->
_ ->
[]
end ++ BaseSecurityInfo
end.
Expand Down Expand Up @@ -2133,16 +2125,40 @@ ssl_options_list([{ciphers = Key, Value}|T], Acc) ->
ssl_options_list([{Key, Value}|T], Acc) ->
ssl_options_list(T, [{Key, Value} | Acc]).

%% Maybe add NSS keylog info according to
%% https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format
maybe_add_keylog(Info) ->
maybe_add_keylog(lists:keyfind(protocol, 1, Info), Info).
keylog_1_3(Info) ->
{client_random, ClientRandomBin} = lists:keyfind(client_random, 1, Info),
{selected_cipher_suite, #{prf := Prf}} = lists:keyfind(selected_cipher_suite, 1, Info),
ClientRandom = binary:decode_unsigned(ClientRandomBin),
hs_keylog_1_3(ClientRandom, Prf, Info) ++ connection_keylog_1_3(ClientRandom, Prf, Info).

keylog_pre_1_3(Info) ->
{client_random, ClientRandomBin} = lists:keyfind(client_random, 1, Info),
ClientRandom = binary:decode_unsigned(ClientRandomBin),
connection_keylog_pre_1_3(ClientRandom, Info).

hs_keylog_1_3(ClientRandom, Prf, Info) ->
{client_handshake_traffic_secret, ClientHSecretBin} =
lists:keyfind(client_handshake_traffic_secret, 1, Info),
{server_handshake_traffic_secret, ServerHSecretBin} =
lists:keyfind(server_handshake_traffic_secret, 1, Info),
ClientHSecret = keylog_secret(ClientHSecretBin, Prf),
ServerHSecret = keylog_secret(ServerHSecretBin, Prf),
Keylog0 = [io_lib:format("CLIENT_HANDSHAKE_TRAFFIC_SECRET ~64.16.0B ",
[ClientRandom]) ++ ClientHSecret,
io_lib:format("SERVER_HANDSHAKE_TRAFFIC_SECRET ~64.16.0B ",
[ClientRandom]) ++ ServerHSecret],
case lists:keyfind(client_early_data_secret, 1, Info) of
{client_early_data_secret, EarlySecret} ->
ClientEarlySecret = keylog_secret(EarlySecret, Prf),
[io_lib:format("CLIENT_EARLY_TRAFFIC_SECRET ~64.16.0B ",
[ClientRandom]) ++ ClientEarlySecret | Keylog0];
_ ->
Keylog0
end.

maybe_add_keylog({_, 'tlsv1.3'}, Info) ->
try
{client_random, ClientRandomBin} = lists:keyfind(client_random, 1, Info),
%% after traffic key update current traffic secret
%% is stored in tls_sender process state
connection_keylog_1_3(ClientRandom, Prf, Info) ->
%% after traffic key update current traffic secret
%% is stored in tls_sender process state
MaybeUpdateTrafficSecret =
fun({Direction, {Sender, TrafficSecret0}}) ->
TrafficSecret =
Expand All @@ -2160,45 +2176,34 @@ maybe_add_keylog({_, 'tlsv1.3'}, Info) ->
MaybeUpdateTrafficSecret(lists:keyfind(client_traffic_secret_0, 1, Info)),
{server_traffic_secret_0, ServerTrafficSecret0Bin} =
MaybeUpdateTrafficSecret(lists:keyfind(server_traffic_secret_0, 1, Info)),
{client_handshake_traffic_secret, ClientHSecretBin} = lists:keyfind(client_handshake_traffic_secret, 1, Info),
{server_handshake_traffic_secret, ServerHSecretBin} = lists:keyfind(server_handshake_traffic_secret, 1, Info),
{selected_cipher_suite, #{prf := Prf}} = lists:keyfind(selected_cipher_suite, 1, Info),
ClientRandom = binary:decode_unsigned(ClientRandomBin),
ClientTrafficSecret0 = keylog_secret(ClientTrafficSecret0Bin, Prf),
ServerTrafficSecret0 = keylog_secret(ServerTrafficSecret0Bin, Prf),
ClientHSecret = keylog_secret(ClientHSecretBin, Prf),
ServerHSecret = keylog_secret(ServerHSecretBin, Prf),
Keylog0 = [io_lib:format("CLIENT_HANDSHAKE_TRAFFIC_SECRET ~64.16.0B ", [ClientRandom]) ++ ClientHSecret,
io_lib:format("SERVER_HANDSHAKE_TRAFFIC_SECRET ~64.16.0B ", [ClientRandom]) ++ ServerHSecret,
io_lib:format("CLIENT_TRAFFIC_SECRET_0 ~64.16.0B ", [ClientRandom]) ++ ClientTrafficSecret0,
io_lib:format("SERVER_TRAFFIC_SECRET_0 ~64.16.0B ", [ClientRandom]) ++ ServerTrafficSecret0],
Keylog = case lists:keyfind(client_early_data_secret, 1, Info) of
{client_early_data_secret, EarlySecret} ->
ClientEarlySecret = keylog_secret(EarlySecret, Prf),
[io_lib:format("CLIENT_EARLY_TRAFFIC_SECRET ~64.16.0B ", [ClientRandom]) ++ ClientEarlySecret
| Keylog0];
_ ->
Keylog0
end,
Info ++ [{keylog,Keylog}]
catch
_Cxx:_Exx ->
Info
end;
maybe_add_keylog({_, _}, Info) ->
try
{client_random, ClientRandomBin} = lists:keyfind(client_random, 1, Info),
{master_secret, MasterSecretBin} = lists:keyfind(master_secret, 1, Info),
ClientRandom = binary:decode_unsigned(ClientRandomBin),
MasterSecret = binary:decode_unsigned(MasterSecretBin),
Keylog = [io_lib:format("CLIENT_RANDOM ~64.16.0B ~96.16.0B", [ClientRandom, MasterSecret])],
Info ++ [{keylog,Keylog}]
catch
_Cxx:_Exx ->

ClientTrafficSecret = keylog_secret(ClientTrafficSecret0Bin, Prf),
ServerTrafficSecret = keylog_secret(ServerTrafficSecret0Bin, Prf),

[io_lib:format("CLIENT_TRAFFIC_SECRET_0 ~64.16.0B ", [ClientRandom]) ++ ClientTrafficSecret,
io_lib:format("SERVER_TRAFFIC_SECRET_0 ~64.16.0B ", [ClientRandom]) ++ ServerTrafficSecret].

connection_keylog_pre_1_3(ClientRandom, Info) ->
{master_secret, MasterSecretBin} = lists:keyfind(master_secret, 1, Info),
MasterSecret = binary:decode_unsigned(MasterSecretBin),
[io_lib:format("CLIENT_RANDOM ~64.16.0B ~96.16.0B", [ClientRandom, MasterSecret])].


%% Maybe add NSS keylog info according to
%% https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format
maybe_add_keylog(Info) ->
case lists:keyfind(keep_secrets, 1, Info) of
{keep_secrets, Value} when Value == true;
is_function(Value)->
case lists:keyfind(protocol, 1, Info) of
{protocol, 'tlsv1.3'} ->
Info ++ [{keylog, keylog_1_3(Info)}];
{protocol, _} ->
Info ++ [{keylog, keylog_pre_1_3(Info)}]
end;
{keep_secrets, false} ->
Info
end;
maybe_add_keylog(_, Info) ->
Info.
end.

keylog_secret(SecretBin, sha256) ->
io_lib:format("~64.16.0B", [binary:decode_unsigned(SecretBin)]);
Expand All @@ -2207,6 +2212,39 @@ keylog_secret(SecretBin, sha384) ->
keylog_secret(SecretBin, sha512) ->
io_lib:format("~128.16.0B", [binary:decode_unsigned(SecretBin)]).

maybe_keep_secrets_callback(#alert{level = ?FATAL}, StateName,
#state{ssl_options = #{keep_secrets := Fun}} = State) when is_function(Fun) ->
case alert_keylog(StateName, State) of
[] ->
ok;
KeyLog ->
Fun(KeyLog)
end;
maybe_keep_secrets_callback(_, _, _) ->
ok.

alert_keylog(connection, #state{connection_env =
#connection_env{negotiated_version = TlsVersion}} = State)
when ?TLS_GTE(TlsVersion, ?TLS_1_3) ->
keylog_1_3(connection_info(State) ++ security_info(State));
alert_keylog(connection, #state{connection_env =
#connection_env{negotiated_version = TlsVersion}} = State)
when ?TLS_LTE(TlsVersion, ?TLS_1_2) ->
keylog_pre_1_3(security_info(State));
alert_keylog(start, _) -> %% TLS 1.3
[];
alert_keylog(wait_sh, _) -> %% TLS 1.3
[];
alert_keylog(_, #state{connection_env =
#connection_env{negotiated_version = TlsVersion}} = State)
when ?TLS_GTE(TlsVersion, ?TLS_1_3) ->
Info = connection_info(State) ++ security_info(State),
{client_random, ClientRandomBin} = lists:keyfind(client_random, 1, Info),
{selected_cipher_suite, #{prf := Prf}} = lists:keyfind(selected_cipher_suite, 1, Info),
ClientRandom = binary:decode_unsigned(ClientRandomBin),
hs_keylog_1_3(ClientRandom, Prf, Info);
alert_keylog(_,_) ->
[].

%%%################################################################
%%%#
Expand Down
4 changes: 3 additions & 1 deletion lib/ssl/src/tls_gen_connection.erl
Original file line number Diff line number Diff line change
Expand Up @@ -910,10 +910,12 @@ handle_alerts([#alert{level = ?WARNING, description = ?CLOSE_NOTIFY} | _Alerts],
recv = #recv{from = From}} = State}) when From == undefined ->
%% Linger to allow recv and setopts to possibly fetch data not yet delivered to user to be fetched
{next_state, StateName, State#state{connection_env = CEnv#connection_env{socket_tls_closed = true}}};
handle_alerts([#alert{level = ?FATAL} = Alert | _Alerts],
handle_alerts([#alert{level = ?FATAL} = Alert0 | _Alerts],
{next_state, connection = StateName, #state{connection_env = CEnv,
static_env = #static_env{role = Role},
socket_options = #socket_options{active = false},
recv = #recv{from = From}} = State}) when From == undefined ->
Alert = Alert0#alert{role = ssl_gen_statem:opposite_role(Role)},
%% Linger to allow recv and setopts to retrieve alert reason
{next_state, StateName, State#state{connection_env = CEnv#connection_env{socket_tls_closed = Alert}}};
handle_alerts([Alert | Alerts], {next_state, StateName, State}) ->
Expand Down
Loading

0 comments on commit 0a9b377

Please sign in to comment.