Skip to content
Open
4 changes: 2 additions & 2 deletions lib/ssl/doc/ssl_app.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,11 @@ The environment parameters can be set on the command line, for example:
early_data_indication extension. Defaults to 16384. Size limit is enforced by
both client and server.

- **`client_session_ticket_lifetime = integer() <optional>`** - Lifetime of
- **`client_session_ticket_lifetime = pos_integer() <optional>`** - Lifetime of
session tickets in the client ticket store. Expired tickets are automatically
removed. Defaults to 7200 seconds (2 hours).

- **`client_session_ticket_store_size = integer() <optional>`** - Sets the
- **`client_session_ticket_store_size = pos_integer() <optional>`** - Sets the
maximum size of the client session ticket store. Defaults to 1000. Size limit
is enforced by dropping old tickets.

Expand Down
6 changes: 3 additions & 3 deletions lib/ssl/src/dtls_client_connection.erl
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@
-export([callback_mode/0,
terminate/3,
code_change/4,
format_status/2]).
format_status/1]).

%% Tracing
-export([handle_trace/3]).
Expand Down Expand Up @@ -573,8 +573,8 @@ terminate(Reason, StateName, State) ->
code_change(_OldVsn, StateName, State, _Extra) ->
{ok, StateName, State}.

format_status(Type, Data) ->
ssl_gen_statem:format_status(Type, Data).
format_status(Data) ->
ssl_gen_statem:format_status(Data).

