From 0b95a08d32db66903ba8155d7e405c901f044427 Mon Sep 17 00:00:00 2001 From: William Yang Date: Thu, 5 Oct 2023 14:51:37 +0200 Subject: [PATCH 01/16] feat(tls): port partial_chain, part 1 --- apps/emqx/src/emqx_const_v2.erl | 126 +++++++++++++++++++++++++++++++ apps/emqx/src/emqx_listeners.erl | 14 +++- apps/emqx/src/emqx_tls_lib.erl | 54 +++++++++++++ 3 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 apps/emqx/src/emqx_const_v2.erl diff --git a/apps/emqx/src/emqx_const_v2.erl b/apps/emqx/src/emqx_const_v2.erl new file mode 100644 index 000000000..9fb2b7fa7 --- /dev/null +++ b/apps/emqx/src/emqx_const_v2.erl @@ -0,0 +1,126 @@ +%%-------------------------------------------------------------------- +%% 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_verify_fun/2 +]). + +-include_lib("public_key/include/public_key.hrl"). +%% @doc Build a root fun for verify TLS partial_chain. +%% The `InputChain' is composed by OTP SSL with local cert store +%% AND the cert (chain if any) from the client. +%% @end +make_tls_root_fun(cacert_from_cacertfile, [Trusted]) -> + %% Allow only one trusted ca cert, and just return the defined trusted CA cert, + fun(_InputChain) -> + %% Note, returing `trusted_ca` doesn't really mean it accepts the connection + %% OTP SSL app will do the path validation, signature validation subsequently. + {trusted_ca, Trusted} + end; +make_tls_root_fun(cacert_from_cacertfile, [TrustedOne, TrustedTwo]) -> + %% Allow two trusted CA certs in case of CA cert renewal + %% This is a little expensive call as it compares the binaries. + fun(InputChain) -> + case lists:member(TrustedOne, InputChain) of + true -> + {trusted_ca, TrustedOne}; + false -> + {trusted_ca, TrustedTwo} + end + end. + +make_tls_verify_fun(verify_cert_extKeyUsage, KeyUsages) -> + AllowedKeyUsages = ext_key_opts(KeyUsages), + {fun verify_fun_peer_extKeyUsage/3, AllowedKeyUsages}. + +verify_fun_peer_extKeyUsage(_, {bad_cert, invalid_ext_key_usage}, UserState) -> + %% !! Override OTP verify peer default + %% OTP SSL is unhappy with the ext_key_usage but we will check on our own. + {unknown, UserState}; +verify_fun_peer_extKeyUsage(_, {bad_cert, _} = Reason, _UserState) -> + %% OTP verify_peer default + {fail, Reason}; +verify_fun_peer_extKeyUsage(_, {extension, _}, UserState) -> + %% OTP verify_peer default + {unknown, UserState}; +verify_fun_peer_extKeyUsage(_, valid, UserState) -> + %% OTP verify_peer default + {valid, UserState}; +verify_fun_peer_extKeyUsage( + #'OTPCertificate'{tbsCertificate = #'OTPTBSCertificate'{extensions = ExtL}}, + %% valid peer cert + valid_peer, + AllowedKeyUsages +) -> + %% override OTP verify_peer default + %% must have id-ce-extKeyUsage + case lists:keyfind(?'id-ce-extKeyUsage', 2, ExtL) of + #'Extension'{extnID = ?'id-ce-extKeyUsage', extnValue = VL} -> + case do_verify_ext_key_usage(VL, AllowedKeyUsages) of + true -> + %% pass the check, + %% fallback to OTP verify_peer default + {valid, AllowedKeyUsages}; + false -> + {fail, extKeyUsage_unmatched} + end; + _ -> + {fail, extKeyUsage_not_set} + end. + +%% @doc check required extkeyUsages are presented in the cert +do_verify_ext_key_usage(_, []) -> + %% Verify finished + true; +do_verify_ext_key_usage(CertExtL, [Usage | T] = _Required) -> + case lists:member(Usage, CertExtL) of + true -> + do_verify_ext_key_usage(CertExtL, T); + false -> + false + end. + +%% @doc Helper tls cert extension +-spec ext_key_opts + (string()) -> [OidString :: string() | public_key:oid()]; + (undefined) -> undefined. +ext_key_opts(Str) -> + Usages = string:tokens(Str, ","), + lists:map( + fun + ("clientAuth") -> + ?'id-kp-clientAuth'; + ("serverAuth") -> + ?'id-kp-serverAuth'; + ("codeSigning") -> + ?'id-kp-codeSigning'; + ("emailProtection") -> + ?'id-kp-emailProtection'; + ("timeStamping") -> + ?'id-kp-timeStamping'; + ("ocspSigning") -> + ?'id-kp-OCSPSigning'; + ([$O, $I, $D, $: | OidStr]) -> + OidList = string:tokens(OidStr, "."), + list_to_tuple(lists:map(fun list_to_integer/1, OidList)) + end, + Usages + ). diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index f2c30c6a7..181841c65 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -610,7 +610,9 @@ esockd_opts(ListenerId, Type, Name, Opts0) -> ssl -> OptsWithCRL = inject_crl_config(Opts0), OptsWithSNI = inject_sni_fun(ListenerId, OptsWithCRL), - SSLOpts = ssl_opts(OptsWithSNI), + OptsWithRootFun = inject_root_fun(OptsWithSNI), + OptsWithVerifyFun = inject_verify_fun(OptsWithRootFun), + SSLOpts = ssl_opts(OptsWithVerifyFun), Opts3#{ssl_options => SSLOpts, tcp_options => tcp_opts(Opts0)} end ). @@ -956,6 +958,16 @@ quic_listener_optional_settings() -> stateless_operation_expiration_ms ]. +inject_root_fun(#{ssl_options := SslOpts} = Opts) -> + Opts#{ssl_options := emqx_tls_lib:opt_partial_chain(SslOpts)}; +inject_root_fun(Opts) -> + Opts. + +inject_verify_fun(#{ssl_options := SslOpts} = Opts) -> + Opts#{ssl_options := emqx_tls_lib:opt_verify_fun(SslOpts)}; +inject_verify_fun(Opts) -> + Opts. + inject_sni_fun(ListenerId, Conf = #{ssl_options := #{ocsp := #{enable_ocsp_stapling := true}}}) -> emqx_ocsp_cache:inject_sni_fun(ListenerId, Conf); inject_sni_fun(_ListenerId, Conf) -> diff --git a/apps/emqx/src/emqx_tls_lib.erl b/apps/emqx/src/emqx_tls_lib.erl index e742b31ba..3c08487a4 100644 --- a/apps/emqx/src/emqx_tls_lib.erl +++ b/apps/emqx/src/emqx_tls_lib.erl @@ -23,6 +23,8 @@ default_ciphers/0, selected_ciphers/1, integral_ciphers/2, + opt_partial_chain/1, + opt_verify_fun/1, all_ciphers_set_cached/0 ]). @@ -679,3 +681,55 @@ ensure_ssl_file_key(SSL, RequiredKeyPaths) -> [] -> ok; Miss -> {error, #{reason => ssl_file_option_not_found, which_options => Miss}} end. + +%% @doc enable TLS partial_chain validation if set. +-spec opt_partial_chain(SslOpts :: map()) -> NewSslOpts :: map(). +opt_partial_chain(#{partial_chain := false} = SslOpts) -> + maps:remove(partial_chain, SslOpts); +opt_partial_chain(#{partial_chain := true} = SslOpts) -> + SslOpts#{partial_chain := rootfun_trusted_ca_from_cacertfile(1, SslOpts)}; +opt_partial_chain(#{partial_chain := cacert_from_cacertfile} = SslOpts) -> + SslOpts#{partial_chain := rootfun_trusted_ca_from_cacertfile(1, SslOpts)}; +opt_partial_chain(#{partial_chain := two_cacerts_from_cacertfile} = SslOpts) -> + SslOpts#{partial_chain := rootfun_trusted_ca_from_cacertfile(2, SslOpts)}; +opt_partial_chain(SslOpts) -> + SslOpts. + +%% @doc make verify_fun if set. +-spec opt_verify_fun(SslOpts :: map()) -> NewSslOpts :: map(). +opt_verify_fun(#{verify_peer_ext_key_usage := V} = SslOpts) -> + SslOpts#{verify_fun => emqx_const_v2:make_tls_verify_fun(verify_cert_extKeyUsage, V)}; +opt_verify_fun(SslOpts) -> + SslOpts. + +%% @doc Helper, make TLS root_fun +rootfun_trusted_ca_from_cacertfile(NumOfCerts, #{cacertfile := Cacertfile}) -> + case file:read_file(Cacertfile) of + {ok, PemBin} -> + try + do_rootfun_trusted_ca_from_cacertfile(NumOfCerts, PemBin) + catch + _Error:_Info:ST -> + %% The cacertfile will be checked by OTP SSL as well and OTP choice to be silent on this. + %% We are touching security sutffs, don't leak extra info.. + ?SLOG(error, #{ + msg => "trusted_cacert_not_found_in_cacertfile", stacktrace => ST + }), + throw({error, ?FUNCTION_NAME}) + end; + {error, Reason} -> + throw({error, {read_cacertfile_error, Cacertfile, Reason}}) + end; +rootfun_trusted_ca_from_cacertfile(_NumOfCerts, _SslOpts) -> + throw({error, cacertfile_unset}). + +do_rootfun_trusted_ca_from_cacertfile(NumOfCerts, PemBin) -> + %% The last one or two should be the top parent in the chain if it is a chain + Certs = public_key:pem_decode(PemBin), + Pos = length(Certs) - NumOfCerts + 1, + Trusted = [ + CADer + || {'Certificate', CADer, _} <- + lists:sublist(public_key:pem_decode(PemBin), Pos, NumOfCerts) + ], + emqx_const_v2:make_tls_root_fun(cacert_from_cacertfile, Trusted). From fa4357ce89e141fbbf41ad525ab34ea472fd138e Mon Sep 17 00:00:00 2001 From: William Yang Date: Thu, 5 Oct 2023 14:54:14 +0200 Subject: [PATCH 02/16] test: port listener tls partial_chain --- .../emqx_listener_tls_verify_chain_SUITE.erl | 248 +++++++ ...mqx_listener_tls_verify_keyusage_SUITE.erl | 360 ++++++++++ ...istener_tls_verify_partial_chain_SUITE.erl | 668 ++++++++++++++++++ apps/emqx/test/emqx_test_tls_certs_helper.erl | 311 ++++++++ 4 files changed, 1587 insertions(+) create mode 100644 apps/emqx/test/emqx_listener_tls_verify_chain_SUITE.erl create mode 100644 apps/emqx/test/emqx_listener_tls_verify_keyusage_SUITE.erl create mode 100644 apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl create mode 100644 apps/emqx/test/emqx_test_tls_certs_helper.erl diff --git a/apps/emqx/test/emqx_listener_tls_verify_chain_SUITE.erl b/apps/emqx/test/emqx_listener_tls_verify_chain_SUITE.erl new file mode 100644 index 000000000..c38426523 --- /dev/null +++ b/apps/emqx/test/emqx_listener_tls_verify_chain_SUITE.erl @@ -0,0 +1,248 @@ +%%-------------------------------------------------------------------- +%% 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("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-import( + emqx_test_tls_certs_helper, + [ + emqx_start_listener/4, + fail_when_ssl_error/1, + fail_when_no_ssl_alert/2, + generate_tls_certs/1 + ] +). + +all() -> emqx_common_test_helpers:all(?MODULE). + +init_per_suite(Config) -> + generate_tls_certs(Config), + application:ensure_all_started(esockd), + [{ssl_config, ssl_config_verify_peer()} | Config]. + +end_per_suite(_Config) -> + application:stop(esockd). + +t_conn_fail_with_intermediate_ca_cert(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [ + {ssl_options, [ + {cacertfile, filename:join(DataDir, "intermediate1.pem")}, + {certfile, filename:join(DataDir, "server1.pem")}, + {keyfile, filename:join(DataDir, "server1.key")} + | ?config(ssl_config, Config) + ]} + ], + emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], + 1000 + ), + + fail_when_no_ssl_alert(Socket, unknown_ca), + ok = ssl:close(Socket). + +t_conn_fail_with_other_intermediate_ca_cert(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [ + {ssl_options, [ + {cacertfile, filename:join(DataDir, "intermediate1.pem")}, + {certfile, filename:join(DataDir, "server1.pem")}, + {keyfile, filename:join(DataDir, "server1.key")} + | ?config(ssl_config, Config) + ]} + ], + emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2.pem")} + ], + 1000 + ), + + fail_when_no_ssl_alert(Socket, unknown_ca), + ok = ssl:close(Socket). + +t_conn_success_with_server_client_composed_complete_chain(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + %% Server has root ca cert + Options = [ + {ssl_options, [ + {cacertfile, filename:join(DataDir, "root.pem")}, + {certfile, filename:join(DataDir, "server2.pem")}, + {keyfile, filename:join(DataDir, "server2.key")} + | ?config(ssl_config, Config) + ]} + ], + %% Client has complete chain + emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")} + ], + 1000 + ), + fail_when_ssl_error(Socket), + ok = ssl:close(Socket). + +t_conn_success_with_other_signed_client_composed_complete_chain(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + %% Server has root ca cert + Options = [ + {ssl_options, [ + {cacertfile, filename:join(DataDir, "root.pem")}, + {certfile, filename:join(DataDir, "server1.pem")}, + {keyfile, filename:join(DataDir, "server1.key")} + | ?config(ssl_config, Config) + ]} + ], + %% Client has partial_chain + emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")} + ], + 1000 + ), + fail_when_ssl_error(Socket), + ok = ssl:close(Socket). + +t_conn_success_with_renewed_intermediate_root_bundle(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + %% Server has root ca cert + Options = [ + {ssl_options, [ + {cacertfile, filename:join(DataDir, "intermediate1_renewed-root-bundle.pem")}, + {certfile, filename:join(DataDir, "server1.pem")}, + {keyfile, filename:join(DataDir, "server1.key")} + | ?config(ssl_config, Config) + ]} + ], + emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], + 1000 + ), + fail_when_ssl_error(Socket), + ok = ssl:close(Socket). + +t_conn_success_with_client_complete_cert_chain(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [ + {ssl_options, [ + {cacertfile, filename:join(DataDir, "root.pem")}, + {certfile, filename:join(DataDir, "server2.pem")}, + {keyfile, filename:join(DataDir, "server2.key")} + | ?config(ssl_config, Config) + ]} + ], + emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2-complete-bundle.pem")} + ], + 1000 + ), + fail_when_ssl_error(Socket), + ok = ssl:close(Socket). + +t_conn_fail_with_server_partial_chain(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + %% imcomplete at server side + Options = [ + {ssl_options, [ + {cacertfile, filename:join(DataDir, "intermediate2.pem")}, + {certfile, filename:join(DataDir, "server2.pem")}, + {keyfile, filename:join(DataDir, "server2.key")} + | ?config(ssl_config, Config) + ]} + ], + emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")} + ], + 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/apps/emqx/test/emqx_listener_tls_verify_keyusage_SUITE.erl b/apps/emqx/test/emqx_listener_tls_verify_keyusage_SUITE.erl new file mode 100644 index 000000000..c12618566 --- /dev/null +++ b/apps/emqx/test/emqx_listener_tls_verify_keyusage_SUITE.erl @@ -0,0 +1,360 @@ +%%-------------------------------------------------------------------- +%% 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_keyusage_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, + gen_host_cert/4 +]). + +all() -> + [ + {group, full_chain}, + {group, partial_chain} + ]. + +all_tc() -> + emqx_ct:all(?MODULE). + +groups() -> + [ + {partial_chain, [], all_tc()}, + {full_chain, [], all_tc()} + ]. + +init_per_suite(Config) -> + generate_tls_certs(Config), + application:ensure_all_started(esockd), + Config. + +end_per_suite(_Config) -> + application:stop(esockd). + +init_per_group(full_chain, Config) -> + [{ssl_config, ssl_config_verify_peer_full_chain(Config)} | Config]; +init_per_group(partial_chain, Config) -> + [{ssl_config, ssl_config_verify_peer_partial_chain(Config)} | Config]; +init_per_group(_, Config) -> + Config. + +end_per_group(_, Config) -> + Config. + +t_conn_success_verify_peer_ext_key_usage_unset(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + %% Given listener keyusage unset + Options = [{ssl_options, ?config(ssl_config, Config)}], + emqx_listeners:start_listener(ssl, Port, Options), + %% when client connect with cert without keyusage ext + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], + 1000 + ), + %% Then connection success + fail_when_ssl_error(Socket), + ok = ssl:close(Socket). + +t_conn_success_verify_peer_ext_key_usage_undefined(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + %% Give listener keyusage is set to undefined + Options = [ + {ssl_options, [ + {verify_peer_ext_key_usage, undefined} + | ?config(ssl_config, Config) + ]} + ], + emqx_listeners:start_listener(ssl, Port, Options), + %% when client connect with cert without keyusages ext + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], + 1000 + ), + %% Then connection success + fail_when_ssl_error(Socket), + ok = ssl:close(Socket). + +t_conn_success_verify_peer_ext_key_usage_matched_predefined(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + %% Give listener keyusage is set to clientAuth + Options = [ + {ssl_options, [ + {verify_peer_ext_key_usage, "clientAuth"} + | ?config(ssl_config, Config) + ]} + ], + + %% When client cert has clientAuth that is matched + gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth"), + emqx_listeners:start_listener(ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, client_key_file(DataDir, ?FUNCTION_NAME)}, + {certfile, client_pem_file(DataDir, ?FUNCTION_NAME)} + ], + 1000 + ), + %% Then connection success + fail_when_ssl_error(Socket), + ok = ssl:close(Socket). + +t_conn_success_verify_peer_ext_key_usage_matched_raw_oid(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + %% Give listener keyusage is set to raw OID + + %% from OTP-PUB-KEY.hrl + Options = [ + {ssl_options, [ + {verify_peer_ext_key_usage, "OID:1.3.6.1.5.5.7.3.2"} + | ?config(ssl_config, Config) + ]} + ], + emqx_listeners:start_listener(ssl, Port, Options), + %% When client cert has keyusage and matched. + gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth"), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, client_key_file(DataDir, ?FUNCTION_NAME)}, + {certfile, client_pem_file(DataDir, ?FUNCTION_NAME)} + ], + 1000 + ), + %% Then connection success + fail_when_ssl_error(Socket), + ok = ssl:close(Socket). + +t_conn_success_verify_peer_ext_key_usage_matched_ordered_list(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + + %% Give listener keyusage is clientAuth,serverAuth + Options = [ + {ssl_options, [ + {verify_peer_ext_key_usage, "clientAuth,serverAuth"} + | ?config(ssl_config, Config) + ]} + ], + emqx_listeners:start_listener(ssl, Port, Options), + %% When client cert has the same keyusage ext list + gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth,serverAuth"), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, client_key_file(DataDir, ?FUNCTION_NAME)}, + {certfile, client_pem_file(DataDir, ?FUNCTION_NAME)} + ], + 1000 + ), + %% Then connection success + fail_when_ssl_error(Socket), + ok = ssl:close(Socket). + +t_conn_success_verify_peer_ext_key_usage_matched_unordered_list(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + %% Give listener keyusage is clientAuth,serverAuth + Options = [ + {ssl_options, [ + {verify_peer_ext_key_usage, "serverAuth,clientAuth"} + | ?config(ssl_config, Config) + ]} + ], + emqx_listeners:start_listener(ssl, Port, Options), + %% When client cert has the same keyusage ext list but different order + gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth,serverAuth"), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, client_key_file(DataDir, ?FUNCTION_NAME)}, + {certfile, client_pem_file(DataDir, ?FUNCTION_NAME)} + ], + 1000 + ), + %% Then connection success + fail_when_ssl_error(Socket), + ok = ssl:close(Socket). + +t_conn_fail_verify_peer_ext_key_usage_unmatched_raw_oid(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + %% Give listener keyusage is using OID + Options = [ + {ssl_options, [ + {verify_peer_ext_key_usage, "OID:1.3.6.1.5.5.7.3.1"} + | ?config(ssl_config, Config) + ]} + ], + emqx_listeners:start_listener(ssl, Port, Options), + + %% When client cert has the keyusage but not matching OID + gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth"), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, client_key_file(DataDir, ?FUNCTION_NAME)}, + {certfile, client_pem_file(DataDir, ?FUNCTION_NAME)} + ], + 1000 + ), + + %% Then connecion should fail. + fail_when_no_ssl_alert(Socket, handshake_failure), + ok = ssl:close(Socket). + +t_conn_fail_verify_peer_ext_key_usage_empty_str(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [ + {ssl_options, [ + {verify_peer_ext_key_usage, ""} + | ?config(ssl_config, Config) + ]} + ], + %% Give listener keyusage is empty string + emqx_listeners:start_listener(ssl, Port, Options), + %% When client connect with cert without keyusage + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], + 1000 + ), + %% Then connecion should fail. + fail_when_no_ssl_alert(Socket, handshake_failure), + ok = ssl:close(Socket). + +t_conn_fail_client_keyusage_unmatch(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + + %% Give listener keyusage is clientAuth + Options = [ + {ssl_options, [ + {verify_peer_ext_key_usage, "clientAuth"} + | ?config(ssl_config, Config) + ]} + ], + emqx_listeners:start_listener(ssl, Port, Options), + %% When client connect with mismatch cert keyusage = codeSigning + gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "codeSigning"), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, client_key_file(DataDir, ?FUNCTION_NAME)}, + {certfile, client_pem_file(DataDir, ?FUNCTION_NAME)} + ], + 1000 + ), + %% Then connecion should fail. + fail_when_no_ssl_alert(Socket, handshake_failure), + ok = ssl:close(Socket). + +t_conn_fail_client_keyusage_incomplete(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + %% Give listener keyusage is codeSigning,clientAuth + Options = [ + {ssl_options, [ + {verify_peer_ext_key_usage, + "serverAuth,clientAuth,codeSigning,emailProtection,timeStamping,ocspSigning"} + | ?config(ssl_config, Config) + ]} + ], + emqx_listeners:start_listener(ssl, Port, Options), + %% When client connect with cert keyusage = clientAuth + gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "codeSigning"), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], + 1000 + ), + %% Then connection should fail + fail_when_no_ssl_alert(Socket, handshake_failure), + ok = ssl:close(Socket). + +%%% +%%% Helpers +%%% +gen_client_cert_ext_keyusage(Name, CA, DataDir, Usage) when is_atom(Name) -> + gen_client_cert_ext_keyusage(atom_to_list(Name), CA, DataDir, Usage); +gen_client_cert_ext_keyusage(Name, CA, DataDir, Usage) -> + gen_host_cert(Name, CA, DataDir, #{ext => "extendedKeyUsage=" ++ Usage}). + +client_key_file(DataDir, Name) -> + filename:join(DataDir, Name) ++ ".key". + +client_pem_file(DataDir, Name) -> + filename:join(DataDir, Name) ++ ".pem". + +ssl_config_verify_peer_full_chain(Config) -> + [ + {cacertfile, filename:join(?config(data_dir, Config), "intermediate1-root-bundle.pem")} + | ssl_config_verify_peer(Config) + ]. +ssl_config_verify_peer_partial_chain(Config) -> + [ + {cacertfile, filename:join(?config(data_dir, Config), "intermediate1.pem")}, + {partial_chain, true} + | ssl_config_verify_peer(Config) + ]. + +ssl_config_verify_peer(Config) -> + DataDir = ?config(data_dir, Config), + [ + {verify, verify_peer}, + {fail_if_no_peer_cert, true}, + {keyfile, filename:join(DataDir, "server1.key")}, + {certfile, filename:join(DataDir, "server1.pem")} + %% , {log_level, debug} + ]. diff --git a/apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl b/apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl new file mode 100644 index 000000000..5ca00bc1d --- /dev/null +++ b/apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl @@ -0,0 +1,668 @@ +%%-------------------------------------------------------------------- +%% 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("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-import( + emqx_test_tls_certs_helper, + [ + emqx_start_listener/4, + fail_when_ssl_error/1, + fail_when_no_ssl_alert/2, + generate_tls_certs/1 + ] +). + +all() -> emqx_common_test_helpers:all(?MODULE). + +init_per_suite(Config) -> + generate_tls_certs(Config), + application:ensure_all_started(esockd), + dbg:tracer(process, {fun dbg:dhandler/2, group_leader()}), + dbg:p(all, c), + dbg:tpl(emqx_listeners, esockd_opts, cx), + dbg:tpl(emqx_listeners, inject_root_fun, cx), + dbg:tpl(esockd, open, cx), + + [{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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], + 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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], + 1000 + ), + fail_when_ssl_error(Socket), + ssl:close(Socket). + +t_conn_success_with_renewed_intermediate_cacert(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [ + {ssl_options, [ + {cacertfile, filename:join(DataDir, "intermediate1_renewed.pem")}, + {certfile, filename:join(DataDir, "server1.pem")}, + {keyfile, filename:join(DataDir, "server1.key")} + | ?config(ssl_config, Config) + ]} + ], + emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], + 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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2-complete-bundle.pem")} + ], + 1000 + ), + 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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")} + ], + 1000 + ), + fail_when_no_ssl_alert(Socket, unknown_ca), + ssl:close(Socket). + +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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2_renewed.pem")} + ], + 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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2_renewed.pem")} + ], + 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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2.pem")} + ], + 1000 + ), + fail_when_ssl_error(Socket), + ssl:close(Socket). + +%% @doc verify when config does not allow two versions of certs from same trusted CA. +t_conn_fail_with_renewed_and_old_intermediate_cacert_and_client_using_old_bundle(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [ + {ssl_options, [ + {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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")} + ], + 1000 + ), + 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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")} + ], + 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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "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), + 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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2.pem")} + ], + 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), + 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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], + 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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2.pem")} + ], + 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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2-complete-bundle.pem")} + ], + 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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2-complete-bundle.pem")} + ], + 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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2-complete-bundle.pem")} + ], + 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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2-complete-bundle.pem")} + ], + 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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")} + ], + 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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2-root-bundle.pem")} + ], + 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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2-root-bundle.pem")} + ], + 1000 + ), + fail_when_ssl_error(Socket), + ok = ssl:close(Socket). + +%% @doc once rootCA cert present in cacertfile, sibling CA signed Client cert could connect. +t_conn_success_with_server_all_CA_bundle_and_client_root_chain(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [ + {ssl_options, [ + {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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2-root-bundle.pem")} + ], + 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_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client2.key")}, + {certfile, filename:join(DataDir, "client2-root-bundle.pem")} + ], + 1000 + ), + fail_when_no_ssl_alert(Socket, unknown_ca), + ok = ssl:close(Socket). + +t_conn_fail_with_server_partial_chain_false_intermediate_cacert_and_client_cert(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [ + {ssl_options, [ + {cacertfile, filename:join(DataDir, "intermediate1.pem")}, + {certfile, filename:join(DataDir, "server1.pem")}, + {keyfile, filename:join(DataDir, "server1.key")}, + {partial_chain, false} + | ?config(ssl_config, Config) + ]} + ], + emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), + {ok, Socket} = ssl:connect( + {127, 0, 0, 1}, + Port, + [ + {keyfile, filename:join(DataDir, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], + 1000 + ), + fail_when_no_ssl_alert(Socket, unknown_ca), + ssl:close(Socket). + +t_error_handling_invalid_cacertfile(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + %% trigger error + Options = [ + {ssl_options, [ + {cacertfile, filename:join(DataDir, "server2.key")}, + {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_start_listener(?FUNCTION_NAME, ssl, Port, Options) + ). + +ssl_config_verify_partial_chain() -> + [ + {verify, verify_peer}, + {fail_if_no_peer_cert, true}, + {partial_chain, true} + ]. diff --git a/apps/emqx/test/emqx_test_tls_certs_helper.erl b/apps/emqx/test/emqx_test_tls_certs_helper.erl new file mode 100644 index 000000000..81babac19 --- /dev/null +++ b/apps/emqx/test/emqx_test_tls_certs_helper.erl @@ -0,0 +1,311 @@ +%%-------------------------------------------------------------------- +%% 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, + generate_tls_certs/1, + + fail_when_ssl_error/1, + fail_when_ssl_error/2, + fail_when_no_ssl_alert/2, + fail_when_no_ssl_alert/3, + + emqx_start_listener/4 +]). + +-include_lib("common_test/include/ct.hrl"). + +%%------------------------------------------------------------------------------- +%% Start Listener +%%------------------------------------------------------------------------------- +emqx_start_listener(Name, Type, Port, Opts) when is_list(Opts) -> + emqx_start_listener(Name, Type, Port, maps:from_list(Opts)); +emqx_start_listener(Name, ssl, Port, #{ssl_options := SslOptions} = Opts0) -> + Opts = Opts0#{ + bind => {{127, 0, 0, 1}, Port}, + mountpoint => <<>>, + zone => default, + ssl_options => maps:from_list(SslOptions) + }, + ct:pal("start listsner with ~p ~p", [Name, Opts]), + emqx_listeners:start_listener(ssl, Name, Opts). + +%%------------------------------------------------------------------------------- +%% TLS certs +%%------------------------------------------------------------------------------- +gen_ca(Path, Name) -> + %% Generate ca.pem and ca.key which will be used to generate certs + %% for hosts server and clients + ECKeyFile = eckey_name(Path), + filelib:ensure_dir(ECKeyFile), + os:cmd("openssl ecparam -name secp256r1 > " ++ ECKeyFile), + Cmd = lists:flatten( + io_lib:format( + "openssl req -new -x509 -nodes " + "-newkey ec:~s " + "-keyout ~s -out ~s -days 3650 " + "-addext basicConstraints=CA:TRUE " + "-subj \"/C=SE/O=TEST CA\"", + [ + ECKeyFile, + ca_key_name(Path, Name), + ca_cert_name(Path, Name) + ] + ) + ), + os:cmd(Cmd). + +ca_cert_name(Path, Name) -> + filename(Path, "~s.pem", [Name]). +ca_key_name(Path, Name) -> + filename(Path, "~s.key", [Name]). + +eckey_name(Path) -> + filename(Path, "ec.key", []). + +gen_host_cert(H, CaName, Path) -> + gen_host_cert(H, CaName, Path, #{}). + +gen_host_cert(H, CaName, Path, Opts) -> + ECKeyFile = eckey_name(Path), + CN = str(H), + HKey = filename(Path, "~s.key", [H]), + HCSR = filename(Path, "~s.csr", [H]), + HCSR2 = filename(Path, "~s.csr", [H]), + HPEM = filename(Path, "~s.pem", [H]), + HPEM2 = filename(Path, "~s_renewed.pem", [H]), + HEXT = filename(Path, "~s.extfile", [H]), + PasswordArg = + case maps:get(password, Opts, undefined) of + undefined -> + " -nodes "; + Password -> + io_lib:format(" -passout pass:'~s' ", [Password]) + end, + + create_file( + HEXT, + "keyUsage=digitalSignature,keyAgreement,keyCertSign\n" + "basicConstraints=CA:TRUE \n" + "~s \n" + "subjectAltName=DNS:~s\n", + [maps:get(ext, Opts, ""), CN] + ), + + CSR_Cmd = csr_cmd(PasswordArg, ECKeyFile, HKey, HCSR, CN), + CSR_Cmd2 = csr_cmd(PasswordArg, ECKeyFile, HKey, HCSR2, CN), + + CERT_Cmd = cert_sign_cmd( + HEXT, HCSR, ca_cert_name(Path, CaName), ca_key_name(Path, CaName), HPEM + ), + %% 2nd cert for testing renewed cert. + CERT_Cmd2 = cert_sign_cmd( + HEXT, HCSR2, ca_cert_name(Path, CaName), ca_key_name(Path, CaName), HPEM2 + ), + ct:pal(os:cmd(CSR_Cmd)), + ct:pal(os:cmd(CSR_Cmd2)), + ct:pal(os:cmd(CERT_Cmd)), + ct:pal(os:cmd(CERT_Cmd2)), + file:delete(HEXT). + +cert_sign_cmd(ExtFile, CSRFile, CACert, CAKey, OutputCert) -> + lists:flatten( + io_lib:format( + "openssl x509 -req " + "-extfile ~s " + "-in ~s -CA ~s -CAkey ~s -CAcreateserial " + "-out ~s -days 500", + [ + ExtFile, + CSRFile, + CACert, + CAKey, + OutputCert + ] + ) + ). + +csr_cmd(PasswordArg, ECKeyFile, HKey, HCSR, CN) -> + lists:flatten( + io_lib:format( + "openssl req -new ~s -newkey ec:~s " + "-keyout ~s -out ~s " + "-addext \"subjectAltName=DNS:~s\" " + "-addext basicConstraints=CA:TRUE " + "-addext keyUsage=digitalSignature,keyAgreement,keyCertSign " + "-subj \"/C=SE/O=TEST/CN=~s\"", + [PasswordArg, ECKeyFile, HKey, HCSR, CN, CN] + ) + ). + +filename(Path, F, A) -> + filename:join(Path, str(io_lib:format(F, A))). + +str(Arg) -> + binary_to_list(iolist_to_binary(Arg)). + +create_file(Filename, Fmt, Args) -> + filelib:ensure_dir(Filename), + {ok, F} = file:open(Filename, [write]), + try + io:format(F, Fmt, Args) + after + file:close(F) + end, + ok. + +%% @doc get unused port from OS +-spec select_free_port(tcp | udp | ssl | quic) -> inets:port_number(). +select_free_port(tcp) -> + select_free_port(gen_tcp, listen); +select_free_port(udp) -> + select_free_port(gen_udp, open); +select_free_port(ssl) -> + select_free_port(tcp); +select_free_port(quic) -> + select_free_port(udp). + +select_free_port(GenModule, Fun) when + GenModule == gen_tcp orelse + GenModule == gen_udp +-> + {ok, S} = GenModule:Fun(0, [{reuseaddr, true}]), + {ok, Port} = inet:port(S), + ok = GenModule:close(S), + case os:type() of + {unix, darwin} -> + %% in MacOS, still get address_in_use after close port + timer:sleep(500); + _ -> + skip + end, + ct:pal("Select free OS port: ~p", [Port]), + Port. + +%% @doc fail the test if ssl_error recvd +%% post check for success conn establishment +fail_when_ssl_error(Socket) -> + fail_when_ssl_error(Socket, 1000). +fail_when_ssl_error(Socket, Timeout) -> + receive + {ssl_error, Socket, _} -> + ct:fail("Handshake failed!") + after Timeout -> + ok + end. + +%% @doc fail the test if no ssl_error 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), + + %% Build bundles below + os:cmd( + io_lib:format("cat ~p ~p ~p > ~p", [ + filename:join(DataDir, "client2.pem"), + filename:join(DataDir, "intermediate2.pem"), + filename:join(DataDir, "root.pem"), + filename:join(DataDir, "client2-complete-bundle.pem") + ]) + ), + os:cmd( + io_lib:format("cat ~p ~p > ~p", [ + filename:join(DataDir, "client2.pem"), + filename:join(DataDir, "intermediate2.pem"), + filename:join(DataDir, "client2-intermediate2-bundle.pem") + ]) + ), + os:cmd( + io_lib:format("cat ~p ~p > ~p", [ + filename:join(DataDir, "client2.pem"), + filename:join(DataDir, "root.pem"), + filename:join(DataDir, "client2-root-bundle.pem") + ]) + ), + os:cmd( + io_lib:format("cat ~p ~p > ~p", [ + filename:join(DataDir, "server1.pem"), + filename:join(DataDir, "intermediate1.pem"), + filename:join(DataDir, "server1-intermediate1-bundle.pem") + ]) + ), + os:cmd( + io_lib:format("cat ~p ~p > ~p", [ + filename:join(DataDir, "intermediate1.pem"), + filename:join(DataDir, "server1.pem"), + filename:join(DataDir, "intermediate1-server1-bundle.pem") + ]) + ), + os:cmd( + io_lib:format("cat ~p ~p > ~p", [ + filename:join(DataDir, "intermediate1_renewed.pem"), + filename:join(DataDir, "root.pem"), + filename:join(DataDir, "intermediate1_renewed-root-bundle.pem") + ]) + ), + os:cmd( + io_lib:format("cat ~p ~p > ~p", [ + filename:join(DataDir, "intermediate2.pem"), + filename:join(DataDir, "intermediate2_renewed.pem"), + filename:join(DataDir, "intermediate2_renewed_old-bundle.pem") + ]) + ), + os:cmd( + io_lib:format("cat ~p ~p > ~p", [ + filename:join(DataDir, "intermediate1.pem"), + filename:join(DataDir, "root.pem"), + filename:join(DataDir, "intermediate1-root-bundle.pem") + ]) + ), + os:cmd( + io_lib:format("cat ~p ~p ~p > ~p", [ + filename:join(DataDir, "root.pem"), + filename:join(DataDir, "intermediate2.pem"), + filename:join(DataDir, "intermediate1.pem"), + filename:join(DataDir, "all-CAcerts-bundle.pem") + ]) + ), + os:cmd( + io_lib:format("cat ~p ~p > ~p", [ + filename:join(DataDir, "intermediate2.pem"), + filename:join(DataDir, "intermediate1.pem"), + filename:join(DataDir, "two-intermediates-bundle.pem") + ]) + ). From 8bc3a86f63315e090384b0137ddc49be1df74735 Mon Sep 17 00:00:00 2001 From: William Yang Date: Thu, 5 Oct 2023 16:07:27 +0200 Subject: [PATCH 03/16] feat(config): partial_chain --- apps/emqx/src/emqx_schema.erl | 8 ++++++++ rel/i18n/emqx_schema.hocon | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 02e31387e..83d5dd2c1 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -2109,6 +2109,14 @@ common_ssl_opts_schema(Defaults, Type) -> desc => ?DESC(common_ssl_opts_schema_verify) } )}, + {"partial_chain", + sc( + hoconsc:enum([true, false, two_cacerts_from_cacertfile, cacert_from_cacertfile]), + #{ + default => Df(partial_chain, false), + desc => ?DESC(common_ssl_opts_schema_partial_chain) + } + )}, {"reuse_sessions", sc( boolean(), diff --git a/rel/i18n/emqx_schema.hocon b/rel/i18n/emqx_schema.hocon index cb504694c..225e88a35 100644 --- a/rel/i18n/emqx_schema.hocon +++ b/rel/i18n/emqx_schema.hocon @@ -678,6 +678,12 @@ common_ssl_opts_schema_verify.desc: common_ssl_opts_schema_verify.label: """Verify peer""" +common_ssl_opts_schema_partial_chain.desc: +"""Enable or disable peer verification with partial_chain""" + +common_ssl_opts_schema_partial_chain.label: +"""Partial chain""" + fields_listeners_ssl.desc: """SSL listeners.""" From eb1ab9adfe86dc917260055d1e6080a4f29021fa Mon Sep 17 00:00:00 2001 From: William Yang Date: Thu, 5 Oct 2023 16:20:24 +0200 Subject: [PATCH 04/16] test(tls): verify peer keyusage --- ...mqx_listener_tls_verify_keyusage_SUITE.erl | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/apps/emqx/test/emqx_listener_tls_verify_keyusage_SUITE.erl b/apps/emqx/test/emqx_listener_tls_verify_keyusage_SUITE.erl index c12618566..54ef07be0 100644 --- a/apps/emqx/test/emqx_listener_tls_verify_keyusage_SUITE.erl +++ b/apps/emqx/test/emqx_listener_tls_verify_keyusage_SUITE.erl @@ -18,17 +18,19 @@ -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, - gen_host_cert/4 -]). +-import( + emqx_test_tls_certs_helper, + [ + fail_when_ssl_error/1, + fail_when_no_ssl_alert/2, + generate_tls_certs/1, + gen_host_cert/4, + emqx_start_listener/4 + ] +). all() -> [ @@ -37,7 +39,7 @@ all() -> ]. all_tc() -> - emqx_ct:all(?MODULE). + emqx_common_test_helpers:all(?MODULE). groups() -> [ @@ -68,7 +70,7 @@ t_conn_success_verify_peer_ext_key_usage_unset(Config) -> DataDir = ?config(data_dir, Config), %% Given listener keyusage unset Options = [{ssl_options, ?config(ssl_config, Config)}], - emqx_listeners:start_listener(ssl, Port, Options), + emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), %% when client connect with cert without keyusage ext {ok, Socket} = ssl:connect( {127, 0, 0, 1}, @@ -93,7 +95,7 @@ t_conn_success_verify_peer_ext_key_usage_undefined(Config) -> | ?config(ssl_config, Config) ]} ], - emqx_listeners:start_listener(ssl, Port, Options), + emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), %% when client connect with cert without keyusages ext {ok, Socket} = ssl:connect( {127, 0, 0, 1}, @@ -121,7 +123,7 @@ t_conn_success_verify_peer_ext_key_usage_matched_predefined(Config) -> %% When client cert has clientAuth that is matched gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth"), - emqx_listeners:start_listener(ssl, Port, Options), + emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( {127, 0, 0, 1}, Port, @@ -147,7 +149,7 @@ t_conn_success_verify_peer_ext_key_usage_matched_raw_oid(Config) -> | ?config(ssl_config, Config) ]} ], - emqx_listeners:start_listener(ssl, Port, Options), + emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), %% When client cert has keyusage and matched. gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth"), {ok, Socket} = ssl:connect( @@ -174,7 +176,7 @@ t_conn_success_verify_peer_ext_key_usage_matched_ordered_list(Config) -> | ?config(ssl_config, Config) ]} ], - emqx_listeners:start_listener(ssl, Port, Options), + emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), %% When client cert has the same keyusage ext list gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth,serverAuth"), {ok, Socket} = ssl:connect( @@ -200,7 +202,7 @@ t_conn_success_verify_peer_ext_key_usage_matched_unordered_list(Config) -> | ?config(ssl_config, Config) ]} ], - emqx_listeners:start_listener(ssl, Port, Options), + emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), %% When client cert has the same keyusage ext list but different order gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth,serverAuth"), {ok, Socket} = ssl:connect( @@ -226,7 +228,7 @@ t_conn_fail_verify_peer_ext_key_usage_unmatched_raw_oid(Config) -> | ?config(ssl_config, Config) ]} ], - emqx_listeners:start_listener(ssl, Port, Options), + emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), %% When client cert has the keyusage but not matching OID gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth"), @@ -254,7 +256,7 @@ t_conn_fail_verify_peer_ext_key_usage_empty_str(Config) -> ]} ], %% Give listener keyusage is empty string - emqx_listeners:start_listener(ssl, Port, Options), + emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), %% When client connect with cert without keyusage {ok, Socket} = ssl:connect( {127, 0, 0, 1}, @@ -280,7 +282,7 @@ t_conn_fail_client_keyusage_unmatch(Config) -> | ?config(ssl_config, Config) ]} ], - emqx_listeners:start_listener(ssl, Port, Options), + emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), %% When client connect with mismatch cert keyusage = codeSigning gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "codeSigning"), {ok, Socket} = ssl:connect( @@ -307,7 +309,7 @@ t_conn_fail_client_keyusage_incomplete(Config) -> | ?config(ssl_config, Config) ]} ], - emqx_listeners:start_listener(ssl, Port, Options), + emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), %% When client connect with cert keyusage = clientAuth gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "codeSigning"), {ok, Socket} = ssl:connect( From 90430fa66dca7d828858aa06ca03ce44c0cd2629 Mon Sep 17 00:00:00 2001 From: William Yang Date: Thu, 5 Oct 2023 16:24:39 +0200 Subject: [PATCH 05/16] fix(tls): undefined keyusage --- apps/emqx/src/emqx_tls_lib.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx_tls_lib.erl b/apps/emqx/src/emqx_tls_lib.erl index 3c08487a4..d2a85264d 100644 --- a/apps/emqx/src/emqx_tls_lib.erl +++ b/apps/emqx/src/emqx_tls_lib.erl @@ -697,7 +697,7 @@ opt_partial_chain(SslOpts) -> %% @doc make verify_fun if set. -spec opt_verify_fun(SslOpts :: map()) -> NewSslOpts :: map(). -opt_verify_fun(#{verify_peer_ext_key_usage := V} = SslOpts) -> +opt_verify_fun(#{verify_peer_ext_key_usage := V} = SslOpts) when V =/= undefined -> SslOpts#{verify_fun => emqx_const_v2:make_tls_verify_fun(verify_cert_extKeyUsage, V)}; opt_verify_fun(SslOpts) -> SslOpts. From 8eb463c58d6f8548e3c4088b9c05cf187bb27d49 Mon Sep 17 00:00:00 2001 From: William Yang Date: Thu, 5 Oct 2023 16:25:09 +0200 Subject: [PATCH 06/16] feat(tls): update schema for TLS keyusage --- apps/emqx/src/emqx_schema.erl | 8 ++++++++ rel/i18n/emqx_schema.hocon | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 83d5dd2c1..46337c422 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -2117,6 +2117,14 @@ common_ssl_opts_schema(Defaults, Type) -> desc => ?DESC(common_ssl_opts_schema_partial_chain) } )}, + {"verify_peer_ext_key_usage", + sc( + string(), + #{ + required => false, + desc => ?DESC(common_ssl_opts_verify_peer_ext_key_usage) + } + )}, {"reuse_sessions", sc( boolean(), diff --git a/rel/i18n/emqx_schema.hocon b/rel/i18n/emqx_schema.hocon index 225e88a35..2df26b2d3 100644 --- a/rel/i18n/emqx_schema.hocon +++ b/rel/i18n/emqx_schema.hocon @@ -684,6 +684,12 @@ common_ssl_opts_schema_partial_chain.desc: common_ssl_opts_schema_partial_chain.label: """Partial chain""" +common_ssl_opts_verify_peer_ext_key_usage.desc: +"""Verify Extended Key Usage in Peer's certificate""" + +common_ssl_opts_verify_peer_ext_key_usage.label: +"""Verify KeyUsage in cert""" + fields_listeners_ssl.desc: """SSL listeners.""" From 4e9c1ec0c9ec22559866ae19a2a0e254addb2059 Mon Sep 17 00:00:00 2001 From: William Yang Date: Thu, 5 Oct 2023 17:02:27 +0200 Subject: [PATCH 07/16] chore: happy elvis --- apps/emqx/src/emqx_const_v2.erl | 1 + apps/emqx/src/emqx_tls_lib.erl | 1 + .../test/emqx_listener_tls_verify_partial_chain_SUITE.erl | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/emqx/src/emqx_const_v2.erl b/apps/emqx/src/emqx_const_v2.erl index 9fb2b7fa7..a4c321b4c 100644 --- a/apps/emqx/src/emqx_const_v2.erl +++ b/apps/emqx/src/emqx_const_v2.erl @@ -17,6 +17,7 @@ %%-------------------------------------------------------------------- -module(emqx_const_v2). +-elvis([{elvis_style, atom_naming_convention, #{regex => "^([a-z][a-z0-9A-Z]*_?)*(_SUITE)?$"}}]). -export([ make_tls_root_fun/2, diff --git a/apps/emqx/src/emqx_tls_lib.erl b/apps/emqx/src/emqx_tls_lib.erl index d2a85264d..6a623f6a8 100644 --- a/apps/emqx/src/emqx_tls_lib.erl +++ b/apps/emqx/src/emqx_tls_lib.erl @@ -15,6 +15,7 @@ %%-------------------------------------------------------------------- -module(emqx_tls_lib). +-elvis([{elvis_style, atom_naming_convention, #{regex => "^([a-z][a-z0-9A-Z]*_?)*(_SUITE)?$"}}]). %% version & cipher suites -export([ diff --git a/apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl b/apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl index 5ca00bc1d..fa270f5ce 100644 --- a/apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl +++ b/apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl @@ -300,8 +300,9 @@ t_conn_success_with_old_and_renewed_intermediate_cacert_bundle_and_client_using_ %% @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). +%% 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), From a29a43e5fc9e1d455893ed05468f13908c21af74 Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 6 Oct 2023 15:07:58 +0200 Subject: [PATCH 08/16] fix(listener): remove partial_chain in wss opts --- apps/emqx/src/emqx_listeners.erl | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 181841c65..6b5a946c5 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -636,8 +636,18 @@ ranch_opts(Type, Opts = #{bind := ListenOn}) -> MaxConnections = maps:get(max_connections, Opts, 1024), SocketOpts = case Type of - wss -> tcp_opts(Opts) ++ proplists:delete(handshake_timeout, ssl_opts(Opts)); - ws -> tcp_opts(Opts) + wss -> + tcp_opts(Opts) ++ + lists:filter( + fun + ({partial_chain, _}) -> false; + ({handshake_timeout, _}) -> false; + (_) -> true + end, + ssl_opts(Opts) + ); + ws -> + tcp_opts(Opts) end, #{ num_acceptors => NumAcceptors, From 43ad665dcfdd354bc9a717db00712a52890e7d6d Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 6 Oct 2023 15:32:18 +0200 Subject: [PATCH 09/16] fix(test): tls_verify_partial_chain --- ...istener_tls_verify_partial_chain_SUITE.erl | 347 ++++++++++-------- 1 file changed, 184 insertions(+), 163 deletions(-) diff --git a/apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl b/apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl index fa270f5ce..872bb9aaf 100644 --- a/apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl +++ b/apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl @@ -36,12 +36,6 @@ all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> generate_tls_certs(Config), application:ensure_all_started(esockd), - dbg:tracer(process, {fun dbg:dhandler/2, group_leader()}), - dbg:p(all, c), - dbg:tpl(emqx_listeners, esockd_opts, cx), - dbg:tpl(emqx_listeners, inject_root_fun, cx), - dbg:tpl(esockd, open, cx), - [{ssl_config, ssl_config_verify_partial_chain()} | Config]. end_per_suite(_Config) -> @@ -51,12 +45,13 @@ 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate1.pem")}, + {certfile, filename:join(DataDir, "server1.pem")}, + {keyfile, filename:join(DataDir, "server1.key")} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -75,12 +70,13 @@ 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "server1-intermediate1-bundle.pem")}, + {certfile, filename:join(DataDir, "server1.pem")}, + {keyfile, filename:join(DataDir, "server1.key")} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -99,12 +95,13 @@ 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate1_renewed.pem")}, + {certfile, filename:join(DataDir, "server1.pem")}, + {keyfile, filename:join(DataDir, "server1.key")} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -123,12 +120,13 @@ t_conn_fail_with_renewed_intermediate_cacert_and_client_using_old_complete_bundl 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate2_renewed.pem")}, + {certfile, filename:join(DataDir, "server2.pem")}, + {keyfile, filename:join(DataDir, "server2.key")} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -147,12 +145,13 @@ 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate2_renewed.pem")}, + {certfile, filename:join(DataDir, "server2.pem")}, + {keyfile, filename:join(DataDir, "server2.key")} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -173,13 +172,14 @@ t_conn_success_with_old_and_renewed_intermediate_cacert_and_client_provides_rene 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")}, + {certfile, filename:join(DataDir, "server2.pem")}, + {keyfile, filename:join(DataDir, "server2.key")}, + {partial_chain, two_cacerts_from_cacertfile} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -201,12 +201,13 @@ t_conn_success_with_new_intermediate_cacert_and_client_provides_renewed_client_c 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate2_renewed.pem")}, + {certfile, filename:join(DataDir, "server2.pem")}, + {keyfile, filename:join(DataDir, "server2.key")} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -226,13 +227,14 @@ t_conn_success_with_old_and_renewed_intermediate_cacert_and_client_provides_clie 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")}, + {certfile, filename:join(DataDir, "server2.pem")}, + {keyfile, filename:join(DataDir, "server2.key")}, + {partial_chain, two_cacerts_from_cacertfile} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -252,12 +254,13 @@ t_conn_fail_with_renewed_and_old_intermediate_cacert_and_client_using_old_bundle 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")}, + {certfile, filename:join(DataDir, "server2.pem")}, + {keyfile, filename:join(DataDir, "server2.key")} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -273,17 +276,20 @@ t_conn_fail_with_renewed_and_old_intermediate_cacert_and_client_using_old_bundle 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) -> +t_001_conn_success_with_old_and_renewed_intermediate_cacert_bundle_and_client_using_old_bundle( + Config +) -> Port = emqx_test_tls_certs_helper:select_free_port(ssl), DataDir = ?config(data_dir, Config), Options = [ - {ssl_options, [ - {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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")}, + {certfile, filename:join(DataDir, "server2.pem")}, + {keyfile, filename:join(DataDir, "server2.key")}, + {partial_chain, two_cacerts_from_cacertfile} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -304,17 +310,18 @@ t_conn_success_with_old_and_renewed_intermediate_cacert_bundle_and_client_using_ %% 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) -> +t_conn_fail_with_old_and_renewed_intermediate_cacert_bundle_and_client_using_all_CAcerts(Config) -> Port = emqx_test_tls_certs_helper:select_free_port(ssl), DataDir = ?config(data_dir, Config), Options = [ - {ssl_options, [ - {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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")}, + {certfile, filename:join(DataDir, "server2.pem")}, + {keyfile, filename:join(DataDir, "server2.key")}, + {partial_chain, two_cacerts_from_cacertfile} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -333,12 +340,13 @@ 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate1_renewed.pem")}, + {certfile, filename:join(DataDir, "server1.pem")}, + {keyfile, filename:join(DataDir, "server1.key")} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -357,12 +365,13 @@ 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate1-server1-bundle.pem")}, + {certfile, filename:join(DataDir, "server1.pem")}, + {keyfile, filename:join(DataDir, "server1.key")} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -381,12 +390,13 @@ 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate1.pem")}, + {certfile, filename:join(DataDir, "server1.pem")}, + {keyfile, filename:join(DataDir, "server1.key")} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -405,12 +415,13 @@ t_conn_success_with_complete_chain_that_server_root_cacert_and_client_complete_c 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "root.pem")}, + {certfile, filename:join(DataDir, "server2.pem")}, + {keyfile, filename:join(DataDir, "server2.key")} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -429,12 +440,13 @@ 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate1.pem")}, + {certfile, filename:join(DataDir, "server1.pem")}, + {keyfile, filename:join(DataDir, "server1.key")} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -453,12 +465,13 @@ 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate1-root-bundle.pem")}, + {certfile, filename:join(DataDir, "server1.pem")}, + {keyfile, filename:join(DataDir, "server1.key")} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -477,12 +490,13 @@ 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate2.pem")}, + {certfile, filename:join(DataDir, "server2.pem")}, + {keyfile, filename:join(DataDir, "server2.key")} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -501,12 +515,13 @@ t_conn_fail_with_server_intermediate_chain_and_client_other_incomplete_cert_chai 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate1.pem")}, + {certfile, filename:join(DataDir, "server1.pem")}, + {keyfile, filename:join(DataDir, "server1.key")} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -525,12 +540,13 @@ 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate1.pem")}, + {certfile, filename:join(DataDir, "server1.pem")}, + {keyfile, filename:join(DataDir, "server1.key")} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -549,12 +565,13 @@ 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate2.pem")}, + {certfile, filename:join(DataDir, "server2.pem")}, + {keyfile, filename:join(DataDir, "server2.key")} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -574,12 +591,13 @@ 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "all-CAcerts-bundle.pem")}, + {certfile, filename:join(DataDir, "server1.pem")}, + {keyfile, filename:join(DataDir, "server1.key")} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -598,12 +616,13 @@ 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) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "two-intermediates-bundle.pem")}, + {certfile, filename:join(DataDir, "server1.pem")}, + {keyfile, filename:join(DataDir, "server1.key")} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -622,13 +641,14 @@ t_conn_fail_with_server_partial_chain_false_intermediate_cacert_and_client_cert( 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")}, - {partial_chain, false} - | ?config(ssl_config, Config) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "intermediate1.pem")}, + {certfile, filename:join(DataDir, "server1.pem")}, + {keyfile, filename:join(DataDir, "server1.key")}, + {partial_chain, false} + ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), {ok, Socket} = ssl:connect( @@ -648,12 +668,13 @@ t_error_handling_invalid_cacertfile(Config) -> DataDir = ?config(data_dir, Config), %% trigger error Options = [ - {ssl_options, [ - {cacertfile, filename:join(DataDir, "server2.key")}, - {certfile, filename:join(DataDir, "server2.pem")}, - {keyfile, filename:join(DataDir, "server2.key")} - | ?config(ssl_config, Config) - ]} + {ssl_options, + ?config(ssl_config, Config) ++ + [ + {cacertfile, filename:join(DataDir, "server2.key")}, + {certfile, filename:join(DataDir, "server2.pem")}, + {keyfile, filename:join(DataDir, "server2.key")} + ]} ], ?assertException( throw, From 650cf4b27ea50bd93590bfdaa629c5a207bf8990 Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 11 Oct 2023 21:15:37 +0200 Subject: [PATCH 10/16] test(partial_chain): update tcs for OTP-25 --- .../emqx_listener_tls_verify_chain_SUITE.erl | 18 ++-- ...istener_tls_verify_partial_chain_SUITE.erl | 87 +++++++++++-------- apps/emqx/test/emqx_test_tls_certs_helper.erl | 13 ++- 3 files changed, 71 insertions(+), 47 deletions(-) diff --git a/apps/emqx/test/emqx_listener_tls_verify_chain_SUITE.erl b/apps/emqx/test/emqx_listener_tls_verify_chain_SUITE.erl index c38426523..a0d4ab9d1 100644 --- a/apps/emqx/test/emqx_listener_tls_verify_chain_SUITE.erl +++ b/apps/emqx/test/emqx_listener_tls_verify_chain_SUITE.erl @@ -205,17 +205,18 @@ t_conn_fail_with_server_partial_chain(Config) -> ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), - {ok, Socket} = ssl:connect( + Res = ssl:connect( {127, 0, 0, 1}, Port, [ {keyfile, filename:join(DataDir, "client2.key")}, - {certfile, filename:join(DataDir, "client2-complete-bundle.pem")} + {certfile, filename:join(DataDir, "client2-complete-bundle.pem")}, + {versions, ['tlsv1.2']}, + {verify, verify_none} ], 1000 ), - fail_when_no_ssl_alert(Socket, unknown_ca), - ok = ssl:close(Socket). + fail_when_no_ssl_alert(Res, unknown_ca). t_conn_fail_without_root_cacert(Config) -> Port = emqx_test_tls_certs_helper:select_free_port(ssl), @@ -229,17 +230,18 @@ t_conn_fail_without_root_cacert(Config) -> ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), - {ok, Socket} = ssl:connect( + Res = ssl:connect( {127, 0, 0, 1}, Port, [ {keyfile, filename:join(DataDir, "client2.key")}, - {certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")} + {certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")}, + %% stick to tlsv1.2 for consistent error message + {versions, ['tlsv1.2']} ], 1000 ), - fail_when_no_ssl_alert(Socket, unknown_ca), - ok = ssl:close(Socket). + fail_when_no_ssl_alert(Res, unknown_ca). ssl_config_verify_peer() -> [ diff --git a/apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl b/apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl index 872bb9aaf..7c5f471b9 100644 --- a/apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl +++ b/apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl @@ -60,6 +60,7 @@ t_conn_success_with_server_intermediate_cacert_and_client_cert(Config) -> [ {keyfile, filename:join(DataDir, "client1.key")}, {certfile, filename:join(DataDir, "client1.pem")} + | client_default_tls_opts() ], 1000 ), @@ -85,6 +86,7 @@ t_conn_success_with_intermediate_cacert_bundle(Config) -> [ {keyfile, filename:join(DataDir, "client1.key")}, {certfile, filename:join(DataDir, "client1.pem")} + | client_default_tls_opts() ], 1000 ), @@ -110,6 +112,7 @@ t_conn_success_with_renewed_intermediate_cacert(Config) -> [ {keyfile, filename:join(DataDir, "client1.key")}, {certfile, filename:join(DataDir, "client1.pem")} + | client_default_tls_opts() ], 1000 ), @@ -129,17 +132,17 @@ t_conn_fail_with_renewed_intermediate_cacert_and_client_using_old_complete_bundl ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), - {ok, Socket} = ssl:connect( + Res = ssl:connect( {127, 0, 0, 1}, Port, [ {keyfile, filename:join(DataDir, "client2.key")}, {certfile, filename:join(DataDir, "client2-complete-bundle.pem")} + | client_default_tls_opts() ], 1000 ), - fail_when_no_ssl_alert(Socket, unknown_ca), - ssl:close(Socket). + fail_when_no_ssl_alert(Res, unknown_ca). t_conn_fail_with_renewed_intermediate_cacert_and_client_using_old_bundle(Config) -> Port = emqx_test_tls_certs_helper:select_free_port(ssl), @@ -154,17 +157,17 @@ t_conn_fail_with_renewed_intermediate_cacert_and_client_using_old_bundle(Config) ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), - {ok, Socket} = ssl:connect( + Res = ssl:connect( {127, 0, 0, 1}, Port, [ {keyfile, filename:join(DataDir, "client2.key")}, {certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")} + | client_default_tls_opts() ], 1000 ), - fail_when_no_ssl_alert(Socket, unknown_ca), - ssl:close(Socket). + fail_when_no_ssl_alert(Res, unknown_ca). t_conn_success_with_old_and_renewed_intermediate_cacert_and_client_provides_renewed_client_cert( Config @@ -188,6 +191,7 @@ t_conn_success_with_old_and_renewed_intermediate_cacert_and_client_provides_rene [ {keyfile, filename:join(DataDir, "client2.key")}, {certfile, filename:join(DataDir, "client2_renewed.pem")} + | client_default_tls_opts() ], 1000 ), @@ -216,6 +220,7 @@ t_conn_success_with_new_intermediate_cacert_and_client_provides_renewed_client_c [ {keyfile, filename:join(DataDir, "client2.key")}, {certfile, filename:join(DataDir, "client2_renewed.pem")} + | client_default_tls_opts() ], 1000 ), @@ -243,6 +248,7 @@ t_conn_success_with_old_and_renewed_intermediate_cacert_and_client_provides_clie [ {keyfile, filename:join(DataDir, "client2.key")}, {certfile, filename:join(DataDir, "client2.pem")} + | client_default_tls_opts() ], 1000 ), @@ -263,17 +269,17 @@ t_conn_fail_with_renewed_and_old_intermediate_cacert_and_client_using_old_bundle ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), - {ok, Socket} = ssl:connect( + Res = ssl:connect( {127, 0, 0, 1}, Port, [ {keyfile, filename:join(DataDir, "client2.key")}, {certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")} + | client_default_tls_opts() ], 1000 ), - fail_when_no_ssl_alert(Socket, unknown_ca), - ssl:close(Socket). + fail_when_no_ssl_alert(Res, unknown_ca). %% @doc verify when config (two_cacerts_from_cacertfile) allows two versions of certs from same trusted CA. t_001_conn_success_with_old_and_renewed_intermediate_cacert_bundle_and_client_using_old_bundle( @@ -298,6 +304,7 @@ t_001_conn_success_with_old_and_renewed_intermediate_cacert_bundle_and_client_us [ {keyfile, filename:join(DataDir, "client2.key")}, {certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")} + | client_default_tls_opts() ], 1000 ), @@ -324,17 +331,17 @@ t_conn_fail_with_old_and_renewed_intermediate_cacert_bundle_and_client_using_all ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), - {ok, Socket} = ssl:connect( + Res = ssl:connect( {127, 0, 0, 1}, Port, [ {keyfile, filename:join(DataDir, "client1.key")}, {certfile, filename:join(DataDir, "all-CAcerts-bundle.pem")} + | client_default_tls_opts() ], 1000 ), - fail_when_no_ssl_alert(Socket, unknown_ca), - ssl:close(Socket). + fail_when_no_ssl_alert(Res, unknown_ca). t_conn_fail_with_renewed_intermediate_cacert_other_client(Config) -> Port = emqx_test_tls_certs_helper:select_free_port(ssl), @@ -349,17 +356,17 @@ t_conn_fail_with_renewed_intermediate_cacert_other_client(Config) -> ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), - {ok, Socket} = ssl:connect( + Res = ssl:connect( {127, 0, 0, 1}, Port, [ {keyfile, filename:join(DataDir, "client2.key")}, {certfile, filename:join(DataDir, "client2.pem")} + | client_default_tls_opts() ], 1000 ), - fail_when_no_ssl_alert(Socket, unknown_ca), - ssl:close(Socket). + fail_when_no_ssl_alert(Res, unknown_ca). t_conn_fail_with_intermediate_cacert_bundle_but_incorrect_order(Config) -> Port = emqx_test_tls_certs_helper:select_free_port(ssl), @@ -374,17 +381,17 @@ t_conn_fail_with_intermediate_cacert_bundle_but_incorrect_order(Config) -> ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), - {ok, Socket} = ssl:connect( + Res = ssl:connect( {127, 0, 0, 1}, Port, [ {keyfile, filename:join(DataDir, "client1.key")}, {certfile, filename:join(DataDir, "client1.pem")} + | client_default_tls_opts() ], 1000 ), - fail_when_no_ssl_alert(Socket, unknown_ca), - ssl:close(Socket). + fail_when_no_ssl_alert(Res, unknown_ca). t_conn_fail_when_singed_by_other_intermediate_ca(Config) -> Port = emqx_test_tls_certs_helper:select_free_port(ssl), @@ -399,17 +406,17 @@ t_conn_fail_when_singed_by_other_intermediate_ca(Config) -> ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), - {ok, Socket} = ssl:connect( + Res = ssl:connect( {127, 0, 0, 1}, Port, [ {keyfile, filename:join(DataDir, "client2.key")}, {certfile, filename:join(DataDir, "client2.pem")} + | client_default_tls_opts() ], 1000 ), - fail_when_no_ssl_alert(Socket, unknown_ca), - ok = ssl:close(Socket). + fail_when_no_ssl_alert(Res, unknown_ca). t_conn_success_with_complete_chain_that_server_root_cacert_and_client_complete_cert_chain(Config) -> Port = emqx_test_tls_certs_helper:select_free_port(ssl), @@ -430,6 +437,7 @@ t_conn_success_with_complete_chain_that_server_root_cacert_and_client_complete_c [ {keyfile, filename:join(DataDir, "client2.key")}, {certfile, filename:join(DataDir, "client2-complete-bundle.pem")} + | client_default_tls_opts() ], 1000 ), @@ -449,17 +457,17 @@ t_conn_fail_with_other_client_complete_cert_chain(Config) -> ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), - {ok, Socket} = ssl:connect( + Res = ssl:connect( {127, 0, 0, 1}, Port, [ {keyfile, filename:join(DataDir, "client2.key")}, {certfile, filename:join(DataDir, "client2-complete-bundle.pem")} + | client_default_tls_opts() ], 1000 ), - fail_when_no_ssl_alert(Socket, unknown_ca), - ok = ssl:close(Socket). + fail_when_no_ssl_alert(Res, unknown_ca). t_conn_fail_with_server_intermediate_and_other_client_complete_cert_chain(Config) -> Port = emqx_test_tls_certs_helper:select_free_port(ssl), @@ -480,6 +488,7 @@ t_conn_fail_with_server_intermediate_and_other_client_complete_cert_chain(Config [ {keyfile, filename:join(DataDir, "client2.key")}, {certfile, filename:join(DataDir, "client2-complete-bundle.pem")} + | client_default_tls_opts() ], 1000 ), @@ -505,6 +514,7 @@ t_conn_success_with_server_intermediate_cacert_and_client_complete_chain(Config) [ {keyfile, filename:join(DataDir, "client2.key")}, {certfile, filename:join(DataDir, "client2-complete-bundle.pem")} + | client_default_tls_opts() ], 1000 ), @@ -524,17 +534,17 @@ t_conn_fail_with_server_intermediate_chain_and_client_other_incomplete_cert_chai ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), - {ok, Socket} = ssl:connect( + Res = ssl:connect( {127, 0, 0, 1}, Port, [ {keyfile, filename:join(DataDir, "client2.key")}, {certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")} + | client_default_tls_opts() ], 1000 ), - fail_when_no_ssl_alert(Socket, unknown_ca), - ok = ssl:close(Socket). + fail_when_no_ssl_alert(Res, unknown_ca). t_conn_fail_with_server_intermediate_and_other_client_root_chain(Config) -> Port = emqx_test_tls_certs_helper:select_free_port(ssl), @@ -549,17 +559,17 @@ t_conn_fail_with_server_intermediate_and_other_client_root_chain(Config) -> ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), - {ok, Socket} = ssl:connect( + Res = ssl:connect( {127, 0, 0, 1}, Port, [ {keyfile, filename:join(DataDir, "client2.key")}, {certfile, filename:join(DataDir, "client2-root-bundle.pem")} + | client_default_tls_opts() ], 1000 ), - fail_when_no_ssl_alert(Socket, unknown_ca), - ok = ssl:close(Socket). + fail_when_no_ssl_alert(Res, unknown_ca). t_conn_success_with_server_intermediate_and_client_root_chain(Config) -> Port = emqx_test_tls_certs_helper:select_free_port(ssl), @@ -580,6 +590,7 @@ t_conn_success_with_server_intermediate_and_client_root_chain(Config) -> [ {keyfile, filename:join(DataDir, "client2.key")}, {certfile, filename:join(DataDir, "client2-root-bundle.pem")} + | client_default_tls_opts() ], 1000 ), @@ -606,6 +617,7 @@ t_conn_success_with_server_all_CA_bundle_and_client_root_chain(Config) -> [ {keyfile, filename:join(DataDir, "client2.key")}, {certfile, filename:join(DataDir, "client2-root-bundle.pem")} + | client_default_tls_opts() ], 1000 ), @@ -625,17 +637,17 @@ t_conn_fail_with_server_two_IA_bundle_and_client_root_chain(Config) -> ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), - {ok, Socket} = ssl:connect( + Res = ssl:connect( {127, 0, 0, 1}, Port, [ {keyfile, filename:join(DataDir, "client2.key")}, {certfile, filename:join(DataDir, "client2-root-bundle.pem")} + | client_default_tls_opts() ], 1000 ), - fail_when_no_ssl_alert(Socket, unknown_ca), - ok = ssl:close(Socket). + fail_when_no_ssl_alert(Res, unknown_ca). t_conn_fail_with_server_partial_chain_false_intermediate_cacert_and_client_cert(Config) -> Port = emqx_test_tls_certs_helper:select_free_port(ssl), @@ -651,17 +663,17 @@ t_conn_fail_with_server_partial_chain_false_intermediate_cacert_and_client_cert( ]} ], emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options), - {ok, Socket} = ssl:connect( + Res = ssl:connect( {127, 0, 0, 1}, Port, [ {keyfile, filename:join(DataDir, "client1.key")}, {certfile, filename:join(DataDir, "client1.pem")} + | client_default_tls_opts() ], 1000 ), - fail_when_no_ssl_alert(Socket, unknown_ca), - ssl:close(Socket). + fail_when_no_ssl_alert(Res, unknown_ca). t_error_handling_invalid_cacertfile(Config) -> Port = emqx_test_tls_certs_helper:select_free_port(ssl), @@ -688,3 +700,6 @@ ssl_config_verify_partial_chain() -> {fail_if_no_peer_cert, true}, {partial_chain, true} ]. + +client_default_tls_opts() -> + [{versions, ['tlsv1.2']}]. diff --git a/apps/emqx/test/emqx_test_tls_certs_helper.erl b/apps/emqx/test/emqx_test_tls_certs_helper.erl index 81babac19..880dc6bfd 100644 --- a/apps/emqx/test/emqx_test_tls_certs_helper.erl +++ b/apps/emqx/test/emqx_test_tls_certs_helper.erl @@ -212,9 +212,16 @@ fail_when_ssl_error(Socket, 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). +%% @doc fail the test if no ssl_error +fail_when_no_ssl_alert(Res, Alert) -> + fail_when_no_ssl_alert(Res, Alert, 1000). + +fail_when_no_ssl_alert({error, {tls_alert, {Alert, _}}}, Alert, _Timeout) -> + ok; +fail_when_no_ssl_alert({error, _} = Other, Alert, _Timeout) -> + ct:fail("returned unexpected ssl_error: ~p, expected ~n", [Other, Alert]); +fail_when_no_ssl_alert({ok, Socket}, Alert, Timeout) -> + fail_when_no_ssl_alert(Socket, Alert, Timeout); fail_when_no_ssl_alert(Socket, Alert, Timeout) -> receive {ssl_error, Socket, {tls_alert, {Alert, AlertInfo}}} -> From 03b093556472fe763531c655f35db990999c413d Mon Sep 17 00:00:00 2001 From: William Yang Date: Thu, 12 Oct 2023 10:20:02 +0200 Subject: [PATCH 11/16] chore: add changelog --- changes/ce/feat-11721.en.md | 5 +++++ changes/ce/feat-11721.zh.md | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 changes/ce/feat-11721.en.md create mode 100644 changes/ce/feat-11721.zh.md diff --git a/changes/ce/feat-11721.en.md b/changes/ce/feat-11721.en.md new file mode 100644 index 000000000..0dfa3245a --- /dev/null +++ b/changes/ce/feat-11721.en.md @@ -0,0 +1,5 @@ +Port two TLS handshake validation features from emqx 4.4 + +- partial_chain support +- Certificate KeyUsage Validation + diff --git a/changes/ce/feat-11721.zh.md b/changes/ce/feat-11721.zh.md new file mode 100644 index 000000000..e448f0953 --- /dev/null +++ b/changes/ce/feat-11721.zh.md @@ -0,0 +1,4 @@ + 移植 emqx 4.4 中的两项 TLS 握手验证功能 + +- 支持部分链 ( partial_chain ) +- 证书密钥使用验证 From 70ffd77f995e05c0eb6821f709f4769c392e614b Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 30 Apr 2024 09:01:52 +0200 Subject: [PATCH 12/16] chore(TLS-chain-test): update for OTP 26 --- apps/emqx/src/emqx_const_v2.erl | 2 +- .../emqx_listener_tls_verify_chain_SUITE.erl | 23 ++++++++----- ...mqx_listener_tls_verify_keyusage_SUITE.erl | 32 ++++++++++++------- ...istener_tls_verify_partial_chain_SUITE.erl | 7 ++-- apps/emqx/test/emqx_test_tls_certs_helper.erl | 3 +- changes/ce/feat-11721.zh.md | 4 --- 6 files changed, 44 insertions(+), 27 deletions(-) delete mode 100644 changes/ce/feat-11721.zh.md diff --git a/apps/emqx/src/emqx_const_v2.erl b/apps/emqx/src/emqx_const_v2.erl index a4c321b4c..a3b7980ff 100644 --- a/apps/emqx/src/emqx_const_v2.erl +++ b/apps/emqx/src/emqx_const_v2.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/apps/emqx/test/emqx_listener_tls_verify_chain_SUITE.erl b/apps/emqx/test/emqx_listener_tls_verify_chain_SUITE.erl index a0d4ab9d1..0b445c939 100644 --- a/apps/emqx/test/emqx_listener_tls_verify_chain_SUITE.erl +++ b/apps/emqx/test/emqx_listener_tls_verify_chain_SUITE.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -58,7 +58,8 @@ t_conn_fail_with_intermediate_ca_cert(Config) -> Port, [ {keyfile, filename:join(DataDir, "client1.key")}, - {certfile, filename:join(DataDir, "client1.pem")} + {certfile, filename:join(DataDir, "client1.pem")}, + {verify, verify_none} ], 1000 ), @@ -83,7 +84,8 @@ t_conn_fail_with_other_intermediate_ca_cert(Config) -> Port, [ {keyfile, filename:join(DataDir, "client2.key")}, - {certfile, filename:join(DataDir, "client2.pem")} + {certfile, filename:join(DataDir, "client2.pem")}, + {verify, verify_none} ], 1000 ), @@ -110,7 +112,8 @@ t_conn_success_with_server_client_composed_complete_chain(Config) -> Port, [ {keyfile, filename:join(DataDir, "client2.key")}, - {certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")} + {certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")}, + {verify, verify_none} ], 1000 ), @@ -136,7 +139,8 @@ t_conn_success_with_other_signed_client_composed_complete_chain(Config) -> Port, [ {keyfile, filename:join(DataDir, "client2.key")}, - {certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")} + {certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")}, + {verify, verify_none} ], 1000 ), @@ -161,7 +165,8 @@ t_conn_success_with_renewed_intermediate_root_bundle(Config) -> Port, [ {keyfile, filename:join(DataDir, "client1.key")}, - {certfile, filename:join(DataDir, "client1.pem")} + {certfile, filename:join(DataDir, "client1.pem")}, + {verify, verify_none} ], 1000 ), @@ -185,7 +190,8 @@ t_conn_success_with_client_complete_cert_chain(Config) -> Port, [ {keyfile, filename:join(DataDir, "client2.key")}, - {certfile, filename:join(DataDir, "client2-complete-bundle.pem")} + {certfile, filename:join(DataDir, "client2-complete-bundle.pem")}, + {verify, verify_none} ], 1000 ), @@ -237,7 +243,8 @@ t_conn_fail_without_root_cacert(Config) -> {keyfile, filename:join(DataDir, "client2.key")}, {certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")}, %% stick to tlsv1.2 for consistent error message - {versions, ['tlsv1.2']} + {versions, ['tlsv1.2']}, + {cacertfile, filename:join(DataDir, "intermediate2.pem")} ], 1000 ), diff --git a/apps/emqx/test/emqx_listener_tls_verify_keyusage_SUITE.erl b/apps/emqx/test/emqx_listener_tls_verify_keyusage_SUITE.erl index 54ef07be0..8265a7492 100644 --- a/apps/emqx/test/emqx_listener_tls_verify_keyusage_SUITE.erl +++ b/apps/emqx/test/emqx_listener_tls_verify_keyusage_SUITE.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -77,7 +77,8 @@ t_conn_success_verify_peer_ext_key_usage_unset(Config) -> Port, [ {keyfile, filename:join(DataDir, "client1.key")}, - {certfile, filename:join(DataDir, "client1.pem")} + {certfile, filename:join(DataDir, "client1.pem")}, + {verify, verify_none} ], 1000 ), @@ -102,7 +103,8 @@ t_conn_success_verify_peer_ext_key_usage_undefined(Config) -> Port, [ {keyfile, filename:join(DataDir, "client1.key")}, - {certfile, filename:join(DataDir, "client1.pem")} + {certfile, filename:join(DataDir, "client1.pem")}, + {verify, verify_none} ], 1000 ), @@ -129,7 +131,8 @@ t_conn_success_verify_peer_ext_key_usage_matched_predefined(Config) -> Port, [ {keyfile, client_key_file(DataDir, ?FUNCTION_NAME)}, - {certfile, client_pem_file(DataDir, ?FUNCTION_NAME)} + {certfile, client_pem_file(DataDir, ?FUNCTION_NAME)}, + {verify, verify_none} ], 1000 ), @@ -157,7 +160,8 @@ t_conn_success_verify_peer_ext_key_usage_matched_raw_oid(Config) -> Port, [ {keyfile, client_key_file(DataDir, ?FUNCTION_NAME)}, - {certfile, client_pem_file(DataDir, ?FUNCTION_NAME)} + {certfile, client_pem_file(DataDir, ?FUNCTION_NAME)}, + {verify, verify_none} ], 1000 ), @@ -184,7 +188,8 @@ t_conn_success_verify_peer_ext_key_usage_matched_ordered_list(Config) -> Port, [ {keyfile, client_key_file(DataDir, ?FUNCTION_NAME)}, - {certfile, client_pem_file(DataDir, ?FUNCTION_NAME)} + {certfile, client_pem_file(DataDir, ?FUNCTION_NAME)}, + {verify, verify_none} ], 1000 ), @@ -210,7 +215,8 @@ t_conn_success_verify_peer_ext_key_usage_matched_unordered_list(Config) -> Port, [ {keyfile, client_key_file(DataDir, ?FUNCTION_NAME)}, - {certfile, client_pem_file(DataDir, ?FUNCTION_NAME)} + {certfile, client_pem_file(DataDir, ?FUNCTION_NAME)}, + {verify, verify_none} ], 1000 ), @@ -237,7 +243,8 @@ t_conn_fail_verify_peer_ext_key_usage_unmatched_raw_oid(Config) -> Port, [ {keyfile, client_key_file(DataDir, ?FUNCTION_NAME)}, - {certfile, client_pem_file(DataDir, ?FUNCTION_NAME)} + {certfile, client_pem_file(DataDir, ?FUNCTION_NAME)}, + {verify, verify_none} ], 1000 ), @@ -263,7 +270,8 @@ t_conn_fail_verify_peer_ext_key_usage_empty_str(Config) -> Port, [ {keyfile, filename:join(DataDir, "client1.key")}, - {certfile, filename:join(DataDir, "client1.pem")} + {certfile, filename:join(DataDir, "client1.pem")}, + {verify, verify_none} ], 1000 ), @@ -290,7 +298,8 @@ t_conn_fail_client_keyusage_unmatch(Config) -> Port, [ {keyfile, client_key_file(DataDir, ?FUNCTION_NAME)}, - {certfile, client_pem_file(DataDir, ?FUNCTION_NAME)} + {certfile, client_pem_file(DataDir, ?FUNCTION_NAME)}, + {verify, verify_none} ], 1000 ), @@ -317,7 +326,8 @@ t_conn_fail_client_keyusage_incomplete(Config) -> Port, [ {keyfile, filename:join(DataDir, "client1.key")}, - {certfile, filename:join(DataDir, "client1.pem")} + {certfile, filename:join(DataDir, "client1.pem")}, + {verify, verify_none} ], 1000 ), diff --git a/apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl b/apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl index 7c5f471b9..1a1963dc9 100644 --- a/apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl +++ b/apps/emqx/test/emqx_listener_tls_verify_partial_chain_SUITE.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -702,4 +702,7 @@ ssl_config_verify_partial_chain() -> ]. client_default_tls_opts() -> - [{versions, ['tlsv1.2']}]. + [ + {versions, ['tlsv1.2']}, + {verify, verify_none} + ]. diff --git a/apps/emqx/test/emqx_test_tls_certs_helper.erl b/apps/emqx/test/emqx_test_tls_certs_helper.erl index 880dc6bfd..759b42821 100644 --- a/apps/emqx/test/emqx_test_tls_certs_helper.erl +++ b/apps/emqx/test/emqx_test_tls_certs_helper.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -40,6 +40,7 @@ emqx_start_listener(Name, Type, Port, Opts) when is_list(Opts) -> emqx_start_listener(Name, Type, Port, maps:from_list(Opts)); emqx_start_listener(Name, ssl, Port, #{ssl_options := SslOptions} = Opts0) -> Opts = Opts0#{ + enable => true, bind => {{127, 0, 0, 1}, Port}, mountpoint => <<>>, zone => default, diff --git a/changes/ce/feat-11721.zh.md b/changes/ce/feat-11721.zh.md deleted file mode 100644 index e448f0953..000000000 --- a/changes/ce/feat-11721.zh.md +++ /dev/null @@ -1,4 +0,0 @@ - 移植 emqx 4.4 中的两项 TLS 握手验证功能 - -- 支持部分链 ( partial_chain ) -- 证书密钥使用验证 From 3a674f44f12a5e2e3e6aa7c41609bc3e41e1815b Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 30 Apr 2024 10:27:02 +0200 Subject: [PATCH 13/16] chore: lock mimerl --- mix.exs | 3 ++- rebar.config | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index bbb11cc52..d70ac8b97 100644 --- a/mix.exs +++ b/mix.exs @@ -101,7 +101,8 @@ defmodule EMQXUmbrella.MixProject do {:bcrypt, github: "emqx/erlang-bcrypt", tag: "0.6.2", override: true}, {:uuid, github: "okeuday/uuid", tag: "v2.0.6", override: true}, {:quickrand, github: "okeuday/quickrand", tag: "v2.0.6", override: true}, - {:ra, "2.7.3", override: true} + {:ra, "2.7.3", override: true}, + {:mimerl, "1.2.0", override: true} ] ++ emqx_apps(profile_info, version) ++ enterprise_deps(profile_info) ++ jq_dep() ++ quicer_dep() diff --git a/rebar.config b/rebar.config index e0f88893c..fdb5e660d 100644 --- a/rebar.config +++ b/rebar.config @@ -111,7 +111,8 @@ {ssl_verify_fun, "1.1.7"}, {rfc3339, {git, "https://github.com/emqx/rfc3339.git", {tag, "0.2.3"}}}, {bcrypt, {git, "https://github.com/emqx/erlang-bcrypt.git", {tag, "0.6.2"}}}, - {ra, "2.7.3"} + {ra, "2.7.3"}, + {mimerl, "1.2.0"} ]}. {xref_ignores, From 337c230e79a6f53eba7dbec503940c12a478fc00 Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 30 Apr 2024 16:41:26 +0200 Subject: [PATCH 14/16] feat(partial_chain): gateway support --- apps/emqx_gateway/src/emqx_gateway_utils.erl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index 8fd9a1519..3150ec675 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -559,6 +559,8 @@ ssl_opts(Name, Opts) -> [ fun ssl_opts_crl_config/2, fun ssl_opts_drop_unsupported/2, + fun ssl_partial_chain/2, + fun ssl_verify_fun/2, fun ssl_server_opts/2 ], SSLOpts, @@ -586,6 +588,12 @@ ssl_server_opts(SSLOpts, ssl_options) -> ssl_server_opts(SSLOpts, dtls_options) -> emqx_tls_lib:to_server_opts(dtls, SSLOpts). +ssl_partial_chain(SSLOpts, _Options) -> + emqx_tls_lib:opt_partial_chain(SSLOpts). + +ssl_verify_fun(SSLOpts, _Options) -> + emqx_tls_lib:opt_verify_fun(SSLOpts). + ranch_opts(Type, ListenOn, Opts) -> NumAcceptors = maps:get(acceptors, Opts, 4), MaxConnections = maps:get(max_connections, Opts, 1024), From fb30207ef3ca1e2c86c813ffacc489f35a5dd958 Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 30 Apr 2024 16:41:46 +0200 Subject: [PATCH 15/16] chore: fix test --- apps/emqx_management/test/emqx_mgmt_api_configs_SUITE.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/emqx_management/test/emqx_mgmt_api_configs_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_configs_SUITE.erl index 2c90c9dac..d37740aa8 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_configs_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_configs_SUITE.erl @@ -466,6 +466,7 @@ t_create_webhook_v1_bridges_api(Config) -> <<"enable">> => true, <<"hibernate_after">> => <<"5s">>, <<"log_level">> => <<"notice">>, + <<"partial_chain">> => false, <<"reuse_sessions">> => true, <<"secure_renegotiate">> => true, <<"user_lookup_fun">> => From 1a4a4bb3a53c72c7d52688c5ed59086f5d7e6b8f Mon Sep 17 00:00:00 2001 From: William Yang Date: Thu, 2 May 2024 10:13:57 +0200 Subject: [PATCH 16/16] chore: fix nit --- apps/emqx/src/emqx_const_v2.erl | 16 +++++++--------- apps/emqx/test/emqx_test_tls_certs_helper.erl | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/apps/emqx/src/emqx_const_v2.erl b/apps/emqx/src/emqx_const_v2.erl index a3b7980ff..0d95cf43c 100644 --- a/apps/emqx/src/emqx_const_v2.erl +++ b/apps/emqx/src/emqx_const_v2.erl @@ -49,8 +49,8 @@ make_tls_root_fun(cacert_from_cacertfile, [TrustedOne, TrustedTwo]) -> end. make_tls_verify_fun(verify_cert_extKeyUsage, KeyUsages) -> - AllowedKeyUsages = ext_key_opts(KeyUsages), - {fun verify_fun_peer_extKeyUsage/3, AllowedKeyUsages}. + RequiredKeyUsages = ext_key_opts(KeyUsages), + {fun verify_fun_peer_extKeyUsage/3, RequiredKeyUsages}. verify_fun_peer_extKeyUsage(_, {bad_cert, invalid_ext_key_usage}, UserState) -> %% !! Override OTP verify peer default @@ -69,17 +69,17 @@ verify_fun_peer_extKeyUsage( #'OTPCertificate'{tbsCertificate = #'OTPTBSCertificate'{extensions = ExtL}}, %% valid peer cert valid_peer, - AllowedKeyUsages + RequiredKeyUsages ) -> %% override OTP verify_peer default %% must have id-ce-extKeyUsage case lists:keyfind(?'id-ce-extKeyUsage', 2, ExtL) of #'Extension'{extnID = ?'id-ce-extKeyUsage', extnValue = VL} -> - case do_verify_ext_key_usage(VL, AllowedKeyUsages) of + case do_verify_ext_key_usage(VL, RequiredKeyUsages) of true -> %% pass the check, %% fallback to OTP verify_peer default - {valid, AllowedKeyUsages}; + {valid, RequiredKeyUsages}; false -> {fail, extKeyUsage_unmatched} end; @@ -100,9 +100,7 @@ do_verify_ext_key_usage(CertExtL, [Usage | T] = _Required) -> end. %% @doc Helper tls cert extension --spec ext_key_opts - (string()) -> [OidString :: string() | public_key:oid()]; - (undefined) -> undefined. +-spec ext_key_opts(string()) -> [OidString :: string() | public_key:oid()]. ext_key_opts(Str) -> Usages = string:tokens(Str, ","), lists:map( @@ -119,7 +117,7 @@ ext_key_opts(Str) -> ?'id-kp-timeStamping'; ("ocspSigning") -> ?'id-kp-OCSPSigning'; - ([$O, $I, $D, $: | OidStr]) -> + ("OID:" ++ OidStr) -> OidList = string:tokens(OidStr, "."), list_to_tuple(lists:map(fun list_to_integer/1, OidList)) end, diff --git a/apps/emqx/test/emqx_test_tls_certs_helper.erl b/apps/emqx/test/emqx_test_tls_certs_helper.erl index 759b42821..78d51c5e0 100644 --- a/apps/emqx/test/emqx_test_tls_certs_helper.erl +++ b/apps/emqx/test/emqx_test_tls_certs_helper.erl @@ -46,7 +46,7 @@ emqx_start_listener(Name, ssl, Port, #{ssl_options := SslOptions} = Opts0) -> zone => default, ssl_options => maps:from_list(SslOptions) }, - ct:pal("start listsner with ~p ~p", [Name, Opts]), + ct:pal("start listener with ~p ~p", [Name, Opts]), emqx_listeners:start_listener(ssl, Name, Opts). %%-------------------------------------------------------------------------------