Revert: TLS partial-chain and keyUsage #12955 #12977

This reverts commit 28b17a2562.
This reverts commit 01467246fc.
This reverts commit c3f8ba5762.
This reverts commit 1a4a4bb3a5.
This reverts commit fb30207ef3.
This reverts commit 337c230e79.
This reverts commit 3a674f44f1.
This reverts commit 70ffd77f99.
This reverts commit 03b0935564.
This reverts commit 650cf4b27e.
This reverts commit 43ad665dcf.
This reverts commit a29a43e5fc.
This reverts commit 4e9c1ec0c9.
This reverts commit 8eb463c58d.
This reverts commit 90430fa66d.
This reverts commit eb1ab9adfe.
This reverts commit 8bc3a86f63.
This reverts commit fa4357ce89.
This reverts commit 0b95a08d32.
This commit is contained in:
William Yang 2024-06-13 13:31:58 +02:00
parent 1dd4b6de5e
commit 1664ea4ad4
15 changed files with 5 additions and 1956 deletions

View File

@ -1,125 +0,0 @@
%%--------------------------------------------------------------------
%% 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) ->
RequiredKeyUsages = ext_key_opts(KeyUsages),
{fun verify_fun_peer_extKeyUsage/3, RequiredKeyUsages}.
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,
RequiredKeyUsages
) ->
%% 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, RequiredKeyUsages) of
true ->
%% pass the check,
%% fallback to OTP verify_peer default
{valid, RequiredKeyUsages};
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()].
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';
("OID:" ++ OidStr) ->
OidList = string:tokens(OidStr, "."),
list_to_tuple(lists:map(fun list_to_integer/1, OidList))
end,
Usages
).

View File

@ -611,9 +611,7 @@ esockd_opts(ListenerId, Type, Name, Opts0) ->
ssl ->
OptsWithCRL = inject_crl_config(Opts0),
OptsWithSNI = inject_sni_fun(ListenerId, OptsWithCRL),
OptsWithRootFun = inject_root_fun(OptsWithSNI),
OptsWithVerifyFun = inject_verify_fun(OptsWithRootFun),
SSLOpts = ssl_opts(OptsWithVerifyFun),
SSLOpts = ssl_opts(OptsWithSNI),
Opts3#{ssl_options => SSLOpts, tcp_options => tcp_opts(Opts0)}
end
).
@ -637,18 +635,8 @@ ranch_opts(Type, Opts = #{bind := ListenOn}) ->
MaxConnections = maps:get(max_connections, Opts, 1024),
SocketOpts =
case Type of
wss ->
tcp_opts(Opts) ++
lists:filter(
fun
({partial_chain, _}) -> false;
({handshake_timeout, _}) -> false;
(_) -> true
end,
ssl_opts(Opts)
);
ws ->
tcp_opts(Opts)
wss -> tcp_opts(Opts) ++ proplists:delete(handshake_timeout, ssl_opts(Opts));
ws -> tcp_opts(Opts)
end,
#{
num_acceptors => NumAcceptors,
@ -974,16 +962,6 @@ 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) ->

View File

@ -2178,22 +2178,6 @@ 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(),

View File