gen_state(StateName, Type, Event, State) ->
try tls_dtls_client_connection:StateName(Type, Event, State)
Expand Down
8 changes: 5 additions & 3 deletions lib/ssl/src/dtls_gen_connection.erl
Original file line number Diff line number Diff line change
Expand Up @@ -887,15 +887,17 @@ next_dtls_record(Data, StateName, #state{protocol_buffers = #protocol_buffers{



decode_cipher_text(#state{protocol_buffers = #protocol_buffers{dtls_cipher_texts = [ CT | Rest]} = Buffers,
decode_cipher_text(#state{protocol_buffers = #protocol_buffers{dtls_cipher_texts = [ CT | Rest]} =
Buffers,
connection_states = ConnStates0} = State) ->
case dtls_record:decode_cipher_text(CT, ConnStates0) of
{Plain, ConnStates} ->
{Plain, ConnStates} ->
{Plain, State#state{protocol_buffers =
Buffers#protocol_buffers{dtls_cipher_texts = Rest},
connection_states = ConnStates}};
#alert{} = Alert ->
{Alert, State}
{Alert, State#state{protocol_buffers =
Buffers#protocol_buffers{dtls_cipher_texts = Rest}}}
end.

decode_alerts(Bin) ->
Expand Down
33 changes: 21 additions & 12 deletions lib/ssl/src/dtls_handshake.erl
Original file line number Diff line number Diff line change
Expand Up @@ -318,24 +318,33 @@ address_to_bin({A,B,C,D,E,F,G,H}, Port) ->
<<A:16,B:16,C:16,D:16,E:16,F:16,G:16,H:16,Port:16>>.

%%--------------------------------------------------------------------

handle_fragments(Version, FragmentData, Buffers0, Options, Acc) ->
Fragments = decode_handshake_fragments(FragmentData),
do_handle_fragments(Version, Fragments, Buffers0, Options, Acc).
try decode_handshake_fragments(FragmentData) of
Fragments ->
do_handle_fragments(Version, Fragments, Buffers0, Options, Acc)
catch
error:_Reason ->
throw(?ALERT_REC(?FATAL, ?DECODE_ERROR, malformed_handshake_fragment))
end.

do_handle_fragments(_, [], Buffers, _Options, Acc) ->
{lists:reverse(Acc), Buffers};
do_handle_fragments(Version, [Fragment | Fragments], Buffers0, #{log_level := LogLevel} = Options, Acc) ->
case reassemble(Version, Fragment, Buffers0) of
{more_data, Buffers} when Fragments == [] ->
{lists:reverse(Acc), Buffers};
{more_data, Buffers} ->
do_handle_fragments(Version, Fragments, Buffers, Options, Acc);
{{Handshake, _} = HsPacket, Buffers} ->
do_handle_fragments(Version, [Fragment | Fragments], Buffers0,
#{log_level := LogLevel} = Options, Acc) ->
try reassemble(Version, Fragment, Buffers0) of
{more_data, Buffers} when Fragments == [] ->
{lists:reverse(Acc), Buffers};
{more_data, Buffers} ->
do_handle_fragments(Version, Fragments, Buffers, Options, Acc);
{{Handshake, _} = HsPacket, Buffers} ->
ssl_logger:debug(LogLevel, inbound, 'handshake', Handshake),
do_handle_fragments(Version, Fragments, Buffers, Options, [HsPacket | Acc])
do_handle_fragments(Version, Fragments, Buffers, Options, [HsPacket | Acc])
catch
error:_Reason ->
throw(?ALERT_REC(?FATAL, ?DECODE_ERROR, malformed_handshake_fragment))
end.


decode_handshake(Version, <<?BYTE(Type), Bin/binary>>) ->
decode_handshake(Version, Type, Bin).

Expand Down Expand Up @@ -379,7 +388,7 @@ decode_tls_handshake(Version, Tag, Msg) ->
ssl_handshake:decode_handshake(TLSVersion, Tag, Msg).

decode_handshake_fragments(<<>>) ->
[<<>>];
[];
decode_handshake_fragments(<<?BYTE(Type), ?UINT24(Length),
?UINT16(MessageSeq),
?UINT24(FragmentOffset), ?UINT24(FragmentLength),
Expand Down
17 changes: 12 additions & 5 deletions lib/ssl/src/dtls_record.erl
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,11 @@ get_connection_state_by_epoch(Epoch, #{current_read := #{epoch := Epoch} = Curre
Current;
get_connection_state_by_epoch(Epoch, #{saved_read := #{epoch := Epoch} = Saved},
read) ->
Saved.
Saved;
%% This can be an attack on read side so return undefined so we can trigger alert
%% on write side this would be a programming error, so let it crash.
get_connection_state_by_epoch(_, _, read) ->
undefined.

set_connection_state_by_epoch(WriteState, Epoch, #{current_write := #{epoch := Epoch}} = States,
write) ->
Expand Down Expand Up @@ -253,10 +257,13 @@ encode_plain_text(Type, Version, Epoch, Data, ConnectionStates) ->
%% Decoding
%%====================================================================

decode_cipher_text(#ssl_tls{epoch = Epoch} = CipherText, ConnnectionStates0) ->
ReadState = get_connection_state_by_epoch(Epoch, ConnnectionStates0, read),
decode_cipher_text(CipherText, ReadState, ConnnectionStates0).

decode_cipher_text(#ssl_tls{epoch = Epoch} = CipherText, ConnectionStates0) ->
case get_connection_state_by_epoch(Epoch, ConnectionStates0, read) of
undefined ->
?ALERT_REC(?FATAL, ?BAD_RECORD_MAC);
ReadState ->
decode_cipher_text(CipherText, ReadState, ConnectionStates0)
end.

%%====================================================================
%% Protocol version handling
Expand Down
6 changes: 3 additions & 3 deletions lib/ssl/src/dtls_server_connection.erl
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@
-export([callback_mode/0,
terminate/3,
code_change/4,
format_status/2]).
format_status/1]).

%% Tracing
-export([handle_trace/3]).
Expand Down Expand Up @@ -547,8 +547,8 @@ terminate(Reason, StateName, State) ->
code_change(_OldVsn, StateName, State, _Extra) ->
{ok, StateName, State}.

format_status(Type, Data) ->
ssl_gen_statem:format_status(Type, Data).
format_status(Data) ->
ssl_gen_statem:format_status(Data).

%%--------------------------------------------------------------------
%% Internal functions
Expand Down
45 changes: 41 additions & 4 deletions lib/ssl/src/inet_epmd_tls_socket.erl
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ accept_open(NetAddress, ListenSocket) ->
{error, Reason} ->
exit({?FUNCTION_NAME, Reason})
end.

accept_handshake(#net_address{ family = Family }, Socket, PeerIp, PeerPort) ->
Opts = inet_tls_dist:get_ssl_server_options(PeerIp),
case
Expand All @@ -126,10 +125,18 @@ accept_handshake(#net_address{ family = Family }, Socket, PeerIp, PeerPort) ->
net_kernel:connecttime())
of
{ok, SslSocket} ->
{SslSocket, {PeerIp, PeerPort}};
%% Verify peer cert matches allowed nodes — mirrors
%% inet_tls_dist:allowed_nodes/2 which the classic path
%% runs post-handshake. Without this, any cert chaining
%% to the trusted CA can claim any node name.
case check_allowed_nodes(SslSocket, PeerIp) of
ok ->
{SslSocket, {PeerIp, PeerPort}};
{error, Reason} ->
ssl:close(SslSocket),
exit({?FUNCTION_NAME, Reason})
end;
{error, {options, _} = Reason} = Error ->
%% Bad options: that's probably our fault.
%% Let's log that.
?LOG_ERROR(
"Cannot accept TLS distribution connection: ~s~n",
[ssl:format_error(Error)]),
Expand All @@ -140,6 +147,36 @@ accept_handshake(#net_address{ family = Family }, Socket, PeerIp, PeerPort) ->
exit({?FUNCTION_NAME, Reason})
end.

check_allowed_nodes(SslSocket, PeerIp) ->
{ok, Allowed} = net_kernel:allowed(),
case Allowed of
[] ->
%% No restriction configured — allow all
ok;
_ ->
case ssl:peercert(SslSocket) of
{ok, PeerCertDER} ->
PeerCert = public_key:pkix_decode_cert(PeerCertDER, otp),
AllowedHosts = inet_tls_dist:allowed_hosts(Allowed),
case
public_key:pkix_verify_hostname(
PeerCert,
[{ip, PeerIp} | [{dns_id, Host} || Host <- AllowedHosts]])
of
true -> ok;
false ->
?LOG_ERROR(
"** Connection attempt from "
"disallowed node ~p ** ~n", [PeerIp]),
{error, cert_no_hostname_nor_ip_match}
end;
{error, no_peercert} ->
%% No peer cert — allow (same as classic path)
ok;
{error, _} = Error ->
Error
end
end.
%% ------------------------------------------------------------
accept_controller(_NetAddress, Controller, SslSocket) ->
maybe
Expand Down
4 changes: 2 additions & 2 deletions lib/ssl/src/inet_tls_dist.erl
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
-export([fam_select/2, fam_address/1, fam_listen/3, fam_accept/2,
fam_accept_connection/6, fam_setup/6]).

-export([verify_client/3, cert_nodes/1,
-export([verify_client/3, cert_nodes/1, allowed_hosts/1,
get_ssl_client_options/0, get_ssl_server_options/1]).

%% kTLS helpers
Expand Down Expand Up @@ -861,7 +861,7 @@ get_ssl_options(Type) ->
dist_defaults(Opts) ->
case proplists:get_value(versions, Opts, undefined) of
undefined ->
[{versions, ['tlsv1.2']} | Opts];
[{versions, ['tlsv1.3']} | Opts];
_ ->
Opts
end.
Expand Down
20 changes: 19 additions & 1 deletion lib/ssl/src/ssl.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,16 @@ There are two implementations available:
extensions](`e:public_key:public_key_records.md`). Requires the
[Inets](`e:inets:introduction.md`) application.

- **`{allowed_hosts, [string()]}`**

If http fetching is allowed, a list of allowed hosts can be specified as
a hardening option. The entries should be "Host:Port". If ":Port" is left out
the default port is 80. If not specified only an external hosts using port 80
or 8080 will be allowed.

> #### Note {: .info }
Putting the local host on the allow list will of course make the local host allowed.

- **`ssl_crl_hash_dir`** - Implementation 2

This module makes use of a directory where CRLs are
Expand Down Expand Up @@ -2063,9 +2073,17 @@ Options only relevant for TLS-1.3.

Configures if the server accepts (`enabled`) or rejects (`disabled`) early data
sent by a client. The default value is `disabled`.

> #### Warning {: .warning }
> 0-RTT data is inherently replay-vulnerable by TLS 1.3 design. The
> mitigation is application-level idempotency OR server-side anti-replay.
> The server side mechanisms for anti-replay is stateful tickets or stateless
> tickets with a configured Bloom filter.

""".
-type server_option_tls13() :: {session_tickets, SessionTickets:: disabled | stateful | stateless |
stateful_with_cert | stateless_with_cert} |
stateful_with_cert |
stateless_with_cert} |
{stateless_tickets_seed, TicketSeed::binary()} |
{anti_replay, '10k' | '100k' |
{BloomFilterWindowSize::pos_integer(),
Expand Down
29 changes: 27 additions & 2 deletions lib/ssl/src/ssl_config.erl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
-define(DEFAULT_MAX_SESSION_CACHE, 1000).
-define(TWO_HOURS, 7200).
-define(SEVEN_DAYS, 604800).
-define(DEFAULT_MAX_CRL_CACHE_SIZE, 100000).

%% Connection parameter configuration
-export([init/2,
Expand All @@ -46,6 +47,7 @@
%% Application configuration
-export([pre_1_3_session_opts/1,
get_max_early_data_size/0,
get_max_crl_cache_size/0,
get_ticket_lifetime/0,
get_ticket_store_size/0,
get_internal_active_n/0,
Expand Down Expand Up @@ -146,6 +148,9 @@ get_internal_active_n(true) ->
get_internal_active_n(false) ->
application_int(internal_active_n, ?INTERNAL_ACTIVE_N).

get_max_crl_cache_size() ->
application_int(crl_cache_max_size, ?DEFAULT_MAX_CRL_CACHE_SIZE).

%%====================================================================
%% Certificate and Key configuration
%%====================================================================
Expand Down Expand Up @@ -1020,6 +1025,13 @@ opt_tickets(UserOpts, #{versions := Versions} = Opts, #{role := server}) ->
option_incompatible(STS =/= undefined andalso not Stateless,
[stateless_tickets_seed, {session_tickets, SessionTickets}]),

case EarlyData =:= enabled andalso Stateless andalso AntiReplay =:= undefined of
true ->
?LOG_WARNING("early_data enabled without anti_replay; "
"0-RTT data is replayable");
false ->
ok
end,
assert_client_only(use_ticket, UserOpts),
Opts#{session_tickets => SessionTickets, early_data => EarlyData,
anti_replay => AntiReplay, stateless_tickets_seed => STS}.
Expand Down Expand Up @@ -1497,9 +1509,22 @@ opt_psk_groups(#supported_groups{supported_groups = [First| _] = SupportedGroups
end.

opt_crl(UserOpts, Opts, _Env) ->
ManagerType = case maps:get(erl_dist, Opts, false) of
false ->
normal;
true ->
dist
end,
{_, Check} = get_opt_of(crl_check, [best_effort, peer, true, false], false, UserOpts, Opts),
Cache = case get_opt(crl_cache, {ssl_crl_cache, {internal, []}}, UserOpts, Opts) of
{_, {Cb, {_Handle, Options}} = Value} when is_atom(Cb), is_list(Options) ->
Cache = case get_opt(crl_cache, {ssl_crl_cache, {internal, [{owner, ManagerType}]}},
UserOpts, Opts) of
{default, {ssl_crl_cache, {_Handle, _Options}} = Value} ->
Value;
{old, {ssl_crl_cache, {_Handle, _Options}} = Value} ->
Value;
{new, {ssl_crl_cache, {Handle, Options}}} when is_list(Options) ->
{ssl_crl_cache, {Handle, [{owner, ManagerType} | Options]}};
{_, {Cb, {_Handle, Options}} = Value} when is_atom(Cb), is_list(Options) ->
Value;
{_, Err} ->
option_error(crl_cache, Err)
Expand Down
23 changes: 14 additions & 9 deletions lib/ssl/src/ssl_crl.erl
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,18 @@ find_issuer(IsIssuerFun, Db, _) ->
verify_crl_issuer(CRL, #cert{otp = OTPCertCandidate}, Issuer, NotIssuer) ->
TBSCert = OTPCertCandidate#'OTPCertificate'.tbsCertificate,
case public_key:pkix_normalize_name(TBSCert#'OTPTBSCertificate'.subject) of
Issuer ->
case public_key:pkix_crl_verify(CRL, OTPCertCandidate) of
true ->
throw({ok, OTPCertCandidate});
false ->
NotIssuer
end;
_ ->
NotIssuer
Issuer ->
try public_key:pkix_crl_verify(CRL, OTPCertCandidate) of
true ->
throw({ok, OTPCertCandidate});
false ->
NotIssuer
catch _:_ ->
%% Fail gracefully unlikely to happen in valid use cases
?LOG_WARNING("SSL WARNING: Ignoring CRL "
"unexpected signature algorithm", [CRL]),
NotIssuer
end;
_ ->
NotIssuer
end.
Loading
Loading