feat(tls-partial-chain): add partial_chain support for TLS listeners
This commit is contained in:
parent
ee50359357
commit
a18018bee0
|
@ -1646,6 +1646,10 @@ end}.
|
||||||
{datatype, atom}
|
{datatype, atom}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
|
{mapping, "listener.ssl.$name.partial_chain", "emqx.listeners", [
|
||||||
|
{datatype, atom}
|
||||||
|
]}.
|
||||||
|
|
||||||
{mapping, "listener.ssl.$name.fail_if_no_peer_cert", "emqx.listeners", [
|
{mapping, "listener.ssl.$name.fail_if_no_peer_cert", "emqx.listeners", [
|
||||||
{datatype, {enum, [true, false]}}
|
{datatype, {enum, [true, false]}}
|
||||||
]}.
|
]}.
|
||||||
|
@ -2377,6 +2381,7 @@ end}.
|
||||||
{certfile, cuttlefish:conf_get(Prefix ++ ".certfile", Conf, undefined)},
|
{certfile, cuttlefish:conf_get(Prefix ++ ".certfile", Conf, undefined)},
|
||||||
{cacertfile, cuttlefish:conf_get(Prefix ++ ".cacertfile", Conf, undefined)},
|
{cacertfile, cuttlefish:conf_get(Prefix ++ ".cacertfile", Conf, undefined)},
|
||||||
{verify, cuttlefish:conf_get(Prefix ++ ".verify", Conf, undefined)},
|
{verify, cuttlefish:conf_get(Prefix ++ ".verify", Conf, undefined)},
|
||||||
|
{partial_chain, cuttlefish:conf_get(Prefix ++ ".partial_chain", Conf, undefined)},
|
||||||
{fail_if_no_peer_cert, cuttlefish:conf_get(Prefix ++ ".fail_if_no_peer_cert", Conf, undefined)},
|
{fail_if_no_peer_cert, cuttlefish:conf_get(Prefix ++ ".fail_if_no_peer_cert", Conf, undefined)},
|
||||||
{secure_renegotiate, cuttlefish:conf_get(Prefix ++ ".secure_renegotiate", Conf, undefined)},
|
{secure_renegotiate, cuttlefish:conf_get(Prefix ++ ".secure_renegotiate", Conf, undefined)},
|
||||||
{reuse_sessions, cuttlefish:conf_get(Prefix ++ ".reuse_sessions", Conf, undefined)},
|
{reuse_sessions, cuttlefish:conf_get(Prefix ++ ".reuse_sessions", Conf, undefined)},
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%
|
||||||
|
%% @doc Never update this module, create a v3 instead.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_const_v2).
|
||||||
|
|
||||||
|
-export([ make_tls_root_fun/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
make_tls_root_fun(cacert_from_cacertfile, CADer) ->
|
||||||
|
fun(InputChain) ->
|
||||||
|
case lists:member(CADer, InputChain) of
|
||||||
|
true -> {trusted_ca, CADer};
|
||||||
|
_ -> unknown_ca
|
||||||
|
end
|
||||||
|
end.
|
|
@ -138,7 +138,8 @@ start_listener(tcp, ListenOn, Options) ->
|
||||||
start_listener(Proto, ListenOn, Options0) when Proto == ssl; Proto == tls ->
|
start_listener(Proto, ListenOn, Options0) when Proto == ssl; Proto == tls ->
|
||||||
ListenerID = proplists:get_value(listener_id, Options0),
|
ListenerID = proplists:get_value(listener_id, Options0),
|
||||||
Options1 = proplists:delete(listener_id, Options0),
|
Options1 = proplists:delete(listener_id, Options0),
|
||||||
Options = emqx_ocsp_cache:inject_sni_fun(ListenerID, Options1),
|
Options2 = emqx_ocsp_cache:inject_sni_fun(ListenerID, Options1),
|
||||||
|
Options = emqx_tls_lib:inject_root_fun(Options2),
|
||||||
ok = maybe_register_crl_urls(Options),
|
ok = maybe_register_crl_urls(Options),
|
||||||
start_mqtt_listener('mqtt:ssl', ListenOn, Options);
|
start_mqtt_listener('mqtt:ssl', ListenOn, Options);
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,8 @@
|
||||||
, default_ciphers/1
|
, default_ciphers/1
|
||||||
, integral_ciphers/2
|
, integral_ciphers/2
|
||||||
, drop_tls13_for_old_otp/1
|
, drop_tls13_for_old_otp/1
|
||||||
|
, inject_root_fun/1
|
||||||
|
, opt_partial_chain/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
%% non-empty string
|
%% non-empty string
|
||||||
|
@ -170,8 +172,34 @@ drop_tls13(SslOpts0) ->
|
||||||
Ciphers -> replace(SslOpts1, ciphers, Ciphers -- ?TLSV13_EXCLUSIVE_CIPHERS)
|
Ciphers -> replace(SslOpts1, ciphers, Ciphers -- ?TLSV13_EXCLUSIVE_CIPHERS)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
inject_root_fun(Options) ->
|
||||||
|
case proplists:get_value(ssl_options, Options) of
|
||||||
|
undefined ->
|
||||||
|
Options;
|
||||||
|
SslOpts ->
|
||||||
|
replace(Options, ssl_options, opt_partial_chain(SslOpts))
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc enable TLS partial_chain validation if set.
|
||||||
|
-spec opt_partial_chain(SslOpts :: proplists:proplist()) -> NewSslOpts :: proplists:proplist().
|
||||||
|
opt_partial_chain(SslOpts) ->
|
||||||
|
case proplists:get_value(partial_chain, SslOpts, undefined) of
|
||||||
|
undefined ->
|
||||||
|
SslOpts;
|
||||||
|
cacert_from_cacertfile ->
|
||||||
|
replace(SslOpts, partial_chain, cacert_from_cacertfile(SslOpts))
|
||||||
|
end.
|
||||||
|
|
||||||
replace(Opts, Key, Value) -> [{Key, Value} | proplists:delete(Key, Opts)].
|
replace(Opts, Key, Value) -> [{Key, Value} | proplists:delete(Key, Opts)].
|
||||||
|
|
||||||
|
%% @doc Helper, make TLS root_fun
|
||||||
|
cacert_from_cacertfile(SslOpts) ->
|
||||||
|
Cacertfile = proplists:get_value(cacertfile, SslOpts, undefined),
|
||||||
|
{ok, PemBin} = file:read_file(Cacertfile),
|
||||||
|
%% The last one should be the top parent in the chain if it is a chain
|
||||||
|
{'Certificate', CADer, _} = lists:last(public_key:pem_decode(PemBin)),
|
||||||
|
emqx_const_v2:make_tls_root_fun(cacert_from_cacertfile, CADer).
|
||||||
|
|
||||||
-if(?OTP_RELEASE > 22).
|
-if(?OTP_RELEASE > 22).
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
@ -194,5 +222,5 @@ drop_tls13_no_versions_cipers_test() ->
|
||||||
has_tlsv13_cipher(Ciphers) ->
|
has_tlsv13_cipher(Ciphers) ->
|
||||||
lists:any(fun(C) -> lists:member(C, Ciphers) end, ?TLSV13_EXCLUSIVE_CIPHERS).
|
lists:any(fun(C) -> lists:member(C, Ciphers) end, ?TLSV13_EXCLUSIVE_CIPHERS).
|
||||||
|
|
||||||
-endif.
|
-endif. %% TEST
|
||||||
-endif.
|
-endif. %% OTP_RELEASE > 22
|
||||||
|
|
|
@ -0,0 +1,339 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
-module(emqx_listener_tls_verify_SUITE).
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
-import(emqx_test_tls_certs_helper, [ gen_ca/2
|
||||||
|
, gen_host_cert/3
|
||||||
|
]).
|
||||||
|
|
||||||
|
all() -> emqx_ct:all(?MODULE).
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
generate_tls_certs(Config),
|
||||||
|
application:ensure_all_started(esockd),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
application:stop(esockd).
|
||||||
|
|
||||||
|
t_tls_conn_success_when_partial_chain_enabled_with_intermediate_ca_cert(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [{ssl_options, [ {verify, verify_peer}
|
||||||
|
, {fail_if_no_peer_cert, true}
|
||||||
|
, {partial_chain, cacert_from_cacertfile}
|
||||||
|
, {cacertfile, filename:join(DataDir, "intermediate1.pem")}
|
||||||
|
, {certfile, filename:join(DataDir, "server1.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}],
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client1.pem")}
|
||||||
|
], 1000),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ssl:close(Socket).
|
||||||
|
|
||||||
|
t_tls_conn_success_when_partial_chain_enabled_with_intermediate_cacert_bundle(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [{ssl_options, [ {verify, verify_peer}
|
||||||
|
, {fail_if_no_peer_cert, true}
|
||||||
|
, {partial_chain, cacert_from_cacertfile}
|
||||||
|
, {cacertfile, filename:join(DataDir, "server1-intermediate1-bundle.pem")}
|
||||||
|
, {certfile, filename:join(DataDir, "server1.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}],
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client1.pem")}
|
||||||
|
], 1000),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ssl:close(Socket).
|
||||||
|
|
||||||
|
t_tls_conn_fail_when_partial_chain_enabled_with_intermediate_cacert_bundle2(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [{ssl_options, [ {verify, verify_peer}
|
||||||
|
, {fail_if_no_peer_cert, true}
|
||||||
|
, {partial_chain, cacert_from_cacertfile}
|
||||||
|
, {cacertfile, filename:join(DataDir, "intermediate1-server1-bundle.pem")}
|
||||||
|
, {certfile, filename:join(DataDir, "server1.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}],
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client1.pem")}
|
||||||
|
], 1000),
|
||||||
|
fail_when_no_ssl_alert(Socket, unknown_ca),
|
||||||
|
ssl:close(Socket).
|
||||||
|
|
||||||
|
t_tls_conn_fail_when_partial_chain_disabled_with_intermediate_ca_cert(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [{ssl_options, [ {verify, verify_peer}
|
||||||
|
, {fail_if_no_peer_cert, true}
|
||||||
|
, {cacertfile, filename:join(DataDir, "intermediate1.pem")}
|
||||||
|
, {certfile, filename:join(DataDir, "server1.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}],
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client1.pem")}
|
||||||
|
], 1000),
|
||||||
|
|
||||||
|
fail_when_no_ssl_alert(Socket, unknown_ca),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
|
||||||
|
t_tls_conn_fail_when_partial_chain_disabled_with_other_intermediate_ca_cert(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [{ssl_options, [ {verify, verify_peer}
|
||||||
|
, {fail_if_no_peer_cert, true}
|
||||||
|
, {cacertfile, filename:join(DataDir, "intermediate1.pem")}
|
||||||
|
, {certfile, filename:join(DataDir, "server1.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}],
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2.pem")}
|
||||||
|
], 1000),
|
||||||
|
|
||||||
|
fail_when_no_ssl_alert(Socket, unknown_ca),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_tls_conn_fail_when_partial_chain_enabled_with_other_intermediate_ca_cert(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [{ssl_options, [ {verify, verify_peer}
|
||||||
|
, {fail_if_no_peer_cert, true}
|
||||||
|
, {partial_chain, cacert_from_cacertfile}
|
||||||
|
, {cacertfile, filename:join(DataDir, "intermediate1.pem")}
|
||||||
|
, {certfile, filename:join(DataDir, "server1.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}],
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2.pem")}
|
||||||
|
], 1000),
|
||||||
|
fail_when_no_ssl_alert(Socket, unknown_ca),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_tls_conn_success_when_partial_chain_enabled_root_ca_with_complete_cert_chain(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [{ssl_options, [ {verify, verify_peer}
|
||||||
|
, {fail_if_no_peer_cert, true}
|
||||||
|
, {partial_chain, cacert_from_cacertfile}
|
||||||
|
, {cacertfile, filename:join(DataDir, "root.pem")}
|
||||||
|
, {certfile, filename:join(DataDir, "server2.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
]}],
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-complete-bundle.pem")}
|
||||||
|
], 1000),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_tls_conn_success_when_partial_chain_disabled_with_intermediate_ca_cert(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [{ssl_options, [ {verify, verify_peer}
|
||||||
|
, {fail_if_no_peer_cert, true}
|
||||||
|
, {cacertfile, filename:join(DataDir, "intermediate1.pem")}
|
||||||
|
, {certfile, filename:join(DataDir, "server1.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}],
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client1.pem")}
|
||||||
|
], 1000),
|
||||||
|
fail_when_no_ssl_alert(Socket, unknown_ca),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_tls_conn_success_when_partial_chain_disabled_with_broken_cert_chain_other_intermediate(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
%% Server has root ca cert
|
||||||
|
Options = [{ssl_options, [ {verify, verify_peer}
|
||||||
|
, {fail_if_no_peer_cert, true}
|
||||||
|
, {cacertfile, filename:join(DataDir, "root.pem")}
|
||||||
|
, {certfile, filename:join(DataDir, "server1.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}],
|
||||||
|
%% Client has complete chain
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")}
|
||||||
|
], 1000),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_tls_conn_fail_when_partial_chain_enabled_with_other_complete_cert_chain(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [{ssl_options, [ {verify, verify_peer}
|
||||||
|
, {fail_if_no_peer_cert, true}
|
||||||
|
, {partial_chain, cacert_from_cacertfile}
|
||||||
|
, {cacertfile, filename:join(DataDir, "intermediate1.pem")}
|
||||||
|
, {certfile, filename:join(DataDir, "server1.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}],
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-complete-bundle.pem")}
|
||||||
|
], 1000),
|
||||||
|
fail_when_no_ssl_alert(Socket, unknown_ca),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_tls_conn_success_when_partial_chain_enabled_with_complete_cert_chain(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [{ssl_options, [ {verify, verify_peer}
|
||||||
|
, {fail_if_no_peer_cert, true}
|
||||||
|
, {partial_chain, cacert_from_cacertfile}
|
||||||
|
, {cacertfile, filename:join(DataDir, "intermediate2.pem")}
|
||||||
|
, {certfile, filename:join(DataDir, "server2.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
]}],
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-complete-bundle.pem")}
|
||||||
|
], 1000),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_tls_conn_success_when_partial_chain_disabled_with_complete_cert_chain_client(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [{ssl_options, [ {verify, verify_peer}
|
||||||
|
, {fail_if_no_peer_cert, true}
|
||||||
|
, {cacertfile, filename:join(DataDir, "root.pem")}
|
||||||
|
, {certfile, filename:join(DataDir, "server2.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
]}],
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-complete-bundle.pem")}
|
||||||
|
], 1000),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
|
||||||
|
t_tls_conn_fail_when_partial_chain_disabled_with_incomplete_cert_chain_server(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [{ssl_options, [ {verify, verify_peer}
|
||||||
|
, {fail_if_no_peer_cert, true}
|
||||||
|
, {cacertfile, filename:join(DataDir, "intermediate2.pem")} %% imcomplete at server side
|
||||||
|
, {certfile, filename:join(DataDir, "server2.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
]}],
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-complete-bundle.pem")}
|
||||||
|
], 1000),
|
||||||
|
fail_when_no_ssl_alert(Socket, unknown_ca),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
|
||||||
|
t_tls_conn_fail_when_partial_chain_enabled_with_imcomplete_cert_chain(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [{ssl_options, [ {verify, verify_peer}
|
||||||
|
, {fail_if_no_peer_cert, true}
|
||||||
|
, {partial_chain, cacert_from_cacertfile}
|
||||||
|
, {cacertfile, filename:join(DataDir, "intermediate1.pem")}
|
||||||
|
, {certfile, filename:join(DataDir, "server1.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}],
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")}
|
||||||
|
], 1000),
|
||||||
|
fail_when_no_ssl_alert(Socket, unknown_ca),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_tls_conn_fail_when_partial_chain_disabled_with_incomplete_cert_chain(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [{ssl_options, [ {verify, verify_peer}
|
||||||
|
, {fail_if_no_peer_cert, true}
|
||||||
|
, {cacertfile, filename:join(DataDir, "intermediate1.pem")}
|
||||||
|
, {certfile, filename:join(DataDir, "server1.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}],
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")}
|
||||||
|
], 1000),
|
||||||
|
fail_when_no_ssl_alert(Socket, unknown_ca),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
generate_tls_certs(Config) ->
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
gen_ca(DataDir, "root"),
|
||||||
|
gen_host_cert("intermediate1", "root", DataDir),
|
||||||
|
gen_host_cert("intermediate2", "root", DataDir),
|
||||||
|
gen_host_cert("server1", "intermediate1", DataDir),
|
||||||
|
gen_host_cert("client1", "intermediate1", DataDir),
|
||||||
|
gen_host_cert("server2", "intermediate2", DataDir),
|
||||||
|
gen_host_cert("client2", "intermediate2", DataDir),
|
||||||
|
os:cmd(io_lib:format("cat ~p ~p ~p > ~p", [filename:join(DataDir, "client2.pem"),
|
||||||
|
filename:join(DataDir, "intermediate2.pem"),
|
||||||
|
filename:join(DataDir, "root.pem"),
|
||||||
|
filename:join(DataDir, "client2-complete-bundle.pem")
|
||||||
|
])),
|
||||||
|
os:cmd(io_lib:format("cat ~p ~p > ~p", [filename:join(DataDir, "client2.pem"),
|
||||||
|
filename:join(DataDir, "intermediate2.pem"),
|
||||||
|
filename:join(DataDir, "client2-intermediate2-bundle.pem")
|
||||||
|
])),
|
||||||
|
os:cmd(io_lib:format("cat ~p ~p > ~p", [filename:join(DataDir, "server1.pem"),
|
||||||
|
filename:join(DataDir, "intermediate1.pem"),
|
||||||
|
filename:join(DataDir, "server1-intermediate1-bundle.pem")
|
||||||
|
])),
|
||||||
|
os:cmd(io_lib:format("cat ~p ~p > ~p", [filename:join(DataDir, "intermediate1.pem"),
|
||||||
|
filename:join(DataDir, "server1.pem"),
|
||||||
|
filename:join(DataDir, "intermediate1-server1-bundle.pem")
|
||||||
|
])).
|
||||||
|
|
||||||
|
fail_when_ssl_error(Socket) ->
|
||||||
|
receive
|
||||||
|
{ssl_error, Socket, _} ->
|
||||||
|
ct:fail("Handshake failed!")
|
||||||
|
after 1000 ->
|
||||||
|
ok
|
||||||
|
end.
|
||||||
|
|
||||||
|
fail_when_no_ssl_alert(Socket, Alert) ->
|
||||||
|
receive
|
||||||
|
{ssl_error, Socket, {tls_alert, {Alert, AlertInfo}}} ->
|
||||||
|
ct:pal("alert info: ~p~n", [AlertInfo]);
|
||||||
|
{ssl_error, Socket, Other} ->
|
||||||
|
ct:fail("recv unexpected ssl_error: ~p~n", [Other])
|
||||||
|
after 1000 ->
|
||||||
|
ct:fail("No expected alert: ~p from Socket: ~p ", [Alert, Socket])
|
||||||
|
end.
|
|
@ -0,0 +1,156 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_test_tls_certs_helper).
|
||||||
|
-export([ gen_ca/2
|
||||||
|
, gen_host_cert/3
|
||||||
|
, gen_host_cert/4
|
||||||
|
|
||||||
|
, select_free_port/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
%%-------------------------------------------------------------------------------
|
||||||
|
%% TLS certs
|
||||||
|
%%-------------------------------------------------------------------------------
|
||||||
|
gen_ca(Path, Name) ->
|
||||||
|
%% Generate ca.pem and ca.key which will be used to generate certs
|
||||||
|
%% for hosts server and clients
|
||||||
|
ECKeyFile = eckey_name(Path),
|
||||||
|
filelib:ensure_dir(ECKeyFile),
|
||||||
|
os:cmd("openssl ecparam -name secp256r1 > " ++ ECKeyFile),
|
||||||
|
Cmd = lists:flatten(
|
||||||
|
io_lib:format(
|
||||||
|
"openssl req -new -x509 -nodes "
|
||||||
|
"-newkey ec:~s "
|
||||||
|
"-keyout ~s -out ~s -days 3650 "
|
||||||
|
"-addext basicConstraints=CA:TRUE "
|
||||||
|
"-subj \"/C=SE/O=Internet Widgits Pty Ltd CA\"",
|
||||||
|
[
|
||||||
|
ECKeyFile,
|
||||||
|
ca_key_name(Path, Name),
|
||||||
|
ca_cert_name(Path, Name)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
os:cmd(Cmd).
|
||||||
|
|
||||||
|
ca_cert_name(Path, Name) ->
|
||||||
|
filename(Path, "~s.pem", [Name]).
|
||||||
|
ca_key_name(Path, Name) ->
|
||||||
|
filename(Path, "~s.key", [Name]).
|
||||||
|
|
||||||
|
eckey_name(Path) ->
|
||||||
|
filename(Path, "ec.key", []).
|
||||||
|
|
||||||
|
gen_host_cert(H, CaName, Path) ->
|
||||||
|
gen_host_cert(H, CaName, Path, #{}).
|
||||||
|
|
||||||
|
gen_host_cert(H, CaName, Path, Opts) ->
|
||||||
|
ECKeyFile = eckey_name(Path),
|
||||||
|
CN = str(H),
|
||||||
|
HKey = filename(Path, "~s.key", [H]),
|
||||||
|
HCSR = filename(Path, "~s.csr", [H]),
|
||||||
|
HPEM = filename(Path, "~s.pem", [H]),
|
||||||
|
HEXT = filename(Path, "~s.extfile", [H]),
|
||||||
|
PasswordArg =
|
||||||
|
case maps:get(password, Opts, undefined) of
|
||||||
|
undefined ->
|
||||||
|
" -nodes ";
|
||||||
|
Password ->
|
||||||
|
io_lib:format(" -passout pass:'~s' ", [Password])
|
||||||
|
end,
|
||||||
|
CSR_Cmd =
|
||||||
|
lists:flatten(
|
||||||
|
io_lib:format(
|
||||||
|
"openssl req -new ~s -newkey ec:~s "
|
||||||
|
"-keyout ~s -out ~s "
|
||||||
|
"-addext \"subjectAltName=DNS:~s\" "
|
||||||
|
"-addext basicConstraints=CA:TRUE "
|
||||||
|
"-addext keyUsage=digitalSignature,keyAgreement,keyCertSign "
|
||||||
|
"-subj \"/C=SE/O=Internet Widgits Pty Ltd/CN=~s\"",
|
||||||
|
[PasswordArg, ECKeyFile, HKey, HCSR, CN, CN]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
create_file(
|
||||||
|
HEXT,
|
||||||
|
"keyUsage=digitalSignature,keyAgreement,keyCertSign\n"
|
||||||
|
"basicConstraints=CA:TRUE \n"
|
||||||
|
"subjectAltName=DNS:~s\n",
|
||||||
|
[CN]
|
||||||
|
),
|
||||||
|
CERT_Cmd =
|
||||||
|
lists:flatten(
|
||||||
|
io_lib:format(
|
||||||
|
"openssl x509 -req "
|
||||||
|
"-extfile ~s "
|
||||||
|
"-in ~s -CA ~s -CAkey ~s -CAcreateserial "
|
||||||
|
"-out ~s -days 500",
|
||||||
|
[
|
||||||
|
HEXT,
|
||||||
|
HCSR,
|
||||||
|
ca_cert_name(Path, CaName),
|
||||||
|
ca_key_name(Path, CaName),
|
||||||
|
HPEM
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ct:pal(os:cmd(CSR_Cmd)),
|
||||||
|
ct:pal(os:cmd(CERT_Cmd)),
|
||||||
|
file:delete(HEXT).
|
||||||
|
|
||||||
|
filename(Path, F, A) ->
|
||||||
|
filename:join(Path, str(io_lib:format(F, A))).
|
||||||
|
|
||||||
|
str(Arg) ->
|
||||||
|
binary_to_list(iolist_to_binary(Arg)).
|
||||||
|
|
||||||
|
create_file(Filename, Fmt, Args) ->
|
||||||
|
filelib:ensure_dir(Filename),
|
||||||
|
{ok, F} = file:open(Filename, [write]),
|
||||||
|
try
|
||||||
|
io:format(F, Fmt, Args)
|
||||||
|
after
|
||||||
|
file:close(F)
|
||||||
|
end,
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @doc get unused port from OS
|
||||||
|
-spec select_free_port(tcp | udp | ssl | quic) -> inets:port_number().
|
||||||
|
select_free_port(tcp) ->
|
||||||
|
select_free_port(gen_tcp, listen);
|
||||||
|
select_free_port(udp) ->
|
||||||
|
select_free_port(gen_udp, open);
|
||||||
|
select_free_port(ssl) ->
|
||||||
|
select_free_port(tcp);
|
||||||
|
select_free_port(quic) ->
|
||||||
|
select_free_port(udp).
|
||||||
|
|
||||||
|
select_free_port(GenModule, Fun) when
|
||||||
|
GenModule == gen_tcp orelse
|
||||||
|
GenModule == gen_udp
|
||||||
|
->
|
||||||
|
{ok, S} = GenModule:Fun(0, [{reuseaddr, true}]),
|
||||||
|
{ok, Port} = inet:port(S),
|
||||||
|
ok = GenModule:close(S),
|
||||||
|
case os:type() of
|
||||||
|
{unix, darwin} ->
|
||||||
|
%% in MacOS, still get address_in_use after close port
|
||||||
|
timer:sleep(500);
|
||||||
|
_ ->
|
||||||
|
skip
|
||||||
|
end,
|
||||||
|
ct:pal("Select free OS port: ~p", [Port]),
|
||||||
|
Port.
|
Loading…
Reference in New Issue