test: port listener tls partial_chain

This commit is contained in:
William Yang 2023-10-05 14:54:14 +02:00
parent 1ce13242a8
commit f7ff9496e6
4 changed files with 1587 additions and 0 deletions

View File

@ -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}
].

View File

@ -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}
].

View File

@ -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}
].

View File

@ -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")
])
).