@ -15,7 +15,6 @@
%%--------------------------------------------------------------------
-module(emqx_tls_lib).
-elvis([{elvis_style, atom_naming_convention, #{regex => "^([a-z][a-z0-9A-Z]*_?)*(_SUITE)?$"}}]).
%% version & cipher suites
-export([
@ -24,8 +23,6 @@
default_ciphers/0,
selected_ciphers/1,
integral_ciphers/2,
opt_partial_chain/1,
opt_verify_fun/1,
all_ciphers_set_cached/0
]).
@ -688,55 +685,3 @@ 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).

View File

@ -1,257 +0,0 @@
%%--------------------------------------------------------------------
%% 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.
%%--------------------------------------------------------------------
-module(emqx_listener_tls_verify_chain_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-import(
emqx_test_tls_certs_helper,
[
emqx_start_listener/4,
fail_when_ssl_error/1,
fail_when_no_ssl_alert/2,
generate_tls_certs/1
]
).
all() -> emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) ->
generate_tls_certs(Config),
application:ensure_all_started(esockd),
[{ssl_config, ssl_config_verify_peer()} | Config].
end_per_suite(_Config) ->
application:stop(esockd).
t_conn_fail_with_intermediate_ca_cert(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options, [
{cacertfile, filename:join(DataDir, "intermediate1.pem")},
{certfile, filename:join(DataDir, "server1.pem")},
{keyfile, filename:join(DataDir, "server1.key")}
| ?config(ssl_config, Config)
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client1.key")},
{certfile, filename:join(DataDir, "client1.pem")},
{verify, verify_none}
],
1000
),
fail_when_no_ssl_alert(Socket, unknown_ca),
ok = ssl:close(Socket).
t_conn_fail_with_other_intermediate_ca_cert(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options, [
{cacertfile, filename:join(DataDir, "intermediate1.pem")},
{certfile, filename:join(DataDir, "server1.pem")},
{keyfile, filename:join(DataDir, "server1.key")}
| ?config(ssl_config, Config)
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2.pem")},
{verify, verify_none}
],
1000
),
fail_when_no_ssl_alert(Socket, unknown_ca),
ok = ssl:close(Socket).
t_conn_success_with_server_client_composed_complete_chain(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
%% Server has root ca cert
Options = [
{ssl_options, [
{cacertfile, filename:join(DataDir, "root.pem")},
{certfile, filename:join(DataDir, "server2.pem")},
{keyfile, filename:join(DataDir, "server2.key")}
| ?config(ssl_config, Config)
]}
],
%% Client has complete chain
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")},
{verify, verify_none}
],
1000
),
fail_when_ssl_error(Socket),
ok = ssl:close(Socket).
t_conn_success_with_other_signed_client_composed_complete_chain(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
%% Server has root ca cert
Options = [
{ssl_options, [
{cacertfile, filename:join(DataDir, "root.pem")},
{certfile, filename:join(DataDir, "server1.pem")},
{keyfile, filename:join(DataDir, "server1.key")}
| ?config(ssl_config, Config)
]}
],
%% Client has partial_chain
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")},
{verify, verify_none}
],
1000
),
fail_when_ssl_error(Socket),
ok = ssl:close(Socket).
t_conn_success_with_renewed_intermediate_root_bundle(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
%% Server has root ca cert
Options = [
{ssl_options, [
{cacertfile, filename:join(DataDir, "intermediate1_renewed-root-bundle.pem")},
{certfile, filename:join(DataDir, "server1.pem")},
{keyfile, filename:join(DataDir, "server1.key")}
| ?config(ssl_config, Config)
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client1.key")},
{certfile, filename:join(DataDir, "client1.pem")},
{verify, verify_none}
],
1000
),
fail_when_ssl_error(Socket),
ok = ssl:close(Socket).
t_conn_success_with_client_complete_cert_chain(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options, [
{cacertfile, filename:join(DataDir, "root.pem")},
{certfile, filename:join(DataDir, "server2.pem")},
{keyfile, filename:join(DataDir, "server2.key")}
| ?config(ssl_config, Config)
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2-complete-bundle.pem")},
{verify, verify_none}
],
1000
),
fail_when_ssl_error(Socket),
ok = ssl:close(Socket).
t_conn_fail_with_server_partial_chain(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
%% imcomplete at server side
Options = [
{ssl_options, [
{cacertfile, filename:join(DataDir, "intermediate2.pem")},
{certfile, filename:join(DataDir, "server2.pem")},
{keyfile, filename:join(DataDir, "server2.key")}
| ?config(ssl_config, Config)
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
Res = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2-complete-bundle.pem")},
{versions, ['tlsv1.2']},
{verify, verify_none}
],
1000
),
fail_when_no_ssl_alert(Res, unknown_ca).
t_conn_fail_without_root_cacert(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options, [
{cacertfile, filename:join(DataDir, "intermediate2.pem")},
{certfile, filename:join(DataDir, "server2.pem")},
{keyfile, filename:join(DataDir, "server2.key")}
| ?config(ssl_config, Config)
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
Res = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")},
%% stick to tlsv1.2 for consistent error message
{versions, ['tlsv1.2']},
{cacertfile, filename:join(DataDir, "intermediate2.pem")}
],
1000
),
fail_when_no_ssl_alert(Res, unknown_ca).
ssl_config_verify_peer() ->
[
{verify, verify_peer},
{fail_if_no_peer_cert, true}
].

View File

@ -1,372 +0,0 @@
%%--------------------------------------------------------------------
%% 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.
%%--------------------------------------------------------------------
-module(emqx_listener_tls_verify_keyusage_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-import(
emqx_test_tls_certs_helper,
[
fail_when_ssl_error/1,
fail_when_no_ssl_alert/2,
generate_tls_certs/1,
gen_host_cert/4,
emqx_start_listener/4
]
).
all() ->
[
{group, full_chain},
{group, partial_chain}
].
all_tc() ->
emqx_common_test_helpers:all(?MODULE).
groups() ->
[
{partial_chain, [], all_tc()},
{full_chain, [], all_tc()}
].
init_per_suite(Config) ->
generate_tls_certs(Config),
application:ensure_all_started(esockd),
Config.
end_per_suite(_Config) ->
application:stop(esockd).
init_per_group(full_chain, Config) ->
[{ssl_config, ssl_config_verify_peer_full_chain(Config)} | Config];
init_per_group(partial_chain, Config) ->
[{ssl_config, ssl_config_verify_peer_partial_chain(Config)} | Config];
init_per_group(_, Config) ->
Config.
end_per_group(_, Config) ->
Config.
t_conn_success_verify_peer_ext_key_usage_unset(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
%% Given listener keyusage unset
Options = [{ssl_options, ?config(ssl_config, Config)}],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
%% when client connect with cert without keyusage ext
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client1.key")},
{certfile, filename:join(DataDir, "client1.pem")},
{verify, verify_none}
],
1000
),
%% Then connection success
fail_when_ssl_error(Socket),
ok = ssl:close(Socket).
t_conn_success_verify_peer_ext_key_usage_undefined(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
%% Give listener keyusage is set to undefined
Options = [
{ssl_options, [
{verify_peer_ext_key_usage, undefined}
| ?config(ssl_config, Config)
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
%% when client connect with cert without keyusages ext
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client1.key")},
{certfile, filename:join(DataDir, "client1.pem")},
{verify, verify_none}
],
1000
),
%% Then connection success
fail_when_ssl_error(Socket),
ok = ssl:close(Socket).
t_conn_success_verify_peer_ext_key_usage_matched_predefined(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
%% Give listener keyusage is set to clientAuth
Options = [
{ssl_options, [
{verify_peer_ext_key_usage, "clientAuth"}
| ?config(ssl_config, Config)
]}
],
%% When client cert has clientAuth that is matched
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth"),
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, client_key_file(DataDir, ?FUNCTION_NAME)},
{certfile, client_pem_file(DataDir, ?FUNCTION_NAME)},
{verify, verify_none}
],
1000
),
%% Then connection success
fail_when_ssl_error(Socket),
ok = ssl:close(Socket).
t_conn_success_verify_peer_ext_key_usage_matched_raw_oid(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
%% Give listener keyusage is set to raw OID
%% from OTP-PUB-KEY.hrl
Options = [
{ssl_options, [
{verify_peer_ext_key_usage, "OID:1.3.6.1.5.5.7.3.2"}
| ?config(ssl_config, Config)
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
%% When client cert has keyusage and matched.
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth"),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, client_key_file(DataDir, ?FUNCTION_NAME)},
{certfile, client_pem_file(DataDir, ?FUNCTION_NAME)},
{verify, verify_none}
],
1000
),
%% Then connection success
fail_when_ssl_error(Socket),
ok = ssl:close(Socket).
t_conn_success_verify_peer_ext_key_usage_matched_ordered_list(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
%% Give listener keyusage is clientAuth,serverAuth
Options = [
{ssl_options, [
{verify_peer_ext_key_usage, "clientAuth,serverAuth"}
| ?config(ssl_config, Config)
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
%% When client cert has the same keyusage ext list
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth,serverAuth"),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, client_key_file(DataDir, ?FUNCTION_NAME)},
{certfile, client_pem_file(DataDir, ?FUNCTION_NAME)},
{verify, verify_none}
],
1000
),
%% Then connection success
fail_when_ssl_error(Socket),
ok = ssl:close(Socket).
t_conn_success_verify_peer_ext_key_usage_matched_unordered_list(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
%% Give listener keyusage is clientAuth,serverAuth
Options = [
{ssl_options, [
{verify_peer_ext_key_usage, "serverAuth,clientAuth"}
| ?config(ssl_config, Config)
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
%% When client cert has the same keyusage ext list but different order
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth,serverAuth"),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, client_key_file(DataDir, ?FUNCTION_NAME)},
{certfile, client_pem_file(DataDir, ?FUNCTION_NAME)},
{verify, verify_none}
],
1000
),
%% Then connection success
fail_when_ssl_error(Socket),
ok = ssl:close(Socket).
t_conn_fail_verify_peer_ext_key_usage_unmatched_raw_oid(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
%% Give listener keyusage is using OID
Options = [
{ssl_options, [
{verify_peer_ext_key_usage, "OID:1.3.6.1.5.5.7.3.1"}
| ?config(ssl_config, Config)
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
%% When client cert has the keyusage but not matching OID
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth"),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, client_key_file(DataDir, ?FUNCTION_NAME)},
{certfile, client_pem_file(DataDir, ?FUNCTION_NAME)},
{verify, verify_none}
],
1000
),
%% Then connecion should fail.
fail_when_no_ssl_alert(Socket, handshake_failure),
ok = ssl:close(Socket).
t_conn_fail_verify_peer_ext_key_usage_empty_str(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options, [
{verify_peer_ext_key_usage, ""}
| ?config(ssl_config, Config)
]}
],
%% Give listener keyusage is empty string
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
%% When client connect with cert without keyusage
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client1.key")},
{certfile, filename:join(DataDir, "client1.pem")},
{verify, verify_none}
],
1000
),
%% Then connecion should fail.
fail_when_no_ssl_alert(Socket, handshake_failure),
ok = ssl:close(Socket).
t_conn_fail_client_keyusage_unmatch(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
%% Give listener keyusage is clientAuth
Options = [
{ssl_options, [
{verify_peer_ext_key_usage, "clientAuth"}
| ?config(ssl_config, Config)
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
%% When client connect with mismatch cert keyusage = codeSigning
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "codeSigning"),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, client_key_file(DataDir, ?FUNCTION_NAME)},
{certfile, client_pem_file(DataDir, ?FUNCTION_NAME)},
{verify, verify_none}
],
1000
),
%% Then connecion should fail.
fail_when_no_ssl_alert(Socket, handshake_failure),
ok = ssl:close(Socket).
t_conn_fail_client_keyusage_incomplete(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
%% Give listener keyusage is codeSigning,clientAuth
Options = [
{ssl_options, [
{verify_peer_ext_key_usage,
"serverAuth,clientAuth,codeSigning,emailProtection,timeStamping,ocspSigning"}
| ?config(ssl_config, Config)
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
%% When client connect with cert keyusage = clientAuth
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "codeSigning"),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client1.key")},
{certfile, filename:join(DataDir, "client1.pem")},
{verify, verify_none}
],
1000
),
%% Then connection should fail
fail_when_no_ssl_alert(Socket, handshake_failure),
ok = ssl:close(Socket).
%%%
%%% Helpers
%%%
gen_client_cert_ext_keyusage(Name, CA, DataDir, Usage) when is_atom(Name) ->
gen_client_cert_ext_keyusage(atom_to_list(Name), CA, DataDir, Usage);
gen_client_cert_ext_keyusage(Name, CA, DataDir, Usage) ->
gen_host_cert(Name, CA, DataDir, #{ext => "extendedKeyUsage=" ++ Usage}).
client_key_file(DataDir, Name) ->
filename:join(DataDir, Name) ++ ".key".
client_pem_file(DataDir, Name) ->
filename:join(DataDir, Name) ++ ".pem".
ssl_config_verify_peer_full_chain(Config) ->
[
{cacertfile, filename:join(?config(data_dir, Config), "intermediate1-root-bundle.pem")}
| ssl_config_verify_peer(Config)
].
ssl_config_verify_peer_partial_chain(Config) ->
[
{cacertfile, filename:join(?config(data_dir, Config), "intermediate1.pem")},
{partial_chain, true}
| ssl_config_verify_peer(Config)
].
ssl_config_verify_peer(Config) ->
DataDir = ?config(data_dir, Config),
[
{verify, verify_peer},
{fail_if_no_peer_cert, true},
{keyfile, filename:join(DataDir, "server1.key")},
{certfile, filename:join(DataDir, "server1.pem")}
%% , {log_level, debug}
].

View File

@ -1,708 +0,0 @@
%%--------------------------------------------------------------------
%% 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.
%%--------------------------------------------------------------------
-module(emqx_listener_tls_verify_partial_chain_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-import(
emqx_test_tls_certs_helper,
[
emqx_start_listener/4,
fail_when_ssl_error/1,
fail_when_no_ssl_alert/2,
generate_tls_certs/1
]
).
all() -> emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) ->
generate_tls_certs(Config),
application:ensure_all_started(esockd),
[{ssl_config, ssl_config_verify_partial_chain()} | Config].
end_per_suite(_Config) ->
application:stop(esockd).
t_conn_success_with_server_intermediate_cacert_and_client_cert(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate1.pem")},
{certfile, filename:join(DataDir, "server1.pem")},
{keyfile, filename:join(DataDir, "server1.key")}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client1.key")},
{certfile, filename:join(DataDir, "client1.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_ssl_error(Socket),
ssl:close(Socket).
t_conn_success_with_intermediate_cacert_bundle(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "server1-intermediate1-bundle.pem")},
{certfile, filename:join(DataDir, "server1.pem")},
{keyfile, filename:join(DataDir, "server1.key")}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client1.key")},
{certfile, filename:join(DataDir, "client1.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_ssl_error(Socket),
ssl:close(Socket).
t_conn_success_with_renewed_intermediate_cacert(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate1_renewed.pem")},
{certfile, filename:join(DataDir, "server1.pem")},
{keyfile, filename:join(DataDir, "server1.key")}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client1.key")},
{certfile, filename:join(DataDir, "client1.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_ssl_error(Socket),
ssl:close(Socket).
t_conn_fail_with_renewed_intermediate_cacert_and_client_using_old_complete_bundle(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate2_renewed.pem")},
{certfile, filename:join(DataDir, "server2.pem")},
{keyfile, filename:join(DataDir, "server2.key")}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
Res = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2-complete-bundle.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_no_ssl_alert(Res, unknown_ca).
t_conn_fail_with_renewed_intermediate_cacert_and_client_using_old_bundle(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate2_renewed.pem")},
{certfile, filename:join(DataDir, "server2.pem")},
{keyfile, filename:join(DataDir, "server2.key")}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
Res = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_no_ssl_alert(Res, unknown_ca).
t_conn_success_with_old_and_renewed_intermediate_cacert_and_client_provides_renewed_client_cert(
Config
) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")},
{certfile, filename:join(DataDir, "server2.pem")},
{keyfile, filename:join(DataDir, "server2.key")},
{partial_chain, two_cacerts_from_cacertfile}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2_renewed.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_ssl_error(Socket),
ssl:close(Socket).
%% Note, this is good to have for usecase coverage
t_conn_success_with_new_intermediate_cacert_and_client_provides_renewed_client_cert_signed_by_old_intermediate(
Config
) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate2_renewed.pem")},
{certfile, filename:join(DataDir, "server2.pem")},
{keyfile, filename:join(DataDir, "server2.key")}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2_renewed.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_ssl_error(Socket),
ssl:close(Socket).
%% @doc server should build a partial_chain with old version of ca cert.
t_conn_success_with_old_and_renewed_intermediate_cacert_and_client_provides_client_cert(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")},
{certfile, filename:join(DataDir, "server2.pem")},
{keyfile, filename:join(DataDir, "server2.key")},
{partial_chain, two_cacerts_from_cacertfile}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_ssl_error(Socket),
ssl:close(Socket).
%% @doc verify when config does not allow two versions of certs from same trusted CA.
t_conn_fail_with_renewed_and_old_intermediate_cacert_and_client_using_old_bundle(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")},
{certfile, filename:join(DataDir, "server2.pem")},
{keyfile, filename:join(DataDir, "server2.key")}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
Res = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_no_ssl_alert(Res, unknown_ca).
%% @doc verify when config (two_cacerts_from_cacertfile) allows two versions of certs from same trusted CA.
t_001_conn_success_with_old_and_renewed_intermediate_cacert_bundle_and_client_using_old_bundle(
Config
) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")},
{certfile, filename:join(DataDir, "server2.pem")},
{keyfile, filename:join(DataDir, "server2.key")},
{partial_chain, two_cacerts_from_cacertfile}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_ssl_error(Socket),
ssl:close(Socket).
%% @doc: verify even if listener has old/new intermediate2 certs,
%% client1 should not able to connect with old intermediate2 cert.
%% In this case, listener verify_fun returns {trusted_ca, Oldintermediate2Cert} but
%% OTP should still fail the validation since the client1 cert is not signed by
%% Oldintermediate2Cert (trusted CA cert).
%% @end
t_conn_fail_with_old_and_renewed_intermediate_cacert_bundle_and_client_using_all_CAcerts(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")},
{certfile, filename:join(DataDir, "server2.pem")},
{keyfile, filename:join(DataDir, "server2.key")},
{partial_chain, two_cacerts_from_cacertfile}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
Res = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client1.key")},
{certfile, filename:join(DataDir, "all-CAcerts-bundle.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_no_ssl_alert(Res, unknown_ca).
t_conn_fail_with_renewed_intermediate_cacert_other_client(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate1_renewed.pem")},
{certfile, filename:join(DataDir, "server1.pem")},
{keyfile, filename:join(DataDir, "server1.key")}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
Res = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_no_ssl_alert(Res, unknown_ca).
t_conn_fail_with_intermediate_cacert_bundle_but_incorrect_order(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate1-server1-bundle.pem")},
{certfile, filename:join(DataDir, "server1.pem")},
{keyfile, filename:join(DataDir, "server1.key")}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
Res = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client1.key")},
{certfile, filename:join(DataDir, "client1.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_no_ssl_alert(Res, unknown_ca).
t_conn_fail_when_singed_by_other_intermediate_ca(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate1.pem")},
{certfile, filename:join(DataDir, "server1.pem")},
{keyfile, filename:join(DataDir, "server1.key")}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
Res = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_no_ssl_alert(Res, unknown_ca).
t_conn_success_with_complete_chain_that_server_root_cacert_and_client_complete_cert_chain(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "root.pem")},
{certfile, filename:join(DataDir, "server2.pem")},
{keyfile, filename:join(DataDir, "server2.key")}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2-complete-bundle.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_ssl_error(Socket),
ok = ssl:close(Socket).
t_conn_fail_with_other_client_complete_cert_chain(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate1.pem")},
{certfile, filename:join(DataDir, "server1.pem")},
{keyfile, filename:join(DataDir, "server1.key")}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
Res = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2-complete-bundle.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_no_ssl_alert(Res, unknown_ca).
t_conn_fail_with_server_intermediate_and_other_client_complete_cert_chain(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate1-root-bundle.pem")},
{certfile, filename:join(DataDir, "server1.pem")},
{keyfile, filename:join(DataDir, "server1.key")}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2-complete-bundle.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_ssl_error(Socket),
ok = ssl:close(Socket).
t_conn_success_with_server_intermediate_cacert_and_client_complete_chain(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate2.pem")},
{certfile, filename:join(DataDir, "server2.pem")},
{keyfile, filename:join(DataDir, "server2.key")}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2-complete-bundle.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_ssl_error(Socket),
ok = ssl:close(Socket).
t_conn_fail_with_server_intermediate_chain_and_client_other_incomplete_cert_chain(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate1.pem")},
{certfile, filename:join(DataDir, "server1.pem")},
{keyfile, filename:join(DataDir, "server1.key")}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
Res = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_no_ssl_alert(Res, unknown_ca).
t_conn_fail_with_server_intermediate_and_other_client_root_chain(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate1.pem")},
{certfile, filename:join(DataDir, "server1.pem")},
{keyfile, filename:join(DataDir, "server1.key")}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
Res = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2-root-bundle.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_no_ssl_alert(Res, unknown_ca).
t_conn_success_with_server_intermediate_and_client_root_chain(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate2.pem")},
{certfile, filename:join(DataDir, "server2.pem")},
{keyfile, filename:join(DataDir, "server2.key")}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2-root-bundle.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_ssl_error(Socket),
ok = ssl:close(Socket).
%% @doc once rootCA cert present in cacertfile, sibling CA signed Client cert could connect.
t_conn_success_with_server_all_CA_bundle_and_client_root_chain(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "all-CAcerts-bundle.pem")},
{certfile, filename:join(DataDir, "server1.pem")},
{keyfile, filename:join(DataDir, "server1.key")}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
{ok, Socket} = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2-root-bundle.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_ssl_error(Socket),
ok = ssl:close(Socket).
t_conn_fail_with_server_two_IA_bundle_and_client_root_chain(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "two-intermediates-bundle.pem")},
{certfile, filename:join(DataDir, "server1.pem")},
{keyfile, filename:join(DataDir, "server1.key")}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
Res = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client2.key")},
{certfile, filename:join(DataDir, "client2-root-bundle.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_no_ssl_alert(Res, unknown_ca).
t_conn_fail_with_server_partial_chain_false_intermediate_cacert_and_client_cert(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "intermediate1.pem")},
{certfile, filename:join(DataDir, "server1.pem")},
{keyfile, filename:join(DataDir, "server1.key")},
{partial_chain, false}
]}
],
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
Res = ssl:connect(
{127, 0, 0, 1},
Port,
[
{keyfile, filename:join(DataDir, "client1.key")},
{certfile, filename:join(DataDir, "client1.pem")}
| client_default_tls_opts()
],
1000
),
fail_when_no_ssl_alert(Res, unknown_ca).
t_error_handling_invalid_cacertfile(Config) ->
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
DataDir = ?config(data_dir, Config),
%% trigger error
Options = [
{ssl_options,
?config(ssl_config, Config) ++
[
{cacertfile, filename:join(DataDir, "server2.key")},
{certfile, filename:join(DataDir, "server2.pem")},
{keyfile, filename:join(DataDir, "server2.key")}
]}
],
?assertException(
throw,
{error, rootfun_trusted_ca_from_cacertfile},
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options)
).
ssl_config_verify_partial_chain() ->
[
{verify, verify_peer},
{fail_if_no_peer_cert, true},
{partial_chain, true}
].
client_default_tls_opts() ->
[
{versions, ['tlsv1.2']},
{verify, verify_none}
].

View File

@ -1,319 +0,0 @@
%%--------------------------------------------------------------------
%% 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.
%%--------------------------------------------------------------------
-module(emqx_test_tls_certs_helper).
-export([
gen_ca/2,
gen_host_cert/3,
gen_host_cert/4,
select_free_port/1,
generate_tls_certs/1,
fail_when_ssl_error/1,
fail_when_ssl_error/2,
fail_when_no_ssl_alert/2,
fail_when_no_ssl_alert/3,
emqx_start_listener/4
]).
-include_lib("common_test/include/ct.hrl").
%%-------------------------------------------------------------------------------
%% Start Listener
%%-------------------------------------------------------------------------------
emqx_start_listener(Name, Type, Port, Opts) when is_list(Opts) ->
emqx_start_listener(Name, Type, Port, maps:from_list(Opts));
emqx_start_listener(Name, ssl, Port, #{ssl_options := SslOptions} = Opts0) ->
Opts = Opts0#{
enable => true,
bind => {{127, 0, 0, 1}, Port},
mountpoint => <<>>,
zone => default,
ssl_options => maps:from_list(SslOptions)
},
ct:pal("start listener with ~p ~p", [Name, Opts]),
emqx_listeners:start_listener(ssl, Name, Opts).
%%-------------------------------------------------------------------------------
%% TLS certs
%%-------------------------------------------------------------------------------
gen_ca(Path, Name) ->
%% Generate ca.pem and ca.key which will be used to generate certs
%% for hosts server and clients
ECKeyFile = eckey_name(Path),
filelib:ensure_dir(ECKeyFile),
os:cmd("openssl ecparam -name secp256r1 > " ++ ECKeyFile),
Cmd = lists:flatten(
io_lib:format(
"openssl req -new -x509 -nodes "
"-newkey ec:~s "
"-keyout ~s -out ~s -days 3650 "
"-addext basicConstraints=CA:TRUE "
"-subj \"/C=SE/O=TEST CA\"",
[
ECKeyFile,
ca_key_name(Path, Name),
ca_cert_name(Path, Name)
]
)
),
os:cmd(Cmd).
ca_cert_name(Path, Name) ->
filename(Path, "~s.pem", [Name]).
ca_key_name(Path, Name) ->
filename(Path, "~s.key", [Name]).
eckey_name(Path) ->
filename(Path, "ec.key", []).
gen_host_cert(H, CaName, Path) ->
gen_host_cert(H, CaName, Path, #{}).
gen_host_cert(H, CaName, Path, Opts) ->
ECKeyFile = eckey_name(Path),
CN = str(H),
HKey = filename(Path, "~s.key", [H]),
HCSR = filename(Path, "~s.csr", [H]),
HCSR2 = filename(Path, "~s.csr", [H]),
HPEM = filename(Path, "~s.pem", [H]),
HPEM2 = filename(Path, "~s_renewed.pem", [H]),
HEXT = filename(Path, "~s.extfile", [H]),
PasswordArg =
case maps:get(password, Opts, undefined) of
undefined ->
" -nodes ";
Password ->
io_lib:format(" -passout pass:'~s' ", [Password])
end,
create_file(
HEXT,
"keyUsage=digitalSignature,keyAgreement,keyCertSign\n"
"basicConstraints=CA:TRUE \n"
"~s \n"
"subjectAltName=DNS:~s\n",
[maps:get(ext, Opts, ""), CN]
),
CSR_Cmd = csr_cmd(PasswordArg, ECKeyFile, HKey, HCSR, CN),
CSR_Cmd2 = csr_cmd(PasswordArg, ECKeyFile, HKey, HCSR2, CN),
CERT_Cmd = cert_sign_cmd(
HEXT, HCSR, ca_cert_name(Path, CaName), ca_key_name(Path, CaName), HPEM
),
%% 2nd cert for testing renewed cert.
CERT_Cmd2 = cert_sign_cmd(
HEXT, HCSR2, ca_cert_name(Path, CaName), ca_key_name(Path, CaName), HPEM2
),
ct:pal(os:cmd(CSR_Cmd)),
ct:pal(os:cmd(CSR_Cmd2)),
ct:pal(os:cmd(CERT_Cmd)),
ct:pal(os:cmd(CERT_Cmd2)),
file:delete(HEXT).
cert_sign_cmd(ExtFile, CSRFile, CACert, CAKey, OutputCert) ->
lists:flatten(
io_lib:format(
"openssl x509 -req "
"-extfile ~s "
"-in ~s -CA ~s -CAkey ~s -CAcreateserial "
"-out ~s -days 500",
[
ExtFile,
CSRFile,
CACert,
CAKey,
OutputCert
]
)
).
csr_cmd(PasswordArg, ECKeyFile, HKey, HCSR, CN) ->
lists:flatten(
io_lib:format(
"openssl req -new ~s -newkey ec:~s "
"-keyout ~s -out ~s "
"-addext \"subjectAltName=DNS:~s\" "
"-addext basicConstraints=CA:TRUE "
"-addext keyUsage=digitalSignature,keyAgreement,keyCertSign "
"-subj \"/C=SE/O=TEST/CN=~s\"",
[PasswordArg, ECKeyFile, HKey, HCSR, CN, CN]
)
).
filename(Path, F, A) ->
filename:join(Path, str(io_lib:format(F, A))).
str(Arg) ->
binary_to_list(iolist_to_binary(Arg)).
create_file(Filename, Fmt, Args) ->
filelib:ensure_dir(Filename),
{ok, F} = file:open(Filename, [write]),
try
io:format(F, Fmt, Args)
after
file:close(F)
end,
ok.
%% @doc get unused port from OS
-spec select_free_port(tcp | udp | ssl | quic) -> inets:port_number().
select_free_port(tcp) ->
select_free_port(gen_tcp, listen);
select_free_port(udp) ->
select_free_port(gen_udp, open);
select_free_port(ssl) ->
select_free_port(tcp);
select_free_port(quic) ->
select_free_port(udp).
select_free_port(GenModule, Fun) when
GenModule == gen_tcp orelse
GenModule == gen_udp
->
{ok, S} = GenModule:Fun(0, [{reuseaddr, true}]),
{ok, Port} = inet:port(S),
ok = GenModule:close(S),
case os:type() of
{unix, darwin} ->
%% in MacOS, still get address_in_use after close port
timer:sleep(500);
_ ->
skip
end,
ct:pal("Select free OS port: ~p", [Port]),
Port.
%% @doc fail the test if ssl_error recvd
%% post check for success conn establishment
fail_when_ssl_error(Socket) ->
fail_when_ssl_error(Socket, 1000).
fail_when_ssl_error(Socket, Timeout) ->
receive
{ssl_error, Socket, _} ->
ct:fail("Handshake failed!")
after Timeout ->
ok
end.
%% @doc fail the test if no ssl_error
fail_when_no_ssl_alert(Res, Alert) ->
fail_when_no_ssl_alert(Res, Alert, 1000).
fail_when_no_ssl_alert({error, {tls_alert, {Alert, _}}}, Alert, _Timeout) ->
ok;
fail_when_no_ssl_alert({error, _} = Other, Alert, _Timeout) ->
ct:fail("returned unexpected ssl_error: ~p, expected ~n", [Other, Alert]);
fail_when_no_ssl_alert({ok, Socket}, Alert, Timeout) ->
fail_when_no_ssl_alert(Socket, Alert, Timeout);
fail_when_no_ssl_alert(Socket, Alert, Timeout) ->
receive
{ssl_error, Socket, {tls_alert, {Alert, AlertInfo}}} ->
ct:pal("alert info: ~p~n", [AlertInfo]);
{ssl_error, Socket, Other} ->
ct:fail("recv unexpected ssl_error: ~p~n", [Other])
after Timeout ->
ct:fail("No expected alert: ~p from Socket: ~p ", [Alert, Socket])
end.
%% @doc Generate TLS cert chain for tests
generate_tls_certs(Config) ->
DataDir = ?config(data_dir, Config),
gen_ca(DataDir, "root"),
gen_host_cert("intermediate1", "root", DataDir),
gen_host_cert("intermediate2", "root", DataDir),
gen_host_cert("server1", "intermediate1", DataDir),
gen_host_cert("client1", "intermediate1", DataDir),
gen_host_cert("server2", "intermediate2", DataDir),
gen_host_cert("client2", "intermediate2", DataDir),
%% Build bundles below
os:cmd(
io_lib:format("cat ~p ~p ~p > ~p", [
filename:join(DataDir, "client2.pem"),
filename:join(DataDir, "intermediate2.pem"),
filename:join(DataDir, "root.pem"),
filename:join(DataDir, "client2-complete-bundle.pem")
])
),
os:cmd(
io_lib:format("cat ~p ~p > ~p", [
filename:join(DataDir, "client2.pem"),
filename:join(DataDir, "intermediate2.pem"),
filename:join(DataDir, "client2-intermediate2-bundle.pem")
])
),
os:cmd(
io_lib:format("cat ~p ~p > ~p", [
filename:join(DataDir, "client2.pem"),
filename:join(DataDir, "root.pem"),
filename:join(DataDir, "client2-root-bundle.pem")
])
),
os:cmd(
io_lib:format("cat ~p ~p > ~p", [
filename:join(DataDir, "server1.pem"),
filename:join(DataDir, "intermediate1.pem"),
filename:join(DataDir, "server1-intermediate1-bundle.pem")
])
),
os:cmd(
io_lib:format("cat ~p ~p > ~p", [
filename:join(DataDir, "intermediate1.pem"),
filename:join(DataDir, "server1.pem"),
filename:join(DataDir, "intermediate1-server1-bundle.pem")
])
),
os:cmd(
io_lib:format("cat ~p ~p > ~p", [
filename:join(DataDir, "intermediate1_renewed.pem"),
filename:join(DataDir, "root.pem"),
filename:join(DataDir, "intermediate1_renewed-root-bundle.pem")
])
),
os:cmd(
io_lib:format("cat ~p ~p > ~p", [
filename:join(DataDir, "intermediate2.pem"),
filename:join(DataDir, "intermediate2_renewed.pem"),
filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")
])
),
os:cmd(
io_lib:format("cat ~p ~p > ~p", [
filename:join(DataDir, "intermediate1.pem"),
filename:join(DataDir, "root.pem"),
filename:join(DataDir, "intermediate1-root-bundle.pem")
])
),
os:cmd(
io_lib:format("cat ~p ~p ~p > ~p", [
filename:join(DataDir, "root.pem"),
filename:join(DataDir, "intermediate2.pem"),
filename:join(DataDir, "intermediate1.pem"),
filename:join(DataDir, "all-CAcerts-bundle.pem")
])
),
os:cmd(
io_lib:format("cat ~p ~p > ~p", [
filename:join(DataDir, "intermediate2.pem"),
filename:join(DataDir, "intermediate1.pem"),
filename:join(DataDir, "two-intermediates-bundle.pem")
])
).

View File

@ -559,8 +559,6 @@ ssl_opts(Name, Opts) ->
[
fun ssl_opts_crl_config/2,
fun ssl_opts_drop_unsupported/2,
fun ssl_partial_chain/2,
fun ssl_verify_fun/2,
fun ssl_server_opts/2
],
SSLOpts,
@ -588,12 +586,6 @@ ssl_server_opts(SSLOpts, ssl_options) ->
ssl_server_opts(SSLOpts, dtls_options) ->
emqx_tls_lib:to_server_opts(dtls, SSLOpts).
ssl_partial_chain(SSLOpts, _Options) ->
emqx_tls_lib:opt_partial_chain(SSLOpts).
ssl_verify_fun(SSLOpts, _Options) ->
emqx_tls_lib:opt_verify_fun(SSLOpts).
ranch_opts(Type, ListenOn, Opts) ->
NumAcceptors = maps:get(acceptors, Opts, 4),
MaxConnections = maps:get(max_connections, Opts, 1024),

View File

@ -421,7 +421,6 @@ t_create_webhook_v1_bridges_api(Config) ->
<<"enable">> => true,
<<"hibernate_after">> => <<"5s">>,
<<"log_level">> => <<"notice">>,
<<"partial_chain">> => false,
<<"reuse_sessions">> => true,
<<"secure_renegotiate">> => true,
<<"user_lookup_fun">> =>

View File

@ -1,22 +0,0 @@
Enhance TLS listener to support more flexible TLS verifications.
- partial_chain support
If the option `partial_chain` is set to `true`, allow connections with incomplete certificate chains.
Check the configuration manual document for more details.
- Certificate KeyUsage Validation
Added support for required Extended Key Usage defined in
[rfc5280](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.12).
Introduced a new option (`verify_peer_ext_key_usage`) to require specific key usages (like "serverAuth")
in peer certificates during the TLS handshake.
This strengthens security by ensuring certificates are used for their intended purposes.
example:
"serverAuth,OID:1.3.6.1.5.5.7.3.2"
Check the configuration manual document for more details.

View File

@ -101,8 +101,7 @@ defmodule EMQXUmbrella.MixProject do
{:bcrypt, github: "emqx/erlang-bcrypt", tag: "0.6.2", override: true},
{:uuid, github: "okeuday/uuid", tag: "v2.0.6", override: true},
{:quickrand, github: "okeuday/quickrand", tag: "v2.0.6", override: true},
{:ra, "2.7.3", override: true},
{:mimerl, "1.2.0", override: true}
{:ra, "2.7.3", override: true}
] ++
emqx_apps(profile_info, version) ++
enterprise_deps(profile_info) ++ jq_dep() ++ quicer_dep()

View File

@ -111,8 +111,7 @@
{ssl_verify_fun, "1.1.7"},
{rfc3339, {git, "https://github.com/emqx/rfc3339.git", {tag, "0.2.3"}}},
{bcrypt, {git, "https://github.com/emqx/erlang-bcrypt.git", {tag, "0.6.2"}}},
{ra, "2.7.3"},
{mimerl, "1.2.0"}
{ra, "2.7.3"}
]}.
{xref_ignores,

View File

@ -684,49 +684,6 @@ common_ssl_opts_schema_verify.desc:
common_ssl_opts_schema_verify.label:
"""Verify peer"""
common_ssl_opts_schema_partial_chain.desc:
"""Enable or disable peer verification with partial_chain.
When local verifies a peer certificate during the x509 path validation
process, it constructs a certificate chain that starts with the peer
certificate and ends with a trust anchor.
By default, if it is set to `false`, the trust anchor is the
Root CA, and the certificate chain must be complete.
However, if the setting is set to `true` or `cacert_from_cacertfile`,
the last certificate in `cacertfile` will be used as the trust anchor
certificate (intermediate CA). This creates a partial chain
in the path validation.
Alternatively, if it is configured with `two_cacerts_from_cacertfile`,
one of the last two certificates in `cacertfile` will be used as the
trust anchor certificate, forming a partial chain. This option is
particularly useful for intermediate CA certificate rotation.
However, please note that it incurs some additional overhead, so it
should only be used for certificate rotation purposes."""
common_ssl_opts_schema_partial_chain.label:
"""Partial chain"""
common_ssl_opts_verify_peer_ext_key_usage.desc:
"""Verify extended key usage in peer's certificate
For additional peer certificate validation, the value defined here must present in the
'Extended Key Usage' of peer certificate defined in
[rfc5280](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.12).
Allowed values are
- `clientAuth`
- `serverAuth`
- `codeSigning`
- `emailProtection`
- `timeStamping`
- `ocspSigning`
- raw OID, for example: "OID:1.3.6.1.5.5.7.3.2" means `id-pk 2` which is equivalent to `clientAuth`
Comma-separated string is also supported for validating more than one key usages.
For example, `"serverAuth,OID:1.3.6.1.5.5.7.3.2"`"""
common_ssl_opts_verify_peer_ext_key_usage.label:
"""Verify KeyUsage in cert"""
fields_listeners_ssl.desc:
"""SSL listeners."""

View File

@ -310,4 +310,3 @@ ElasticSearch
doc_as_upsert
upsert
aliyun
OID