From a18018bee0fd2d4c5add8a6fff8cdd135b3bc18d Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 9 May 2023 16:05:49 +0200 Subject: [PATCH 01/13] feat(tls-partial-chain): add partial_chain support for TLS listeners --- priv/emqx.schema | 5 + src/emqx_const_v2.erl | 30 +++ src/emqx_listeners.erl | 3 +- src/emqx_tls_lib.erl | 32 ++- test/emqx_listener_tls_verify_SUITE.erl | 339 ++++++++++++++++++++++++ test/emqx_test_tls_certs_helper.erl | 156 +++++++++++ 6 files changed, 562 insertions(+), 3 deletions(-) create mode 100644 src/emqx_const_v2.erl create mode 100644 test/emqx_listener_tls_verify_SUITE.erl create mode 100644 test/emqx_test_tls_certs_helper.erl diff --git a/priv/emqx.schema b/priv/emqx.schema index a4b9daeef..b3846dc0d 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1646,6 +1646,10 @@ end}. {datatype, atom} ]}. +{mapping, "listener.ssl.$name.partial_chain", "emqx.listeners", [ + {datatype, atom} +]}. + {mapping, "listener.ssl.$name.fail_if_no_peer_cert", "emqx.listeners", [ {datatype, {enum, [true, false]}} ]}. @@ -2377,6 +2381,7 @@ end}. {certfile, cuttlefish:conf_get(Prefix ++ ".certfile", Conf, undefined)}, {cacertfile, cuttlefish:conf_get(Prefix ++ ".cacertfile", Conf, undefined)}, {verify, cuttlefish:conf_get(Prefix ++ ".verify", Conf, undefined)}, + {partial_chain, cuttlefish:conf_get(Prefix ++ ".partial_chain", Conf, undefined)}, {fail_if_no_peer_cert, cuttlefish:conf_get(Prefix ++ ".fail_if_no_peer_cert", Conf, undefined)}, {secure_renegotiate, cuttlefish:conf_get(Prefix ++ ".secure_renegotiate", Conf, undefined)}, {reuse_sessions, cuttlefish:conf_get(Prefix ++ ".reuse_sessions", Conf, undefined)}, diff --git a/src/emqx_const_v2.erl b/src/emqx_const_v2.erl new file mode 100644 index 000000000..536b0215a --- /dev/null +++ b/src/emqx_const_v2.erl @@ -0,0 +1,30 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 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). + +-export([ make_tls_root_fun/2 + ]). + +make_tls_root_fun(cacert_from_cacertfile, CADer) -> + fun(InputChain) -> + case lists:member(CADer, InputChain) of + true -> {trusted_ca, CADer}; + _ -> unknown_ca + end + end. diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 94bb72136..19963e8b6 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -138,7 +138,8 @@ start_listener(tcp, ListenOn, Options) -> start_listener(Proto, ListenOn, Options0) when Proto == ssl; Proto == tls -> ListenerID = proplists:get_value(listener_id, Options0), Options1 = proplists:delete(listener_id, Options0), - Options = emqx_ocsp_cache:inject_sni_fun(ListenerID, Options1), + Options2 = emqx_ocsp_cache:inject_sni_fun(ListenerID, Options1), + Options = emqx_tls_lib:inject_root_fun(Options2), ok = maybe_register_crl_urls(Options), start_mqtt_listener('mqtt:ssl', ListenOn, Options); diff --git a/src/emqx_tls_lib.erl b/src/emqx_tls_lib.erl index c027634b5..e7383b05c 100644 --- a/src/emqx_tls_lib.erl +++ b/src/emqx_tls_lib.erl @@ -22,6 +22,8 @@ , default_ciphers/1 , integral_ciphers/2 , drop_tls13_for_old_otp/1 + , inject_root_fun/1 + , opt_partial_chain/1 ]). %% non-empty string @@ -170,8 +172,34 @@ drop_tls13(SslOpts0) -> Ciphers -> replace(SslOpts1, ciphers, Ciphers -- ?TLSV13_EXCLUSIVE_CIPHERS) end. +inject_root_fun(Options) -> + case proplists:get_value(ssl_options, Options) of + undefined -> + Options; + SslOpts -> + replace(Options, ssl_options, opt_partial_chain(SslOpts)) + end. + +%% @doc enable TLS partial_chain validation if set. +-spec opt_partial_chain(SslOpts :: proplists:proplist()) -> NewSslOpts :: proplists:proplist(). +opt_partial_chain(SslOpts) -> + case proplists:get_value(partial_chain, SslOpts, undefined) of + undefined -> + SslOpts; + cacert_from_cacertfile -> + replace(SslOpts, partial_chain, cacert_from_cacertfile(SslOpts)) + end. + replace(Opts, Key, Value) -> [{Key, Value} | proplists:delete(Key, Opts)]. +%% @doc Helper, make TLS root_fun +cacert_from_cacertfile(SslOpts) -> + Cacertfile = proplists:get_value(cacertfile, SslOpts, undefined), + {ok, PemBin} = file:read_file(Cacertfile), + %% The last one should be the top parent in the chain if it is a chain + {'Certificate', CADer, _} = lists:last(public_key:pem_decode(PemBin)), + emqx_const_v2:make_tls_root_fun(cacert_from_cacertfile, CADer). + -if(?OTP_RELEASE > 22). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). @@ -194,5 +222,5 @@ drop_tls13_no_versions_cipers_test() -> has_tlsv13_cipher(Ciphers) -> lists:any(fun(C) -> lists:member(C, Ciphers) end, ?TLSV13_EXCLUSIVE_CIPHERS). --endif. --endif. +-endif. %% TEST +-endif. %% OTP_RELEASE > 22 diff --git a/test/emqx_listener_tls_verify_SUITE.erl b/test/emqx_listener_tls_verify_SUITE.erl new file mode 100644 index 000000000..49ba18e6c --- /dev/null +++ b/test/emqx_listener_tls_verify_SUITE.erl @@ -0,0 +1,339 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 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_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/emqx_mqtt.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-import(emqx_test_tls_certs_helper, [ gen_ca/2 + , gen_host_cert/3 + ]). + +all() -> emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + generate_tls_certs(Config), + application:ensure_all_started(esockd), + Config. + +end_per_suite(_Config) -> + application:stop(esockd). + +t_tls_conn_success_when_partial_chain_enabled_with_intermediate_ca_cert(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [{ssl_options, [ {verify, verify_peer} + , {fail_if_no_peer_cert, true} + , {partial_chain, cacert_from_cacertfile} + , {cacertfile, filename:join(DataDir, "intermediate1.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + ]}], + emqx_listeners:start_listener(ssl, Port, Options), + {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], 1000), + fail_when_ssl_error(Socket), + ssl:close(Socket). + +t_tls_conn_success_when_partial_chain_enabled_with_intermediate_cacert_bundle(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [{ssl_options, [ {verify, verify_peer} + , {fail_if_no_peer_cert, true} + , {partial_chain, cacert_from_cacertfile} + , {cacertfile, filename:join(DataDir, "server1-intermediate1-bundle.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + ]}], + emqx_listeners:start_listener(ssl, Port, Options), + {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], 1000), + fail_when_ssl_error(Socket), + ssl:close(Socket). + +t_tls_conn_fail_when_partial_chain_enabled_with_intermediate_cacert_bundle2(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [{ssl_options, [ {verify, verify_peer} + , {fail_if_no_peer_cert, true} + , {partial_chain, cacert_from_cacertfile} + , {cacertfile, filename:join(DataDir, "intermediate1-server1-bundle.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + ]}], + emqx_listeners:start_listener(ssl, Port, Options), + {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ssl:close(Socket). + +t_tls_conn_fail_when_partial_chain_disabled_with_intermediate_ca_cert(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [{ssl_options, [ {verify, verify_peer} + , {fail_if_no_peer_cert, true} + , {cacertfile, filename:join(DataDir, "intermediate1.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + ]}], + emqx_listeners:start_listener(ssl, Port, Options), + {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], 1000), + + fail_when_no_ssl_alert(Socket, unknown_ca), + ok = ssl:close(Socket). + + +t_tls_conn_fail_when_partial_chain_disabled_with_other_intermediate_ca_cert(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [{ssl_options, [ {verify, verify_peer} + , {fail_if_no_peer_cert, true} + , {cacertfile, filename:join(DataDir, "intermediate1.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + ]}], + emqx_listeners:start_listener(ssl, Port, Options), + {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2.pem")} + ], 1000), + + fail_when_no_ssl_alert(Socket, unknown_ca), + ok = ssl:close(Socket). + +t_tls_conn_fail_when_partial_chain_enabled_with_other_intermediate_ca_cert(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [{ssl_options, [ {verify, verify_peer} + , {fail_if_no_peer_cert, true} + , {partial_chain, cacert_from_cacertfile} + , {cacertfile, filename:join(DataDir, "intermediate1.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + ]}], + emqx_listeners:start_listener(ssl, Port, Options), + {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2.pem")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ok = ssl:close(Socket). + +t_tls_conn_success_when_partial_chain_enabled_root_ca_with_complete_cert_chain(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [{ssl_options, [ {verify, verify_peer} + , {fail_if_no_peer_cert, true} + , {partial_chain, cacert_from_cacertfile} + , {cacertfile, filename:join(DataDir, "root.pem")} + , {certfile, filename:join(DataDir, "server2.pem")} + , {keyfile, filename:join(DataDir, "server2.key")} + ]}], + emqx_listeners:start_listener(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")} + ], 1000), + fail_when_ssl_error(Socket), + ok = ssl:close(Socket). + +t_tls_conn_success_when_partial_chain_disabled_with_intermediate_ca_cert(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [{ssl_options, [ {verify, verify_peer} + , {fail_if_no_peer_cert, true} + , {cacertfile, filename:join(DataDir, "intermediate1.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + ]}], + emqx_listeners:start_listener(ssl, Port, Options), + {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ok = ssl:close(Socket). + +t_tls_conn_success_when_partial_chain_disabled_with_broken_cert_chain_other_intermediate(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + %% Server has root ca cert + Options = [{ssl_options, [ {verify, verify_peer} + , {fail_if_no_peer_cert, true} + , {cacertfile, filename:join(DataDir, "root.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + ]}], + %% Client has complete chain + emqx_listeners:start_listener(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")} + ], 1000), + fail_when_ssl_error(Socket), + ok = ssl:close(Socket). + +t_tls_conn_fail_when_partial_chain_enabled_with_other_complete_cert_chain(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [{ssl_options, [ {verify, verify_peer} + , {fail_if_no_peer_cert, true} + , {partial_chain, cacert_from_cacertfile} + , {cacertfile, filename:join(DataDir, "intermediate1.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + ]}], + emqx_listeners:start_listener(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")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ok = ssl:close(Socket). + +t_tls_conn_success_when_partial_chain_enabled_with_complete_cert_chain(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [{ssl_options, [ {verify, verify_peer} + , {fail_if_no_peer_cert, true} + , {partial_chain, cacert_from_cacertfile} + , {cacertfile, filename:join(DataDir, "intermediate2.pem")} + , {certfile, filename:join(DataDir, "server2.pem")} + , {keyfile, filename:join(DataDir, "server2.key")} + ]}], + emqx_listeners:start_listener(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")} + ], 1000), + fail_when_ssl_error(Socket), + ok = ssl:close(Socket). + +t_tls_conn_success_when_partial_chain_disabled_with_complete_cert_chain_client(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + DataDir = ?config(data_dir, Config), + Options = [{ssl_options, [ {verify, verify_peer} + , {fail_if_no_peer_cert, true} + , {cacertfile, filename:join(DataDir, "root.pem")} + , {certfile, filename:join(DataDir, "server2.pem")} + , {keyfile, filename:join(DataDir, "server2.key")} + ]}], + emqx_listeners:start_listener(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")} + ], 1000), + fail_when_ssl_error(Socket), + ok = ssl:close(Socket). + + +t_tls_conn_fail_when_partial_chain_disabled_with_incomplete_cert_chain_server(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [{ssl_options, [ {verify, verify_peer} + , {fail_if_no_peer_cert, true} + , {cacertfile, filename:join(DataDir, "intermediate2.pem")} %% imcomplete at server side + , {certfile, filename:join(DataDir, "server2.pem")} + , {keyfile, filename:join(DataDir, "server2.key")} + ]}], + emqx_listeners:start_listener(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")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ok = ssl:close(Socket). + + +t_tls_conn_fail_when_partial_chain_enabled_with_imcomplete_cert_chain(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [{ssl_options, [ {verify, verify_peer} + , {fail_if_no_peer_cert, true} + , {partial_chain, cacert_from_cacertfile} + , {cacertfile, filename:join(DataDir, "intermediate1.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + ]}], + emqx_listeners:start_listener(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")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ok = ssl:close(Socket). + +t_tls_conn_fail_when_partial_chain_disabled_with_incomplete_cert_chain(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [{ssl_options, [ {verify, verify_peer} + , {fail_if_no_peer_cert, true} + , {cacertfile, filename:join(DataDir, "intermediate1.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + ]}], + emqx_listeners:start_listener(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")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ok = ssl:close(Socket). + +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), + 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, "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") + ])). + +fail_when_ssl_error(Socket) -> + receive + {ssl_error, Socket, _} -> + ct:fail("Handshake failed!") + after 1000 -> + ok + end. + +fail_when_no_ssl_alert(Socket, Alert) -> + 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 1000 -> + ct:fail("No expected alert: ~p from Socket: ~p ", [Alert, Socket]) + end. diff --git a/test/emqx_test_tls_certs_helper.erl b/test/emqx_test_tls_certs_helper.erl new file mode 100644 index 000000000..36813074c --- /dev/null +++ b/test/emqx_test_tls_certs_helper.erl @@ -0,0 +1,156 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 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 + ]). + +%%------------------------------------------------------------------------------- +%% 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=Internet Widgits Pty Ltd 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]), + HPEM = filename(Path, "~s.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, + CSR_Cmd = + 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=Internet Widgits Pty Ltd/CN=~s\"", + [PasswordArg, ECKeyFile, HKey, HCSR, CN, CN] + ) + ), + create_file( + HEXT, + "keyUsage=digitalSignature,keyAgreement,keyCertSign\n" + "basicConstraints=CA:TRUE \n" + "subjectAltName=DNS:~s\n", + [CN] + ), + CERT_Cmd = + lists:flatten( + io_lib:format( + "openssl x509 -req " + "-extfile ~s " + "-in ~s -CA ~s -CAkey ~s -CAcreateserial " + "-out ~s -days 500", + [ + HEXT, + HCSR, + ca_cert_name(Path, CaName), + ca_key_name(Path, CaName), + HPEM + ] + ) + ), + ct:pal(os:cmd(CSR_Cmd)), + ct:pal(os:cmd(CERT_Cmd)), + file:delete(HEXT). + +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. From ea35b20035756380578f0eba8ff787ae2d1459e0 Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 28 Apr 2023 17:10:35 +0200 Subject: [PATCH 02/13] test(tls): test refactoring --- test/emqx_listener_tls_verify_SUITE.erl | 339 ------------------ test/emqx_listener_tls_verify_chain_SUITE.erl | 157 ++++++++ ...istener_tls_verify_partial_chain_SUITE.erl | 212 +++++++++++ test/emqx_test_tls_certs_helper.erl | 71 ++++ 4 files changed, 440 insertions(+), 339 deletions(-) delete mode 100644 test/emqx_listener_tls_verify_SUITE.erl create mode 100644 test/emqx_listener_tls_verify_chain_SUITE.erl create mode 100644 test/emqx_listener_tls_verify_partial_chain_SUITE.erl diff --git a/test/emqx_listener_tls_verify_SUITE.erl b/test/emqx_listener_tls_verify_SUITE.erl deleted file mode 100644 index 49ba18e6c..000000000 --- a/test/emqx_listener_tls_verify_SUITE.erl +++ /dev/null @@ -1,339 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2023 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_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/emqx_mqtt.hrl"). --include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). - --import(emqx_test_tls_certs_helper, [ gen_ca/2 - , gen_host_cert/3 - ]). - -all() -> emqx_ct:all(?MODULE). - -init_per_suite(Config) -> - generate_tls_certs(Config), - application:ensure_all_started(esockd), - Config. - -end_per_suite(_Config) -> - application:stop(esockd). - -t_tls_conn_success_when_partial_chain_enabled_with_intermediate_ca_cert(Config) -> - Port = emqx_test_tls_certs_helper:select_free_port(ssl), - DataDir = ?config(data_dir, Config), - Options = [{ssl_options, [ {verify, verify_peer} - , {fail_if_no_peer_cert, true} - , {partial_chain, cacert_from_cacertfile} - , {cacertfile, filename:join(DataDir, "intermediate1.pem")} - , {certfile, filename:join(DataDir, "server1.pem")} - , {keyfile, filename:join(DataDir, "server1.key")} - ]}], - emqx_listeners:start_listener(ssl, Port, Options), - {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")}, - {certfile, filename:join(DataDir, "client1.pem")} - ], 1000), - fail_when_ssl_error(Socket), - ssl:close(Socket). - -t_tls_conn_success_when_partial_chain_enabled_with_intermediate_cacert_bundle(Config) -> - Port = emqx_test_tls_certs_helper:select_free_port(ssl), - DataDir = ?config(data_dir, Config), - Options = [{ssl_options, [ {verify, verify_peer} - , {fail_if_no_peer_cert, true} - , {partial_chain, cacert_from_cacertfile} - , {cacertfile, filename:join(DataDir, "server1-intermediate1-bundle.pem")} - , {certfile, filename:join(DataDir, "server1.pem")} - , {keyfile, filename:join(DataDir, "server1.key")} - ]}], - emqx_listeners:start_listener(ssl, Port, Options), - {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")}, - {certfile, filename:join(DataDir, "client1.pem")} - ], 1000), - fail_when_ssl_error(Socket), - ssl:close(Socket). - -t_tls_conn_fail_when_partial_chain_enabled_with_intermediate_cacert_bundle2(Config) -> - Port = emqx_test_tls_certs_helper:select_free_port(ssl), - DataDir = ?config(data_dir, Config), - Options = [{ssl_options, [ {verify, verify_peer} - , {fail_if_no_peer_cert, true} - , {partial_chain, cacert_from_cacertfile} - , {cacertfile, filename:join(DataDir, "intermediate1-server1-bundle.pem")} - , {certfile, filename:join(DataDir, "server1.pem")} - , {keyfile, filename:join(DataDir, "server1.key")} - ]}], - emqx_listeners:start_listener(ssl, Port, Options), - {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")}, - {certfile, filename:join(DataDir, "client1.pem")} - ], 1000), - fail_when_no_ssl_alert(Socket, unknown_ca), - ssl:close(Socket). - -t_tls_conn_fail_when_partial_chain_disabled_with_intermediate_ca_cert(Config) -> - Port = emqx_test_tls_certs_helper:select_free_port(ssl), - DataDir = ?config(data_dir, Config), - Options = [{ssl_options, [ {verify, verify_peer} - , {fail_if_no_peer_cert, true} - , {cacertfile, filename:join(DataDir, "intermediate1.pem")} - , {certfile, filename:join(DataDir, "server1.pem")} - , {keyfile, filename:join(DataDir, "server1.key")} - ]}], - emqx_listeners:start_listener(ssl, Port, Options), - {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")}, - {certfile, filename:join(DataDir, "client1.pem")} - ], 1000), - - fail_when_no_ssl_alert(Socket, unknown_ca), - ok = ssl:close(Socket). - - -t_tls_conn_fail_when_partial_chain_disabled_with_other_intermediate_ca_cert(Config) -> - Port = emqx_test_tls_certs_helper:select_free_port(ssl), - DataDir = ?config(data_dir, Config), - Options = [{ssl_options, [ {verify, verify_peer} - , {fail_if_no_peer_cert, true} - , {cacertfile, filename:join(DataDir, "intermediate1.pem")} - , {certfile, filename:join(DataDir, "server1.pem")} - , {keyfile, filename:join(DataDir, "server1.key")} - ]}], - emqx_listeners:start_listener(ssl, Port, Options), - {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")}, - {certfile, filename:join(DataDir, "client2.pem")} - ], 1000), - - fail_when_no_ssl_alert(Socket, unknown_ca), - ok = ssl:close(Socket). - -t_tls_conn_fail_when_partial_chain_enabled_with_other_intermediate_ca_cert(Config) -> - Port = emqx_test_tls_certs_helper:select_free_port(ssl), - DataDir = ?config(data_dir, Config), - Options = [{ssl_options, [ {verify, verify_peer} - , {fail_if_no_peer_cert, true} - , {partial_chain, cacert_from_cacertfile} - , {cacertfile, filename:join(DataDir, "intermediate1.pem")} - , {certfile, filename:join(DataDir, "server1.pem")} - , {keyfile, filename:join(DataDir, "server1.key")} - ]}], - emqx_listeners:start_listener(ssl, Port, Options), - {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")}, - {certfile, filename:join(DataDir, "client2.pem")} - ], 1000), - fail_when_no_ssl_alert(Socket, unknown_ca), - ok = ssl:close(Socket). - -t_tls_conn_success_when_partial_chain_enabled_root_ca_with_complete_cert_chain(Config) -> - Port = emqx_test_tls_certs_helper:select_free_port(ssl), - DataDir = ?config(data_dir, Config), - Options = [{ssl_options, [ {verify, verify_peer} - , {fail_if_no_peer_cert, true} - , {partial_chain, cacert_from_cacertfile} - , {cacertfile, filename:join(DataDir, "root.pem")} - , {certfile, filename:join(DataDir, "server2.pem")} - , {keyfile, filename:join(DataDir, "server2.key")} - ]}], - emqx_listeners:start_listener(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")} - ], 1000), - fail_when_ssl_error(Socket), - ok = ssl:close(Socket). - -t_tls_conn_success_when_partial_chain_disabled_with_intermediate_ca_cert(Config) -> - Port = emqx_test_tls_certs_helper:select_free_port(ssl), - DataDir = ?config(data_dir, Config), - Options = [{ssl_options, [ {verify, verify_peer} - , {fail_if_no_peer_cert, true} - , {cacertfile, filename:join(DataDir, "intermediate1.pem")} - , {certfile, filename:join(DataDir, "server1.pem")} - , {keyfile, filename:join(DataDir, "server1.key")} - ]}], - emqx_listeners:start_listener(ssl, Port, Options), - {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")}, - {certfile, filename:join(DataDir, "client1.pem")} - ], 1000), - fail_when_no_ssl_alert(Socket, unknown_ca), - ok = ssl:close(Socket). - -t_tls_conn_success_when_partial_chain_disabled_with_broken_cert_chain_other_intermediate(Config) -> - Port = emqx_test_tls_certs_helper:select_free_port(ssl), - DataDir = ?config(data_dir, Config), - %% Server has root ca cert - Options = [{ssl_options, [ {verify, verify_peer} - , {fail_if_no_peer_cert, true} - , {cacertfile, filename:join(DataDir, "root.pem")} - , {certfile, filename:join(DataDir, "server1.pem")} - , {keyfile, filename:join(DataDir, "server1.key")} - ]}], - %% Client has complete chain - emqx_listeners:start_listener(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")} - ], 1000), - fail_when_ssl_error(Socket), - ok = ssl:close(Socket). - -t_tls_conn_fail_when_partial_chain_enabled_with_other_complete_cert_chain(Config) -> - Port = emqx_test_tls_certs_helper:select_free_port(ssl), - DataDir = ?config(data_dir, Config), - Options = [{ssl_options, [ {verify, verify_peer} - , {fail_if_no_peer_cert, true} - , {partial_chain, cacert_from_cacertfile} - , {cacertfile, filename:join(DataDir, "intermediate1.pem")} - , {certfile, filename:join(DataDir, "server1.pem")} - , {keyfile, filename:join(DataDir, "server1.key")} - ]}], - emqx_listeners:start_listener(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")} - ], 1000), - fail_when_no_ssl_alert(Socket, unknown_ca), - ok = ssl:close(Socket). - -t_tls_conn_success_when_partial_chain_enabled_with_complete_cert_chain(Config) -> - Port = emqx_test_tls_certs_helper:select_free_port(ssl), - DataDir = ?config(data_dir, Config), - Options = [{ssl_options, [ {verify, verify_peer} - , {fail_if_no_peer_cert, true} - , {partial_chain, cacert_from_cacertfile} - , {cacertfile, filename:join(DataDir, "intermediate2.pem")} - , {certfile, filename:join(DataDir, "server2.pem")} - , {keyfile, filename:join(DataDir, "server2.key")} - ]}], - emqx_listeners:start_listener(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")} - ], 1000), - fail_when_ssl_error(Socket), - ok = ssl:close(Socket). - -t_tls_conn_success_when_partial_chain_disabled_with_complete_cert_chain_client(Config) -> - Port = emqx_test_tls_certs_helper:select_free_port(ssl), - DataDir = ?config(data_dir, Config), - DataDir = ?config(data_dir, Config), - Options = [{ssl_options, [ {verify, verify_peer} - , {fail_if_no_peer_cert, true} - , {cacertfile, filename:join(DataDir, "root.pem")} - , {certfile, filename:join(DataDir, "server2.pem")} - , {keyfile, filename:join(DataDir, "server2.key")} - ]}], - emqx_listeners:start_listener(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")} - ], 1000), - fail_when_ssl_error(Socket), - ok = ssl:close(Socket). - - -t_tls_conn_fail_when_partial_chain_disabled_with_incomplete_cert_chain_server(Config) -> - Port = emqx_test_tls_certs_helper:select_free_port(ssl), - DataDir = ?config(data_dir, Config), - Options = [{ssl_options, [ {verify, verify_peer} - , {fail_if_no_peer_cert, true} - , {cacertfile, filename:join(DataDir, "intermediate2.pem")} %% imcomplete at server side - , {certfile, filename:join(DataDir, "server2.pem")} - , {keyfile, filename:join(DataDir, "server2.key")} - ]}], - emqx_listeners:start_listener(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")} - ], 1000), - fail_when_no_ssl_alert(Socket, unknown_ca), - ok = ssl:close(Socket). - - -t_tls_conn_fail_when_partial_chain_enabled_with_imcomplete_cert_chain(Config) -> - Port = emqx_test_tls_certs_helper:select_free_port(ssl), - DataDir = ?config(data_dir, Config), - Options = [{ssl_options, [ {verify, verify_peer} - , {fail_if_no_peer_cert, true} - , {partial_chain, cacert_from_cacertfile} - , {cacertfile, filename:join(DataDir, "intermediate1.pem")} - , {certfile, filename:join(DataDir, "server1.pem")} - , {keyfile, filename:join(DataDir, "server1.key")} - ]}], - emqx_listeners:start_listener(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")} - ], 1000), - fail_when_no_ssl_alert(Socket, unknown_ca), - ok = ssl:close(Socket). - -t_tls_conn_fail_when_partial_chain_disabled_with_incomplete_cert_chain(Config) -> - Port = emqx_test_tls_certs_helper:select_free_port(ssl), - DataDir = ?config(data_dir, Config), - Options = [{ssl_options, [ {verify, verify_peer} - , {fail_if_no_peer_cert, true} - , {cacertfile, filename:join(DataDir, "intermediate1.pem")} - , {certfile, filename:join(DataDir, "server1.pem")} - , {keyfile, filename:join(DataDir, "server1.key")} - ]}], - emqx_listeners:start_listener(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")} - ], 1000), - fail_when_no_ssl_alert(Socket, unknown_ca), - ok = ssl:close(Socket). - -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), - 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, "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") - ])). - -fail_when_ssl_error(Socket) -> - receive - {ssl_error, Socket, _} -> - ct:fail("Handshake failed!") - after 1000 -> - ok - end. - -fail_when_no_ssl_alert(Socket, Alert) -> - 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 1000 -> - ct:fail("No expected alert: ~p from Socket: ~p ", [Alert, Socket]) - end. diff --git a/test/emqx_listener_tls_verify_chain_SUITE.erl b/test/emqx_listener_tls_verify_chain_SUITE.erl new file mode 100644 index 000000000..65f6a55b5 --- /dev/null +++ b/test/emqx_listener_tls_verify_chain_SUITE.erl @@ -0,0 +1,157 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 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("emqx/include/emqx.hrl"). +-include_lib("emqx/include/emqx_mqtt.hrl"). +-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 + ]). + + +all() -> emqx_ct: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_listeners:start_listener(ssl, Port, Options), + {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], 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_listeners:start_listener(ssl, Port, Options), + {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2.pem")} + ], 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_listeners:start_listener(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")} + ], 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 complete chain + emqx_listeners:start_listener(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")} + ], 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_listeners:start_listener(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")} + ], 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), + Options = [{ssl_options, [{cacertfile, filename:join(DataDir, "intermediate2.pem")} %% imcomplete at server side + , {certfile, filename:join(DataDir, "server2.pem")} + , {keyfile, filename:join(DataDir, "server2.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(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")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ok = ssl:close(Socket). + +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_listeners:start_listener(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")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ok = ssl:close(Socket). + +ssl_config_verify_peer() -> + [ {verify, verify_peer} + , {fail_if_no_peer_cert, true} + ]. diff --git a/test/emqx_listener_tls_verify_partial_chain_SUITE.erl b/test/emqx_listener_tls_verify_partial_chain_SUITE.erl new file mode 100644 index 000000000..e97f11cf8 --- /dev/null +++ b/test/emqx_listener_tls_verify_partial_chain_SUITE.erl @@ -0,0 +1,212 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 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("emqx/include/emqx.hrl"). +-include_lib("emqx/include/emqx_mqtt.hrl"). +-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 + ]). + + +all() -> emqx_ct: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, [ {cacertfile, filename:join(DataDir, "intermediate1.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(ssl, Port, Options), + {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], 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, [ {cacertfile, filename:join(DataDir, "server1-intermediate1-bundle.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(ssl, Port, Options), + {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], 1000), + fail_when_ssl_error(Socket), + ssl:close(Socket). + +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, [ {cacertfile, filename:join(DataDir, "intermediate1-server1-bundle.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(ssl, Port, Options), + {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ssl:close(Socket). + +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, [ {cacertfile, filename:join(DataDir, "intermediate1.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(ssl, Port, Options), + {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2.pem")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ok = ssl:close(Socket). + +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, [ {cacertfile, filename:join(DataDir, "root.pem")} + , {certfile, filename:join(DataDir, "server2.pem")} + , {keyfile, filename:join(DataDir, "server2.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(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")} + ], 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, [ {cacertfile, filename:join(DataDir, "intermediate1.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(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")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ok = ssl:close(Socket). + +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, [ {cacertfile, filename:join(DataDir, "intermediate1-root-bundle.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(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")} + ], 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, [ {cacertfile, filename:join(DataDir, "intermediate2.pem")} + , {certfile, filename:join(DataDir, "server2.pem")} + , {keyfile, filename:join(DataDir, "server2.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(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")} + ], 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, [ {cacertfile, filename:join(DataDir, "intermediate1.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(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")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ok = ssl:close(Socket). + +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, [ {cacertfile, filename:join(DataDir, "intermediate1.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(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")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ok = ssl:close(Socket). + +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, [ {cacertfile, filename:join(DataDir, "intermediate2.pem")} + , {certfile, filename:join(DataDir, "server2.pem")} + , {keyfile, filename:join(DataDir, "server2.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(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")} + ], 1000), + fail_when_ssl_error(Socket), + ok = ssl:close(Socket). + +ssl_config_verify_partial_chain() -> + [ {verify, verify_peer} + , {fail_if_no_peer_cert, true} + , {partial_chain, cacert_from_cacertfile} + ]. diff --git a/test/emqx_test_tls_certs_helper.erl b/test/emqx_test_tls_certs_helper.erl index 36813074c..82b343fdb 100644 --- a/test/emqx_test_tls_certs_helper.erl +++ b/test/emqx_test_tls_certs_helper.erl @@ -20,8 +20,18 @@ , 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 + + ]). +-include_lib("common_test/include/ct.hrl"). + %%------------------------------------------------------------------------------- %% TLS certs %%------------------------------------------------------------------------------- @@ -154,3 +164,64 @@ select_free_port(GenModule, Fun) when 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 recvd +fail_when_no_ssl_alert(Socket, Alert) -> + fail_when_no_ssl_alert(Socket, Alert, 1000). +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), + 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.pem"), + filename:join(DataDir, "root.pem"), + filename:join(DataDir, "intermediate1-root-bundle.pem") + ])). From 11c8e937b4c9bea2e6719d42e7939e6fde3d7f28 Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 5 May 2023 10:16:20 +0200 Subject: [PATCH 03/13] docs: changelog for TLS listener partial certificate chain validation --- changes/v4.4.18-en.md | 5 +++++ changes/v4.4.18-zh.md | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/changes/v4.4.18-en.md b/changes/v4.4.18-en.md index bc5af67b3..edfedd621 100644 --- a/changes/v4.4.18-en.md +++ b/changes/v4.4.18-en.md @@ -7,4 +7,9 @@ The parameters of certain actions support using placeholder syntax to dynamically fill in the content of strings. The format of the placeholder syntax is `${key}`. Prior to the improvement, the `key` in `${key}` could only contain letters, numbers, and underscores. Now the `key` supports any UTF8 character after the improvement. +- Adds a new feature to enable partial certificate chain validation for TLS listeners[#10553](https://github.com/emqx/emqx/pull/10553). + If TLS listener has `partial_chain` set to `cacert_from_cacertfile`, + the certificate in the `cacertfile` will be used as the `cacert` for chain path validation. If the `cacertfile` has a chain of certificates, the cert at the end of the file will be used as the `cacert` for path validation. + ## Bug fixes + diff --git a/changes/v4.4.18-zh.md b/changes/v4.4.18-zh.md index 5a9e7c42a..f78bd5246 100644 --- a/changes/v4.4.18-zh.md +++ b/changes/v4.4.18-zh.md @@ -7,5 +7,9 @@ 某些动作的参数支持使用占位符语法,来动态的填充字符串的内容,占位符语法的格式为 `${key}`。 改进前,`${key}` 中的 `key` 只能包含字母、数字和下划线。改进后 `key` 支持任意的 UTF8 字符了。 +- 增加了一个新的功能,为TLS监听器启用部分证书链验证[#10553](https://github.com/emqx/emqx/pull/10553)。 + 如果TLS监听器的 `partial_chain` 设置为 `cacert_from_cacertfile`, + `cacertfile` 中的证书将被用作链式路径验证的 `cacert` 。如果 `cacertfile` 文件有一连串的证书,文件末尾的证书将被用作路径验证的 `cacert`。 + ## 修复 From 8503d3c6dd408b50ea4a91097a5881fe5ef68014 Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 3 May 2023 14:34:03 +0200 Subject: [PATCH 04/13] test(tls-partial-chains): renewed intermediate_cacert --- changes/v4.4.18-en.md | 3 +- changes/v4.4.18-zh.md | 4 +- src/emqx_tls_lib.erl | 4 +- ...istener_tls_verify_partial_chain_SUITE.erl | 47 ++++++++++++- test/emqx_test_tls_certs_helper.erl | 70 +++++++++++-------- 5 files changed, 94 insertions(+), 34 deletions(-) diff --git a/changes/v4.4.18-en.md b/changes/v4.4.18-en.md index edfedd621..1b26c5522 100644 --- a/changes/v4.4.18-en.md +++ b/changes/v4.4.18-en.md @@ -8,8 +8,7 @@ Prior to the improvement, the `key` in `${key}` could only contain letters, numbers, and underscores. Now the `key` supports any UTF8 character after the improvement. - Adds a new feature to enable partial certificate chain validation for TLS listeners[#10553](https://github.com/emqx/emqx/pull/10553). - If TLS listener has `partial_chain` set to `cacert_from_cacertfile`, - the certificate in the `cacertfile` will be used as the `cacert` for chain path validation. If the `cacertfile` has a chain of certificates, the cert at the end of the file will be used as the `cacert` for path validation. + If partial_chain is set to 'true', the last certificate in cacertfile is treated as the terminal of the certificate trust-chain. That is, the TLS handshake does not require full trust-chain, and EMQX will not try to validate the chain all the way up to the root CA. ## Bug fixes diff --git a/changes/v4.4.18-zh.md b/changes/v4.4.18-zh.md index f78bd5246..714572012 100644 --- a/changes/v4.4.18-zh.md +++ b/changes/v4.4.18-zh.md @@ -8,8 +8,8 @@ 改进前,`${key}` 中的 `key` 只能包含字母、数字和下划线。改进后 `key` 支持任意的 UTF8 字符了。 - 增加了一个新的功能,为TLS监听器启用部分证书链验证[#10553](https://github.com/emqx/emqx/pull/10553)。 - 如果TLS监听器的 `partial_chain` 设置为 `cacert_from_cacertfile`, - `cacertfile` 中的证书将被用作链式路径验证的 `cacert` 。如果 `cacertfile` 文件有一连串的证书,文件末尾的证书将被用作路径验证的 `cacert`。 + 如果 partial_chain 设置为“true”,cacertfile 中的最后一个证书将被视为证书信任链的顶端证书。 也就是说,TLS 握手不需要完整的链,并且 EMQX 不会尝试一直验证链直到根 CA。 + ## 修复 diff --git a/src/emqx_tls_lib.erl b/src/emqx_tls_lib.erl index e7383b05c..eaa46d3f5 100644 --- a/src/emqx_tls_lib.erl +++ b/src/emqx_tls_lib.erl @@ -186,7 +186,9 @@ opt_partial_chain(SslOpts) -> case proplists:get_value(partial_chain, SslOpts, undefined) of undefined -> SslOpts; - cacert_from_cacertfile -> + false -> + SslOpts; + V when V =:= cacert_from_cacertfile orelse V == true -> replace(SslOpts, partial_chain, cacert_from_cacertfile(SslOpts)) end. diff --git a/test/emqx_listener_tls_verify_partial_chain_SUITE.erl b/test/emqx_listener_tls_verify_partial_chain_SUITE.erl index e97f11cf8..615709a44 100644 --- a/test/emqx_listener_tls_verify_partial_chain_SUITE.erl +++ b/test/emqx_listener_tls_verify_partial_chain_SUITE.erl @@ -70,6 +70,51 @@ t_conn_success_with_intermediate_cacert_bundle(Config) -> 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, [ {cacertfile, filename:join(DataDir, "intermediate1_renewed.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(ssl, Port, Options), + {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], 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, [ {cacertfile, filename:join(DataDir, "intermediate2_renewed.pem")} + , {certfile, filename:join(DataDir, "server2.pem")} + , {keyfile, filename:join(DataDir, "server2.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(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")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ssl:close(Socket). + +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, [ {cacertfile, filename:join(DataDir, "intermediate1_renewed.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(ssl, Port, Options), + {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2.pem")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ssl:close(Socket). + 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), @@ -208,5 +253,5 @@ t_conn_success_with_server_intermediate_and_client_root_chain(Config) -> ssl_config_verify_partial_chain() -> [ {verify, verify_peer} , {fail_if_no_peer_cert, true} - , {partial_chain, cacert_from_cacertfile} + , {partial_chain, true} ]. diff --git a/test/emqx_test_tls_certs_helper.erl b/test/emqx_test_tls_certs_helper.erl index 82b343fdb..e51875272 100644 --- a/test/emqx_test_tls_certs_helper.erl +++ b/test/emqx_test_tls_certs_helper.erl @@ -73,7 +73,9 @@ gen_host_cert(H, CaName, Path, Opts) -> 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 @@ -82,44 +84,56 @@ gen_host_cert(H, CaName, Path, Opts) -> Password -> io_lib:format(" -passout pass:'~s' ", [Password]) end, - CSR_Cmd = - 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=Internet Widgits Pty Ltd/CN=~s\"", - [PasswordArg, ECKeyFile, HKey, HCSR, CN, CN] - ) - ), + create_file( - HEXT, - "keyUsage=digitalSignature,keyAgreement,keyCertSign\n" - "basicConstraints=CA:TRUE \n" - "subjectAltName=DNS:~s\n", - [CN] + HEXT, + "keyUsage=digitalSignature,keyAgreement,keyCertSign\n" + "basicConstraints=CA:TRUE \n" + "subjectAltName=DNS:~s\n", + [CN] ), - CERT_Cmd = - lists:flatten( + + 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, HCSR, 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", [ - HEXT, - HCSR, - ca_cert_name(Path, CaName), - ca_key_name(Path, CaName), - HPEM + ExtFile, + CSRFile, + CACert, + CAKey, + OutputCert ] ) - ), - ct:pal(os:cmd(CSR_Cmd)), - ct:pal(os:cmd(CERT_Cmd)), - file:delete(HEXT). + ). + +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=Internet Widgits Pty Ltd/CN=~s\"", + [PasswordArg, ECKeyFile, HKey, HCSR, CN, CN] + ) + ). filename(Path, F, A) -> filename:join(Path, str(io_lib:format(F, A))). From c3430b8883ec9f1385079fe8f847b19f7d3af69c Mon Sep 17 00:00:00 2001 From: William Yang Date: Thu, 4 May 2023 17:54:59 +0200 Subject: [PATCH 05/13] feat(tls-partial-chain): just return trusted CA. --- src/emqx_const_v2.erl | 7 ++--- test/emqx_listener_tls_verify_chain_SUITE.erl | 18 ++++++++++- ...istener_tls_verify_partial_chain_SUITE.erl | 31 +++++++++++++++++++ test/emqx_test_tls_certs_helper.erl | 8 +++++ 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/emqx_const_v2.erl b/src/emqx_const_v2.erl index 536b0215a..0c211ec5f 100644 --- a/src/emqx_const_v2.erl +++ b/src/emqx_const_v2.erl @@ -22,9 +22,6 @@ ]). make_tls_root_fun(cacert_from_cacertfile, CADer) -> - fun(InputChain) -> - case lists:member(CADer, InputChain) of - true -> {trusted_ca, CADer}; - _ -> unknown_ca - end + fun(_InputChain) -> + {trusted_ca, CADer} end. diff --git a/test/emqx_listener_tls_verify_chain_SUITE.erl b/test/emqx_listener_tls_verify_chain_SUITE.erl index 65f6a55b5..b5eca0e62 100644 --- a/test/emqx_listener_tls_verify_chain_SUITE.erl +++ b/test/emqx_listener_tls_verify_chain_SUITE.erl @@ -98,7 +98,7 @@ t_conn_success_with_other_signed_client_composed_complete_chain(Config) -> , {keyfile, filename:join(DataDir, "server1.key")} | ?config(ssl_config, Config) ]}], - %% Client has complete chain + %% Client has partial_chain emqx_listeners:start_listener(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")} @@ -106,6 +106,22 @@ t_conn_success_with_other_signed_client_composed_complete_chain(Config) -> 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_listeners:start_listener(ssl, Port, Options), + {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], 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), diff --git a/test/emqx_listener_tls_verify_partial_chain_SUITE.erl b/test/emqx_listener_tls_verify_partial_chain_SUITE.erl index 615709a44..c38039cf1 100644 --- a/test/emqx_listener_tls_verify_partial_chain_SUITE.erl +++ b/test/emqx_listener_tls_verify_partial_chain_SUITE.erl @@ -100,6 +100,37 @@ t_conn_fail_with_renewed_intermediate_cacert_and_client_using_old_complete_bundl fail_when_no_ssl_alert(Socket, unknown_ca), ssl:close(Socket). +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, [ {cacertfile, filename:join(DataDir, "intermediate2_renewed.pem")} + , {certfile, filename:join(DataDir, "server2.pem")} + , {keyfile, filename:join(DataDir, "server2.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(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")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ssl:close(Socket). + +%%@TODO limitation: EMQX is not able to check if the trusted CAcert and the old CAcert belongs to same 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, [ {cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")} + , {certfile, filename:join(DataDir, "server2.pem")} + , {keyfile, filename:join(DataDir, "server2.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(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")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ssl:close(Socket). + 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), diff --git a/test/emqx_test_tls_certs_helper.erl b/test/emqx_test_tls_certs_helper.erl index e51875272..d95a1a4f9 100644 --- a/test/emqx_test_tls_certs_helper.erl +++ b/test/emqx_test_tls_certs_helper.erl @@ -235,6 +235,14 @@ generate_tls_certs(Config) -> 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") From 30cccab38705920966d4d374c4756ca64e376e72 Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 5 May 2023 09:23:56 +0200 Subject: [PATCH 06/13] chore: review comments --- test/emqx_test_tls_certs_helper.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/emqx_test_tls_certs_helper.erl b/test/emqx_test_tls_certs_helper.erl index d95a1a4f9..6b9921884 100644 --- a/test/emqx_test_tls_certs_helper.erl +++ b/test/emqx_test_tls_certs_helper.erl @@ -47,7 +47,7 @@ gen_ca(Path, Name) -> "-newkey ec:~s " "-keyout ~s -out ~s -days 3650 " "-addext basicConstraints=CA:TRUE " - "-subj \"/C=SE/O=Internet Widgits Pty Ltd CA\"", + "-subj \"/C=SE/O=TEST CA\"", [ ECKeyFile, ca_key_name(Path, Name), @@ -130,7 +130,7 @@ csr_cmd(PasswordArg, ECKeyFile, HKey, HCSR, CN) -> "-addext \"subjectAltName=DNS:~s\" " "-addext basicConstraints=CA:TRUE " "-addext keyUsage=digitalSignature,keyAgreement,keyCertSign " - "-subj \"/C=SE/O=Internet Widgits Pty Ltd/CN=~s\"", + "-subj \"/C=SE/O=TEST/CN=~s\"", [PasswordArg, ECKeyFile, HKey, HCSR, CN, CN] ) ). From 6d0a76805acdeb3f77a6b0f747c53fb8162dec66 Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 5 May 2023 10:04:20 +0200 Subject: [PATCH 07/13] feat(tls-partial-chains): error handling for invalid cacertfile --- src/emqx_tls_lib.erl | 15 +++++++++++++-- ...qx_listener_tls_verify_partial_chain_SUITE.erl | 11 +++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/emqx_tls_lib.erl b/src/emqx_tls_lib.erl index eaa46d3f5..11299787c 100644 --- a/src/emqx_tls_lib.erl +++ b/src/emqx_tls_lib.erl @@ -26,6 +26,8 @@ , opt_partial_chain/1 ]). +-include("logger.hrl"). + %% non-empty string -define(IS_STRING(L), (is_list(L) andalso L =/= [] andalso is_integer(hd(L)))). %% non-empty list of strings @@ -189,14 +191,23 @@ opt_partial_chain(SslOpts) -> false -> SslOpts; V when V =:= cacert_from_cacertfile orelse V == true -> - replace(SslOpts, partial_chain, cacert_from_cacertfile(SslOpts)) + replace(SslOpts, partial_chain, rootfun_trusted_ca_from_cacertfile(SslOpts)) end. replace(Opts, Key, Value) -> [{Key, Value} | proplists:delete(Key, Opts)]. %% @doc Helper, make TLS root_fun -cacert_from_cacertfile(SslOpts) -> +rootfun_trusted_ca_from_cacertfile(SslOpts) -> Cacertfile = proplists:get_value(cacertfile, SslOpts, undefined), + try do_rootfun_trusted_ca_from_cacertfile(Cacertfile) + catch _Error:_ -> + %% 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.. + ?LOG(error, "Failed to look for trusted cacert from cacertfile. loc: ~p:~p", + [?MODULE, ?FUNCTION_NAME]), + throw({error, ?FUNCTION_NAME}) + end. +do_rootfun_trusted_ca_from_cacertfile(Cacertfile) -> {ok, PemBin} = file:read_file(Cacertfile), %% The last one should be the top parent in the chain if it is a chain {'Certificate', CADer, _} = lists:last(public_key:pem_decode(PemBin)), diff --git a/test/emqx_listener_tls_verify_partial_chain_SUITE.erl b/test/emqx_listener_tls_verify_partial_chain_SUITE.erl index c38039cf1..4b90b3a31 100644 --- a/test/emqx_listener_tls_verify_partial_chain_SUITE.erl +++ b/test/emqx_listener_tls_verify_partial_chain_SUITE.erl @@ -281,6 +281,17 @@ t_conn_success_with_server_intermediate_and_client_root_chain(Config) -> fail_when_ssl_error(Socket), ok = ssl:close(Socket). +t_error_handling_invalid_cacertfile(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [{ssl_options, [ {cacertfile, filename:join(DataDir, "server2.key")} %% trigger error + , {certfile, filename:join(DataDir, "server2.pem")} + , {keyfile, filename:join(DataDir, "server2.key")} + | ?config(ssl_config, Config) + ]}], + ?assertException(throw, {error, rootfun_trusted_ca_from_cacertfile}, emqx_listeners:start_listener(ssl, Port, Options)). + + ssl_config_verify_partial_chain() -> [ {verify, verify_peer} , {fail_if_no_peer_cert, true} From 1520bd7b3ff694d624a76e38592d6904e2619112 Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 9 May 2023 13:07:16 +0200 Subject: [PATCH 08/13] feat(tls-partial-chains): update appup src --- src/emqx.appup.src | 144 +++++++++++++++++++++++++++++++++------------ 1 file changed, 106 insertions(+), 38 deletions(-) diff --git a/src/emqx.appup.src b/src/emqx.appup.src index 442bf0ef5..1bbff0a77 100644 --- a/src/emqx.appup.src +++ b/src/emqx.appup.src @@ -4,19 +4,28 @@ [{"4.4.18", [{load_module,emqx_relup,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]}]}, - {"4.4.17", - [{load_module,emqx_relup,brutal_purge,soft_purge,[]}, + [{"4.4.17", + [{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, + {load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {add_module,emqx_const_v2}, + {load_module,emqx_relup,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]}, {load_module,emqx_plugins,brutal_purge,soft_purge,[]}]}, {"4.4.16", - [{load_module,emqx_channel,brutal_purge,soft_purge,[]}, + [{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, + {load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {add_module,emqx_const_v2}, + {load_module,emqx_channel,brutal_purge,soft_purge,[]}, {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_misc,brutal_purge,soft_purge,[]}, {load_module,emqx_relup,brutal_purge,soft_purge,[]}, {load_module,emqx_plugins,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]}]}, {"4.4.15", - [{load_module,emqx_channel,brutal_purge,soft_purge,[]}, + [{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, + {load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {add_module,emqx_const_v2}, + {load_module,emqx_channel,brutal_purge,soft_purge,[]}, {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx,brutal_purge,soft_purge,[]}, {load_module,emqx_misc,brutal_purge,soft_purge,[]}, @@ -25,7 +34,10 @@ {load_module,emqx_relup,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]}]}, {"4.4.14", - [{load_module,emqx_channel,brutal_purge,soft_purge,[]}, + [{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, + {load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {add_module,emqx_const_v2}, + {load_module,emqx_channel,brutal_purge,soft_purge,[]}, {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, @@ -41,7 +53,10 @@ {load_module,emqx_app,brutal_purge,soft_purge,[]}, {load_module,emqx_rule_actions_trans,brutal_purge,soft_purge,[]}]}, {"4.4.13", - [{load_module,emqx_channel,brutal_purge,soft_purge,[]}, + [{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, + {load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {add_module,emqx_const_v2}, + {load_module,emqx_channel,brutal_purge,soft_purge,[]}, {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, @@ -58,7 +73,10 @@ {load_module,emqx_relup,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]}]}, {"4.4.12", - [{load_module,emqx_channel,brutal_purge,soft_purge,[]}, + [{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, + {load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {add_module,emqx_const_v2}, + {load_module,emqx_channel,brutal_purge,soft_purge,[]}, {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, @@ -75,7 +93,10 @@ {load_module,emqx_relup,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]}]}, {"4.4.11", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, + {load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {add_module,emqx_const_v2}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_plugins,brutal_purge,soft_purge,[]}, @@ -94,7 +115,9 @@ {load_module,emqx_session,brutal_purge,soft_purge,[]}, {load_module,emqx_channel,brutal_purge,soft_purge,[]}]}, {"4.4.10", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {add_module,emqx_const_v2}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_plugins,brutal_purge,soft_purge,[]}, @@ -122,7 +145,9 @@ {apply,{application,set_env, [gen_rpc,insecure_auth_fallback_allowed,true]}}]}, {"4.4.9", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {add_module,emqx_const_v2}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_broker,brutal_purge,soft_purge,[]}, @@ -155,7 +180,9 @@ {apply,{application,set_env, [gen_rpc,insecure_auth_fallback_allowed,true]}}]}, {"4.4.8", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {add_module,emqx_const_v2}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_broker,brutal_purge,soft_purge,[]}, @@ -189,7 +216,9 @@ {apply,{application,set_env, [gen_rpc,insecure_auth_fallback_allowed,true]}}]}, {"4.4.7", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {add_module,emqx_const_v2}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_broker,brutal_purge,soft_purge,[]}, @@ -223,7 +252,9 @@ {apply,{application,set_env, [gen_rpc,insecure_auth_fallback_allowed,true]}}]}, {"4.4.6", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {add_module,emqx_const_v2}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_broker,brutal_purge,soft_purge,[]}, @@ -257,7 +288,9 @@ {apply,{application,set_env, [gen_rpc,insecure_auth_fallback_allowed,true]}}]}, {"4.4.5", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {add_module,emqx_const_v2}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_broker,brutal_purge,soft_purge,[]}, @@ -293,7 +326,9 @@ {apply,{application,set_env, [gen_rpc,insecure_auth_fallback_allowed,true]}}]}, {"4.4.4", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {add_module,emqx_const_v2}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_banned,brutal_purge,soft_purge,[]}, @@ -335,7 +370,9 @@ {apply,{application,set_env, [gen_rpc,insecure_auth_fallback_allowed,true]}}]}, {"4.4.3", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {add_module,emqx_const_v2}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_banned,brutal_purge,soft_purge,[]}, @@ -383,7 +420,9 @@ {apply,{application,set_env, [gen_rpc,insecure_auth_fallback_allowed,true]}}]}, {"4.4.2", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {add_module,emqx_const_v2}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_banned,brutal_purge,soft_purge,[]}, @@ -432,7 +471,9 @@ {apply,{application,set_env, [gen_rpc,insecure_auth_fallback_allowed,true]}}]}, {"4.4.1", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {add_module,emqx_const_v2}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_rule_actions_trans,brutal_purge,soft_purge,[]}, @@ -485,7 +526,9 @@ {apply,{application,set_env, [gen_rpc,insecure_auth_fallback_allowed,true]}}]}, {"4.4.0", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {add_module,emqx_const_v2}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_rule_actions_trans,brutal_purge,soft_purge,[]}, @@ -543,19 +586,25 @@ [{"4.4.18", [{load_module,emqx_relup,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]}]}, - {"4.4.17", - [{load_module,emqx_relup,brutal_purge,soft_purge,[]}, + [{"4.4.17", + [{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, + {load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_relup,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]}, {load_module,emqx_plugins,brutal_purge,soft_purge,[]}]}, {"4.4.16", - [{load_module,emqx_channel,brutal_purge,soft_purge,[]}, + [{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, + {load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_channel,brutal_purge,soft_purge,[]}, {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_misc,brutal_purge,soft_purge,[]}, {load_module,emqx_plugins,brutal_purge,soft_purge,[]}, {load_module,emqx_relup,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]}]}, {"4.4.15", - [{load_module,emqx_channel,brutal_purge,soft_purge,[]}, + [{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, + {load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_channel,brutal_purge,soft_purge,[]}, {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx,brutal_purge,soft_purge,[]}, {load_module,emqx_plugins,brutal_purge,soft_purge,[]}, @@ -564,7 +613,9 @@ {load_module,emqx_relup,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]}]}, {"4.4.14", - [{load_module,emqx_channel,brutal_purge,soft_purge,[]}, + [{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, + {load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_channel,brutal_purge,soft_purge,[]}, {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_plugins,brutal_purge,soft_purge,[]}, @@ -580,7 +631,9 @@ {load_module,emqx_app,brutal_purge,soft_purge,[]}, {load_module,emqx_rule_actions_trans,brutal_purge,soft_purge,[]}]}, {"4.4.13", - [{load_module,emqx_channel,brutal_purge,soft_purge,[]}, + [{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, + {load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_channel,brutal_purge,soft_purge,[]}, {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_plugins,brutal_purge,soft_purge,[]}, @@ -597,7 +650,9 @@ {load_module,emqx_relup,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]}]}, {"4.4.12", - [{load_module,emqx_channel,brutal_purge,soft_purge,[]}, + [{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, + {load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_channel,brutal_purge,soft_purge,[]}, {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, @@ -614,7 +669,9 @@ {load_module,emqx_relup,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]}]}, {"4.4.11", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, + {load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_broker,brutal_purge,soft_purge,[]}, @@ -633,7 +690,8 @@ {load_module,emqx_session,brutal_purge,soft_purge,[]}, {delete_module,emqx_cover}]}, {"4.4.10", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_broker,brutal_purge,soft_purge,[]}, @@ -658,7 +716,8 @@ {delete_module,emqx_crl_cache}, {delete_module,emqx_ocsp_cache}]}, {"4.4.9", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_broker,brutal_purge,soft_purge,[]}, @@ -687,7 +746,8 @@ {delete_module,emqx_crl_cache}, {delete_module,emqx_ocsp_cache}]}, {"4.4.8", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_broker,brutal_purge,soft_purge,[]}, @@ -717,7 +777,8 @@ {delete_module,emqx_crl_cache}, {delete_module,emqx_ocsp_cache}]}, {"4.4.7", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_broker,brutal_purge,soft_purge,[]}, @@ -747,7 +808,8 @@ {delete_module,emqx_crl_cache}, {delete_module,emqx_ocsp_cache}]}, {"4.4.6", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_broker,brutal_purge,soft_purge,[]}, @@ -777,7 +839,8 @@ {delete_module,emqx_crl_cache}, {delete_module,emqx_ocsp_cache}]}, {"4.4.5", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_broker,brutal_purge,soft_purge,[]}, @@ -809,7 +872,8 @@ {delete_module,emqx_crl_cache}, {delete_module,emqx_ocsp_cache}]}, {"4.4.4", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_banned,brutal_purge,soft_purge,[]}, @@ -847,7 +911,8 @@ {delete_module,emqx_crl_cache}, {delete_module,emqx_ocsp_cache}]}, {"4.4.3", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_banned,brutal_purge,soft_purge,[]}, @@ -890,7 +955,8 @@ {delete_module,emqx_crl_cache}, {delete_module,emqx_ocsp_cache}]}, {"4.4.2", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_banned,brutal_purge,soft_purge,[]}, @@ -934,7 +1000,8 @@ {delete_module,emqx_crl_cache}, {delete_module,emqx_ocsp_cache}]}, {"4.4.1", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_rule_actions_trans,brutal_purge,soft_purge,[]}, @@ -982,7 +1049,8 @@ {delete_module,emqx_crl_cache}, {delete_module,emqx_ocsp_cache}]}, {"4.4.0", - [{load_module,emqx_pool,brutal_purge,soft_purge,[]}, + [{load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_pool,brutal_purge,soft_purge,[]}, {load_module,emqx_vm,brutal_purge,soft_purge,[]}, {load_module,emqx_keepalive,brutal_purge,soft_purge,[]}, {load_module,emqx_rule_actions_trans,brutal_purge,soft_purge,[]}, From 90efea4765bce6734b84e9f308f0eb3f48f5d2e4 Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 5 May 2023 16:12:00 +0200 Subject: [PATCH 09/13] fix(test): sign CSR2 --- test/emqx_test_tls_certs_helper.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/emqx_test_tls_certs_helper.erl b/test/emqx_test_tls_certs_helper.erl index 6b9921884..e51ff13a9 100644 --- a/test/emqx_test_tls_certs_helper.erl +++ b/test/emqx_test_tls_certs_helper.erl @@ -98,7 +98,7 @@ gen_host_cert(H, CaName, Path, Opts) -> 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, HCSR, ca_cert_name(Path, CaName), ca_key_name(Path, CaName), HPEM2), + 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)), From f8bb1b7b556dd595a04d2fca4810949560e34a8b Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 5 May 2023 18:14:39 +0200 Subject: [PATCH 10/13] fix(tls-partial-chain): stack trace --- src/emqx_tls_lib.erl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/emqx_tls_lib.erl b/src/emqx_tls_lib.erl index 11299787c..ad4e5c0c4 100644 --- a/src/emqx_tls_lib.erl +++ b/src/emqx_tls_lib.erl @@ -200,11 +200,10 @@ replace(Opts, Key, Value) -> [{Key, Value} | proplists:delete(Key, Opts)]. rootfun_trusted_ca_from_cacertfile(SslOpts) -> Cacertfile = proplists:get_value(cacertfile, SslOpts, undefined), try do_rootfun_trusted_ca_from_cacertfile(Cacertfile) - catch _Error:_ -> + 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.. - ?LOG(error, "Failed to look for trusted cacert from cacertfile. loc: ~p:~p", - [?MODULE, ?FUNCTION_NAME]), + ?LOG(error, "Failed to look for trusted cacert from cacertfile. Stacktrace: ~p", [ST]), throw({error, ?FUNCTION_NAME}) end. do_rootfun_trusted_ca_from_cacertfile(Cacertfile) -> From 151176a6be6c7dde5374e51660efdd728d5888a0 Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 9 May 2023 09:52:10 +0200 Subject: [PATCH 11/13] test(tls-partial-chain): add more tests --- ...istener_tls_verify_partial_chain_SUITE.erl | 32 +++++++++++++++++++ test/emqx_test_tls_certs_helper.erl | 9 ++++++ 2 files changed, 41 insertions(+) diff --git a/test/emqx_listener_tls_verify_partial_chain_SUITE.erl b/test/emqx_listener_tls_verify_partial_chain_SUITE.erl index 4b90b3a31..4f1d613e5 100644 --- a/test/emqx_listener_tls_verify_partial_chain_SUITE.erl +++ b/test/emqx_listener_tls_verify_partial_chain_SUITE.erl @@ -281,6 +281,38 @@ t_conn_success_with_server_intermediate_and_client_root_chain(Config) -> 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, [ {cacertfile, filename:join(DataDir, "all-CAcerts-bundle.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(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")} + ], 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, [ {cacertfile, filename:join(DataDir, "two-intermediates-bundle.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(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")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ok = ssl:close(Socket). + t_error_handling_invalid_cacertfile(Config) -> Port = emqx_test_tls_certs_helper:select_free_port(ssl), DataDir = ?config(data_dir, Config), diff --git a/test/emqx_test_tls_certs_helper.erl b/test/emqx_test_tls_certs_helper.erl index e51ff13a9..d527b6468 100644 --- a/test/emqx_test_tls_certs_helper.erl +++ b/test/emqx_test_tls_certs_helper.erl @@ -246,4 +246,13 @@ generate_tls_certs(Config) -> 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") ])). From 285d3dabc7f605ae1a765da0a7f6a87e988f40d5 Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 9 May 2023 12:56:35 +0200 Subject: [PATCH 12/13] feat(tls-partial-chain): support CAcert renewal The listener could support two versions of CAcerts if partial_chain is set to `two_cacerts_from_cacertfile` --- src/emqx_const_v2.erl | 26 +++++- src/emqx_tls_lib.erl | 19 ++-- ...istener_tls_verify_partial_chain_SUITE.erl | 89 ++++++++++++++++++- test/emqx_test_tls_certs_helper.erl | 2 + 4 files changed, 124 insertions(+), 12 deletions(-) diff --git a/src/emqx_const_v2.erl b/src/emqx_const_v2.erl index 0c211ec5f..d692dd3b4 100644 --- a/src/emqx_const_v2.erl +++ b/src/emqx_const_v2.erl @@ -21,7 +21,25 @@ -export([ make_tls_root_fun/2 ]). -make_tls_root_fun(cacert_from_cacertfile, CADer) -> - fun(_InputChain) -> - {trusted_ca, CADer} - end. +%% @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. diff --git a/src/emqx_tls_lib.erl b/src/emqx_tls_lib.erl index ad4e5c0c4..720c886a9 100644 --- a/src/emqx_tls_lib.erl +++ b/src/emqx_tls_lib.erl @@ -191,26 +191,31 @@ opt_partial_chain(SslOpts) -> false -> SslOpts; V when V =:= cacert_from_cacertfile orelse V == true -> - replace(SslOpts, partial_chain, rootfun_trusted_ca_from_cacertfile(SslOpts)) + replace(SslOpts, partial_chain, rootfun_trusted_ca_from_cacertfile(1, SslOpts)); + V when V =:= two_cacerts_from_cacertfile -> %% for certificate rotations + replace(SslOpts, partial_chain, rootfun_trusted_ca_from_cacertfile(2, SslOpts)) end. replace(Opts, Key, Value) -> [{Key, Value} | proplists:delete(Key, Opts)]. %% @doc Helper, make TLS root_fun -rootfun_trusted_ca_from_cacertfile(SslOpts) -> +rootfun_trusted_ca_from_cacertfile(NumOfCerts, SslOpts) -> Cacertfile = proplists:get_value(cacertfile, SslOpts, undefined), - try do_rootfun_trusted_ca_from_cacertfile(Cacertfile) + try do_rootfun_trusted_ca_from_cacertfile(NumOfCerts, Cacertfile) 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.. ?LOG(error, "Failed to look for trusted cacert from cacertfile. Stacktrace: ~p", [ST]), throw({error, ?FUNCTION_NAME}) end. -do_rootfun_trusted_ca_from_cacertfile(Cacertfile) -> +do_rootfun_trusted_ca_from_cacertfile(NumOfCerts, Cacertfile) -> {ok, PemBin} = file:read_file(Cacertfile), - %% The last one should be the top parent in the chain if it is a chain - {'Certificate', CADer, _} = lists:last(public_key:pem_decode(PemBin)), - emqx_const_v2:make_tls_root_fun(cacert_from_cacertfile, CADer). + %% 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). -if(?OTP_RELEASE > 22). -ifdef(TEST). diff --git a/test/emqx_listener_tls_verify_partial_chain_SUITE.erl b/test/emqx_listener_tls_verify_partial_chain_SUITE.erl index 4f1d613e5..224e1a8b6 100644 --- a/test/emqx_listener_tls_verify_partial_chain_SUITE.erl +++ b/test/emqx_listener_tls_verify_partial_chain_SUITE.erl @@ -115,7 +115,56 @@ t_conn_fail_with_renewed_intermediate_cacert_and_client_using_old_bundle(Config) fail_when_no_ssl_alert(Socket, unknown_ca), ssl:close(Socket). -%%@TODO limitation: EMQX is not able to check if the trusted CAcert and the old CAcert belongs to same 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, [ {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} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(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")} + ], 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, [ {cacertfile, filename:join(DataDir, "intermediate2_renewed.pem")} + , {certfile, filename:join(DataDir, "server2.pem")} + , {keyfile, filename:join(DataDir, "server2.key")} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(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")} + ], 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, [ {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} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(ssl, Port, Options), + {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2.pem")} + ], 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), @@ -131,6 +180,44 @@ t_conn_fail_with_renewed_and_old_intermediate_cacert_and_client_using_old_bundle fail_when_no_ssl_alert(Socket, unknown_ca), ssl:close(Socket). +%% @doc verify when config (two_cacerts_from_cacertfile) allows two versions of certs from same trusted CA. +t_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, [ {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} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(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")} + ], 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_fail_success_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, [ {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} + | ?config(ssl_config, Config) + ]}], + emqx_listeners:start_listener(ssl, Port, Options), + {ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "all-CAcerts-bundle.pem")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ssl:close(Socket). + 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), diff --git a/test/emqx_test_tls_certs_helper.erl b/test/emqx_test_tls_certs_helper.erl index d527b6468..8882f9f40 100644 --- a/test/emqx_test_tls_certs_helper.erl +++ b/test/emqx_test_tls_certs_helper.erl @@ -214,6 +214,8 @@ generate_tls_certs(Config) -> 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"), From 7894bb040ecc439f2df4755e848b2ba980c8bd55 Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 9 May 2023 16:06:27 +0200 Subject: [PATCH 13/13] chore: update app up src --- src/emqx.appup.src | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/emqx.appup.src b/src/emqx.appup.src index 1bbff0a77..fdf2d4aa0 100644 --- a/src/emqx.appup.src +++ b/src/emqx.appup.src @@ -2,9 +2,12 @@ %% Unless you know what you are doing, DO NOT edit manually!! {VSN, [{"4.4.18", - [{load_module,emqx_relup,brutal_purge,soft_purge,[]}, + [{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, + {load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {add_module,emqx_const_v2}, + {load_module,emqx_relup,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]}]}, - [{"4.4.17", + {"4.4.17", [{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, {load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, {add_module,emqx_const_v2}, @@ -584,9 +587,11 @@ [gen_rpc,insecure_auth_fallback_allowed,true]}}]}, {<<".*">>,[]}], [{"4.4.18", - [{load_module,emqx_relup,brutal_purge,soft_purge,[]}, + [{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, + {load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_relup,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]}]}, - [{"4.4.17", + {"4.4.17", [{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, {load_module,emqx_tls_lib,brutal_purge,soft_purge,[]}, {load_module,emqx_relup,brutal_purge,soft_purge,[]},