Skip to content

Commit

Permalink
Merge fb30207 into 002dc85
Browse files Browse the repository at this point in the history
  • Loading branch information
qzhuyan committed Apr 30, 2024
2 parents 002dc85 + fb30207 commit 3b1296d
Show file tree
Hide file tree
Showing 14 changed files with 1,909 additions and 5 deletions.
127 changes: 127 additions & 0 deletions apps/emqx/src/emqx_const_v2.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2024 EMQ Technologies Co., Ltd. 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.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% @doc Never update this module, create a v3 instead.
%%--------------------------------------------------------------------

-module(emqx_const_v2).
-elvis([{elvis_style, atom_naming_convention, #{regex => "^([a-z][a-z0-9A-Z]*_?)*(_SUITE)?$"}}]).

-export([
make_tls_root_fun/2,
make_tls_verify_fun/2
]).

-include_lib("public_key/include/public_key.hrl").
%% @doc Build a root fun for verify TLS partial_chain.
%% The `InputChain' is composed by OTP SSL with local cert store
%% AND the cert (chain if any) from the client.
%% @end
make_tls_root_fun(cacert_from_cacertfile, [Trusted]) ->
%% Allow only one trusted ca cert, and just return the defined trusted CA cert,
fun(_InputChain) ->
%% Note, returing `trusted_ca` doesn't really mean it accepts the connection
%% OTP SSL app will do the path validation, signature validation subsequently.
{trusted_ca, Trusted}
end;
make_tls_root_fun(cacert_from_cacertfile, [TrustedOne, TrustedTwo]) ->
%% Allow two trusted CA certs in case of CA cert renewal
%% This is a little expensive call as it compares the binaries.
fun(InputChain) ->
case lists:member(TrustedOne, InputChain) of
true ->
{trusted_ca, TrustedOne};
false ->
{trusted_ca, TrustedTwo}
end
end.

make_tls_verify_fun(verify_cert_extKeyUsage, KeyUsages) ->
AllowedKeyUsages = ext_key_opts(KeyUsages),
{fun verify_fun_peer_extKeyUsage/3, AllowedKeyUsages}.

verify_fun_peer_extKeyUsage(_, {bad_cert, invalid_ext_key_usage}, UserState) ->
%% !! Override OTP verify peer default
%% OTP SSL is unhappy with the ext_key_usage but we will check on our own.
{unknown, UserState};
verify_fun_peer_extKeyUsage(_, {bad_cert, _} = Reason, _UserState) ->
%% OTP verify_peer default
{fail, Reason};
verify_fun_peer_extKeyUsage(_, {extension, _}, UserState) ->
%% OTP verify_peer default
{unknown, UserState};
verify_fun_peer_extKeyUsage(_, valid, UserState) ->
%% OTP verify_peer default
{valid, UserState};
verify_fun_peer_extKeyUsage(
#'OTPCertificate'{tbsCertificate = #'OTPTBSCertificate'{extensions = ExtL}},
%% valid peer cert
valid_peer,
AllowedKeyUsages
) ->
%% override OTP verify_peer default
%% must have id-ce-extKeyUsage
case lists:keyfind(?'id-ce-extKeyUsage', 2, ExtL) of
#'Extension'{extnID = ?'id-ce-extKeyUsage', extnValue = VL} ->
case do_verify_ext_key_usage(VL, AllowedKeyUsages) of
true ->
%% pass the check,
%% fallback to OTP verify_peer default
{valid, AllowedKeyUsages};
false ->
{fail, extKeyUsage_unmatched}
end;
_ ->
{fail, extKeyUsage_not_set}
end.

%% @doc check required extkeyUsages are presented in the cert
do_verify_ext_key_usage(_, []) ->
%% Verify finished
true;
do_verify_ext_key_usage(CertExtL, [Usage | T] = _Required) ->
case lists:member(Usage, CertExtL) of
true ->
do_verify_ext_key_usage(CertExtL, T);
false ->
false
end.

%% @doc Helper tls cert extension
-spec ext_key_opts
(string()) -> [OidString :: string() | public_key:oid()];
(undefined) -> undefined.
ext_key_opts(Str) ->
Usages = string:tokens(Str, ","),
lists:map(
fun
("clientAuth") ->
?'id-kp-clientAuth';
("serverAuth") ->
?'id-kp-serverAuth';
("codeSigning") ->
?'id-kp-codeSigning';
("emailProtection") ->
?'id-kp-emailProtection';
("timeStamping") ->
?'id-kp-timeStamping';
("ocspSigning") ->
?'id-kp-OCSPSigning';
([$O, $I, $D, $: | OidStr]) ->
OidList = string:tokens(OidStr, "."),
list_to_tuple(lists:map(fun list_to_integer/1, OidList))
end,
Usages
).
28 changes: 25 additions & 3 deletions apps/emqx/src/emqx_listeners.erl
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,9 @@ esockd_opts(ListenerId, Type, Name, Opts0) ->
ssl ->
OptsWithCRL = inject_crl_config(Opts0),
OptsWithSNI = inject_sni_fun(ListenerId, OptsWithCRL),
SSLOpts = ssl_opts(OptsWithSNI),
OptsWithRootFun = inject_root_fun(OptsWithSNI),
OptsWithVerifyFun = inject_verify_fun(OptsWithRootFun),
SSLOpts = ssl_opts(OptsWithVerifyFun),
Opts3#{ssl_options => SSLOpts, tcp_options => tcp_opts(Opts0)}
end
).
Expand All @@ -634,8 +636,18 @@ ranch_opts(Type, Opts = #{bind := ListenOn}) ->
MaxConnections = maps:get(max_connections, Opts, 1024),
SocketOpts =
case Type of
wss -> tcp_opts(Opts) ++ proplists:delete(handshake_timeout, ssl_opts(Opts));
ws -> tcp_opts(Opts)
wss ->
tcp_opts(Opts) ++
lists:filter(
fun
({partial_chain, _}) -> false;
({handshake_timeout, _}) -> false;
(_) -> true
end,
ssl_opts(Opts)
);
ws ->
tcp_opts(Opts)
end,
#{
num_acceptors => NumAcceptors,
Expand Down Expand Up @@ -956,6 +968,16 @@ quic_listener_optional_settings() ->
stateless_operation_expiration_ms
].

inject_root_fun(#{ssl_options := SslOpts} = Opts) ->
Opts#{ssl_options := emqx_tls_lib:opt_partial_chain(SslOpts)};
inject_root_fun(Opts) ->
Opts.

inject_verify_fun(#{ssl_options := SslOpts} = Opts) ->
Opts#{ssl_options := emqx_tls_lib:opt_verify_fun(SslOpts)};
inject_verify_fun(Opts) ->
Opts.

inject_sni_fun(ListenerId, Conf = #{ssl_options := #{ocsp := #{enable_ocsp_stapling := true}}}) ->
emqx_ocsp_cache:inject_sni_fun(ListenerId, Conf);
inject_sni_fun(_ListenerId, Conf) ->
Expand Down
16 changes: 16 additions & 0 deletions apps/emqx/src/emqx_schema.erl
Original file line number Diff line number Diff line change
Expand Up @@ -2109,6 +2109,22 @@ common_ssl_opts_schema(Defaults, Type) ->
desc => ?DESC(common_ssl_opts_schema_verify)
}
)},
{"partial_chain",
sc(
hoconsc:enum([true, false, two_cacerts_from_cacertfile, cacert_from_cacertfile]),
#{
default => Df(partial_chain, false),
desc => ?DESC(common_ssl_opts_schema_partial_chain)
}
)},
{"verify_peer_ext_key_usage",
sc(
string(),
#{
required => false,
desc => ?DESC(common_ssl_opts_verify_peer_ext_key_usage)
}
)},
{"reuse_sessions",
sc(
boolean(),
Expand Down
55 changes: 55 additions & 0 deletions apps/emqx/src/emqx_tls_lib.erl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
%%--------------------------------------------------------------------

-module(emqx_tls_lib).
-elvis([{elvis_style, atom_naming_convention, #{regex => "^([a-z][a-z0-9A-Z]*_?)*(_SUITE)?$"}}]).

%% version & cipher suites
-export([
Expand All @@ -23,6 +24,8 @@
default_ciphers/0,
selected_ciphers/1,
integral_ciphers/2,
opt_partial_chain/1,
opt_verify_fun/1,
all_ciphers_set_cached/0
]).

Expand Down Expand Up @@ -679,3 +682,55 @@ ensure_ssl_file_key(SSL, RequiredKeyPaths) ->
[] -> ok;
Miss -> {error, #{reason => ssl_file_option_not_found, which_options => Miss}}
end.

%% @doc enable TLS partial_chain validation if set.
-spec opt_partial_chain(SslOpts :: map()) -> NewSslOpts :: map().
opt_partial_chain(#{partial_chain := false} = SslOpts) ->
maps:remove(partial_chain, SslOpts);
opt_partial_chain(#{partial_chain := true} = SslOpts) ->
SslOpts#{partial_chain := rootfun_trusted_ca_from_cacertfile(1, SslOpts)};
opt_partial_chain(#{partial_chain := cacert_from_cacertfile} = SslOpts) ->
SslOpts#{partial_chain := rootfun_trusted_ca_from_cacertfile(1, SslOpts)};
opt_partial_chain(#{partial_chain := two_cacerts_from_cacertfile} = SslOpts) ->
SslOpts#{partial_chain := rootfun_trusted_ca_from_cacertfile(2, SslOpts)};
opt_partial_chain(SslOpts) ->
SslOpts.

%% @doc make verify_fun if set.
-spec opt_verify_fun(SslOpts :: map()) -> NewSslOpts :: map().
opt_verify_fun(#{verify_peer_ext_key_usage := V} = SslOpts) when V =/= undefined ->
SslOpts#{verify_fun => emqx_const_v2:make_tls_verify_fun(verify_cert_extKeyUsage, V)};
opt_verify_fun(SslOpts) ->
SslOpts.

%% @doc Helper, make TLS root_fun
rootfun_trusted_ca_from_cacertfile(NumOfCerts, #{cacertfile := Cacertfile}) ->
case file:read_file(Cacertfile) of
{ok, PemBin} ->
try
do_rootfun_trusted_ca_from_cacertfile(NumOfCerts, PemBin)
catch
_Error:_Info:ST ->
%% The cacertfile will be checked by OTP SSL as well and OTP choice to be silent on this.
%% We are touching security sutffs, don't leak extra info..
?SLOG(error, #{
msg => "trusted_cacert_not_found_in_cacertfile", stacktrace => ST
}),
throw({error, ?FUNCTION_NAME})
end;
{error, Reason} ->
throw({error, {read_cacertfile_error, Cacertfile, Reason}})
end;
rootfun_trusted_ca_from_cacertfile(_NumOfCerts, _SslOpts) ->
throw({error, cacertfile_unset}).

do_rootfun_trusted_ca_from_cacertfile(NumOfCerts, PemBin) ->
%% The last one or two should be the top parent in the chain if it is a chain
Certs = public_key:pem_decode(PemBin),
Pos = length(Certs) - NumOfCerts + 1,
Trusted = [
CADer
|| {'Certificate', CADer, _} <-
lists:sublist(public_key:pem_decode(PemBin), Pos, NumOfCerts)
],
emqx_const_v2:make_tls_root_fun(cacert_from_cacertfile, Trusted).
Loading

0 comments on commit 3b1296d

Please sign in to comment.