Merge pull request #12955 from qzhuyan/port/william/5/partial-chain
port: TLS partial chain
This commit is contained in:
commit
8f780ae8bc
|
@ -0,0 +1,125 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%
|
||||||
|
%% @doc Never update this module, create a v3 instead.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_const_v2).
|
||||||
|
-elvis([{elvis_style, atom_naming_convention, #{regex => "^([a-z][a-z0-9A-Z]*_?)*(_SUITE)?$"}}]).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
make_tls_root_fun/2,
|
||||||
|
make_tls_verify_fun/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-include_lib("public_key/include/public_key.hrl").
|
||||||
|
%% @doc Build a root fun for verify TLS partial_chain.
|
||||||
|
%% The `InputChain' is composed by OTP SSL with local cert store
|
||||||
|
%% AND the cert (chain if any) from the client.
|
||||||
|
%% @end
|
||||||
|
make_tls_root_fun(cacert_from_cacertfile, [Trusted]) ->
|
||||||
|
%% Allow only one trusted ca cert, and just return the defined trusted CA cert,
|
||||||
|
fun(_InputChain) ->
|
||||||
|
%% Note, returing `trusted_ca` doesn't really mean it accepts the connection
|
||||||
|
%% OTP SSL app will do the path validation, signature validation subsequently.
|
||||||
|
{trusted_ca, Trusted}
|
||||||
|
end;
|
||||||
|
make_tls_root_fun(cacert_from_cacertfile, [TrustedOne, TrustedTwo]) ->
|
||||||
|
%% Allow two trusted CA certs in case of CA cert renewal
|
||||||
|
%% This is a little expensive call as it compares the binaries.
|
||||||
|
fun(InputChain) ->
|
||||||
|
case lists:member(TrustedOne, InputChain) of
|
||||||
|
true ->
|
||||||
|
{trusted_ca, TrustedOne};
|
||||||
|
false ->
|
||||||
|
{trusted_ca, TrustedTwo}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
make_tls_verify_fun(verify_cert_extKeyUsage, KeyUsages) ->
|
||||||
|
RequiredKeyUsages = ext_key_opts(KeyUsages),
|
||||||
|
{fun verify_fun_peer_extKeyUsage/3, RequiredKeyUsages}.
|
||||||
|
|
||||||
|
verify_fun_peer_extKeyUsage(_, {bad_cert, invalid_ext_key_usage}, UserState) ->
|
||||||
|
%% !! Override OTP verify peer default
|
||||||
|
%% OTP SSL is unhappy with the ext_key_usage but we will check on our own.
|
||||||
|
{unknown, UserState};
|
||||||
|
verify_fun_peer_extKeyUsage(_, {bad_cert, _} = Reason, _UserState) ->
|
||||||
|
%% OTP verify_peer default
|
||||||
|
{fail, Reason};
|
||||||
|
verify_fun_peer_extKeyUsage(_, {extension, _}, UserState) ->
|
||||||
|
%% OTP verify_peer default
|
||||||
|
{unknown, UserState};
|
||||||
|
verify_fun_peer_extKeyUsage(_, valid, UserState) ->
|
||||||
|
%% OTP verify_peer default
|
||||||
|
{valid, UserState};
|
||||||
|
verify_fun_peer_extKeyUsage(
|
||||||
|
#'OTPCertificate'{tbsCertificate = #'OTPTBSCertificate'{extensions = ExtL}},
|
||||||
|
%% valid peer cert
|
||||||
|
valid_peer,
|
||||||
|
RequiredKeyUsages
|
||||||
|
) ->
|
||||||
|
%% override OTP verify_peer default
|
||||||
|
%% must have id-ce-extKeyUsage
|
||||||
|
case lists:keyfind(?'id-ce-extKeyUsage', 2, ExtL) of
|
||||||
|
#'Extension'{extnID = ?'id-ce-extKeyUsage', extnValue = VL} ->
|
||||||
|
case do_verify_ext_key_usage(VL, RequiredKeyUsages) of
|
||||||
|
true ->
|
||||||
|
%% pass the check,
|
||||||
|
%% fallback to OTP verify_peer default
|
||||||
|
{valid, RequiredKeyUsages};
|
||||||
|
false ->
|
||||||
|
{fail, extKeyUsage_unmatched}
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{fail, extKeyUsage_not_set}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc check required extkeyUsages are presented in the cert
|
||||||
|
do_verify_ext_key_usage(_, []) ->
|
||||||
|
%% Verify finished
|
||||||
|
true;
|
||||||
|
do_verify_ext_key_usage(CertExtL, [Usage | T] = _Required) ->
|
||||||
|
case lists:member(Usage, CertExtL) of
|
||||||
|
true ->
|
||||||
|
do_verify_ext_key_usage(CertExtL, T);
|
||||||
|
false ->
|
||||||
|
false
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Helper tls cert extension
|
||||||
|
-spec ext_key_opts(string()) -> [OidString :: string() | public_key:oid()].
|
||||||
|
ext_key_opts(Str) ->
|
||||||
|
Usages = string:tokens(Str, ","),
|
||||||
|
lists:map(
|
||||||
|
fun
|
||||||
|
("clientAuth") ->
|
||||||
|
?'id-kp-clientAuth';
|
||||||
|
("serverAuth") ->
|
||||||
|
?'id-kp-serverAuth';
|
||||||
|
("codeSigning") ->
|
||||||
|
?'id-kp-codeSigning';
|
||||||
|
("emailProtection") ->
|
||||||
|
?'id-kp-emailProtection';
|
||||||
|
("timeStamping") ->
|
||||||
|
?'id-kp-timeStamping';
|
||||||
|
("ocspSigning") ->
|
||||||
|
?'id-kp-OCSPSigning';
|
||||||
|
("OID:" ++ OidStr) ->
|
||||||
|
OidList = string:tokens(OidStr, "."),
|
||||||
|
list_to_tuple(lists:map(fun list_to_integer/1, OidList))
|
||||||
|
end,
|
||||||
|
Usages
|
||||||
|
).
|
|
@ -610,7 +610,9 @@ esockd_opts(ListenerId, Type, Name, Opts0) ->
|
||||||
ssl ->
|
ssl ->
|
||||||
OptsWithCRL = inject_crl_config(Opts0),
|
OptsWithCRL = inject_crl_config(Opts0),
|
||||||
OptsWithSNI = inject_sni_fun(ListenerId, OptsWithCRL),
|
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)}
|
Opts3#{ssl_options => SSLOpts, tcp_options => tcp_opts(Opts0)}
|
||||||
end
|
end
|
||||||
).
|
).
|
||||||
|
@ -634,8 +636,18 @@ ranch_opts(Type, Opts = #{bind := ListenOn}) ->
|
||||||
MaxConnections = maps:get(max_connections, Opts, 1024),
|
MaxConnections = maps:get(max_connections, Opts, 1024),
|
||||||
SocketOpts =
|
SocketOpts =
|
||||||
case Type of
|
case Type of
|
||||||
wss -> tcp_opts(Opts) ++ proplists:delete(handshake_timeout, ssl_opts(Opts));
|
wss ->
|
||||||
ws -> tcp_opts(Opts)
|
tcp_opts(Opts) ++
|
||||||
|
lists:filter(
|
||||||
|
fun
|
||||||
|
({partial_chain, _}) -> false;
|
||||||
|
({handshake_timeout, _}) -> false;
|
||||||
|
(_) -> true
|
||||||
|
end,
|
||||||
|
ssl_opts(Opts)
|
||||||
|
);
|
||||||
|
ws ->
|
||||||
|
tcp_opts(Opts)
|
||||||
end,
|
end,
|
||||||
#{
|
#{
|
||||||
num_acceptors => NumAcceptors,
|
num_acceptors => NumAcceptors,
|
||||||
|
@ -956,6 +968,16 @@ quic_listener_optional_settings() ->
|
||||||
stateless_operation_expiration_ms
|
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}}}) ->
|
inject_sni_fun(ListenerId, Conf = #{ssl_options := #{ocsp := #{enable_ocsp_stapling := true}}}) ->
|
||||||
emqx_ocsp_cache:inject_sni_fun(ListenerId, Conf);
|
emqx_ocsp_cache:inject_sni_fun(ListenerId, Conf);
|
||||||
inject_sni_fun(_ListenerId, Conf) ->
|
inject_sni_fun(_ListenerId, Conf) ->
|
||||||
|
|
|
@ -2118,6 +2118,22 @@ common_ssl_opts_schema(Defaults, Type) ->
|
||||||
desc => ?DESC(common_ssl_opts_schema_verify)
|
desc => ?DESC(common_ssl_opts_schema_verify)
|
||||||
}
|
}
|
||||||
)},
|
)},
|
||||||
|
{"partial_chain",
|
||||||
|
sc(
|
||||||
|
hoconsc:enum([true, false, two_cacerts_from_cacertfile, cacert_from_cacertfile]),
|
||||||
|
#{
|
||||||
|
default => Df(partial_chain, false),
|
||||||
|
desc => ?DESC(common_ssl_opts_schema_partial_chain)
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{"verify_peer_ext_key_usage",
|
||||||
|
sc(
|
||||||
|
string(),
|
||||||
|
#{
|
||||||
|
required => false,
|
||||||
|
desc => ?DESC(common_ssl_opts_verify_peer_ext_key_usage)
|
||||||
|
}
|
||||||
|
)},
|
||||||
{"reuse_sessions",
|
{"reuse_sessions",
|
||||||
sc(
|
sc(
|
||||||
boolean(),
|
boolean(),
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
-module(emqx_tls_lib).
|
-module(emqx_tls_lib).
|
||||||
|
-elvis([{elvis_style, atom_naming_convention, #{regex => "^([a-z][a-z0-9A-Z]*_?)*(_SUITE)?$"}}]).
|
||||||
|
|
||||||
%% version & cipher suites
|
%% version & cipher suites
|
||||||
-export([
|
-export([
|
||||||
|
@ -23,6 +24,8 @@
|
||||||
default_ciphers/0,
|
default_ciphers/0,
|
||||||
selected_ciphers/1,
|
selected_ciphers/1,
|
||||||
integral_ciphers/2,
|
integral_ciphers/2,
|
||||||
|
opt_partial_chain/1,
|
||||||
|
opt_verify_fun/1,
|
||||||
all_ciphers_set_cached/0
|
all_ciphers_set_cached/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
@ -679,3 +682,55 @@ ensure_ssl_file_key(SSL, RequiredKeyPaths) ->
|
||||||
[] -> ok;
|
[] -> ok;
|
||||||
Miss -> {error, #{reason => ssl_file_option_not_found, which_options => Miss}}
|
Miss -> {error, #{reason => ssl_file_option_not_found, which_options => Miss}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%% @doc enable TLS partial_chain validation if set.
|
||||||
|
-spec opt_partial_chain(SslOpts :: map()) -> NewSslOpts :: map().
|
||||||
|
opt_partial_chain(#{partial_chain := false} = SslOpts) ->
|
||||||
|
maps:remove(partial_chain, SslOpts);
|
||||||
|
opt_partial_chain(#{partial_chain := true} = SslOpts) ->
|
||||||
|
SslOpts#{partial_chain := rootfun_trusted_ca_from_cacertfile(1, SslOpts)};
|
||||||
|
opt_partial_chain(#{partial_chain := cacert_from_cacertfile} = SslOpts) ->
|
||||||
|
SslOpts#{partial_chain := rootfun_trusted_ca_from_cacertfile(1, SslOpts)};
|
||||||
|
opt_partial_chain(#{partial_chain := two_cacerts_from_cacertfile} = SslOpts) ->
|
||||||
|
SslOpts#{partial_chain := rootfun_trusted_ca_from_cacertfile(2, SslOpts)};
|
||||||
|
opt_partial_chain(SslOpts) ->
|
||||||
|
SslOpts.
|
||||||
|
|
||||||
|
%% @doc make verify_fun if set.
|
||||||
|
-spec opt_verify_fun(SslOpts :: map()) -> NewSslOpts :: map().
|
||||||
|
opt_verify_fun(#{verify_peer_ext_key_usage := V} = SslOpts) when V =/= undefined ->
|
||||||
|
SslOpts#{verify_fun => emqx_const_v2:make_tls_verify_fun(verify_cert_extKeyUsage, V)};
|
||||||
|
opt_verify_fun(SslOpts) ->
|
||||||
|
SslOpts.
|
||||||
|
|
||||||
|
%% @doc Helper, make TLS root_fun
|
||||||
|
rootfun_trusted_ca_from_cacertfile(NumOfCerts, #{cacertfile := Cacertfile}) ->
|
||||||
|
case file:read_file(Cacertfile) of
|
||||||
|
{ok, PemBin} ->
|
||||||
|
try
|
||||||
|
do_rootfun_trusted_ca_from_cacertfile(NumOfCerts, PemBin)
|
||||||
|
catch
|
||||||
|
_Error:_Info:ST ->
|
||||||
|
%% The cacertfile will be checked by OTP SSL as well and OTP choice to be silent on this.
|
||||||
|
%% We are touching security sutffs, don't leak extra info..
|
||||||
|
?SLOG(error, #{
|
||||||
|
msg => "trusted_cacert_not_found_in_cacertfile", stacktrace => ST
|
||||||
|
}),
|
||||||
|
throw({error, ?FUNCTION_NAME})
|
||||||
|
end;
|
||||||
|
{error, Reason} ->
|
||||||
|
throw({error, {read_cacertfile_error, Cacertfile, Reason}})
|
||||||
|
end;
|
||||||
|
rootfun_trusted_ca_from_cacertfile(_NumOfCerts, _SslOpts) ->
|
||||||
|
throw({error, cacertfile_unset}).
|
||||||
|
|
||||||
|
do_rootfun_trusted_ca_from_cacertfile(NumOfCerts, PemBin) ->
|
||||||
|
%% The last one or two should be the top parent in the chain if it is a chain
|
||||||
|
Certs = public_key:pem_decode(PemBin),
|
||||||
|
Pos = length(Certs) - NumOfCerts + 1,
|
||||||
|
Trusted = [
|
||||||
|
CADer
|
||||||
|
|| {'Certificate', CADer, _} <-
|
||||||
|
lists:sublist(public_key:pem_decode(PemBin), Pos, NumOfCerts)
|
||||||
|
],
|
||||||
|
emqx_const_v2:make_tls_root_fun(cacert_from_cacertfile, Trusted).
|
||||||
|
|
|
@ -0,0 +1,257 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
-module(emqx_listener_tls_verify_chain_SUITE).
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
-import(
|
||||||
|
emqx_test_tls_certs_helper,
|
||||||
|
[
|
||||||
|
emqx_start_listener/4,
|
||||||
|
fail_when_ssl_error/1,
|
||||||
|
fail_when_no_ssl_alert/2,
|
||||||
|
generate_tls_certs/1
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
||||||
|
all() -> emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
generate_tls_certs(Config),
|
||||||
|
application:ensure_all_started(esockd),
|
||||||
|
[{ssl_config, ssl_config_verify_peer()} | Config].
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
application:stop(esockd).
|
||||||
|
|
||||||
|
t_conn_fail_with_intermediate_ca_cert(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options, [
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate1.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server1.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client1.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client1.pem")},
|
||||||
|
{verify, verify_none}
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
|
||||||
|
fail_when_no_ssl_alert(Socket, unknown_ca),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_fail_with_other_intermediate_ca_cert(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options, [
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate1.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server1.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2.pem")},
|
||||||
|
{verify, verify_none}
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
|
||||||
|
fail_when_no_ssl_alert(Socket, unknown_ca),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_success_with_server_client_composed_complete_chain(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
%% Server has root ca cert
|
||||||
|
Options = [
|
||||||
|
{ssl_options, [
|
||||||
|
{cacertfile, filename:join(DataDir, "root.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server2.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
%% Client has complete chain
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")},
|
||||||
|
{verify, verify_none}
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_success_with_other_signed_client_composed_complete_chain(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
%% Server has root ca cert
|
||||||
|
Options = [
|
||||||
|
{ssl_options, [
|
||||||
|
{cacertfile, filename:join(DataDir, "root.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server1.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
%% Client has partial_chain
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")},
|
||||||
|
{verify, verify_none}
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_success_with_renewed_intermediate_root_bundle(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
%% Server has root ca cert
|
||||||
|
Options = [
|
||||||
|
{ssl_options, [
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate1_renewed-root-bundle.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server1.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client1.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client1.pem")},
|
||||||
|
{verify, verify_none}
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_success_with_client_complete_cert_chain(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options, [
|
||||||
|
{cacertfile, filename:join(DataDir, "root.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server2.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-complete-bundle.pem")},
|
||||||
|
{verify, verify_none}
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_fail_with_server_partial_chain(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
%% imcomplete at server side
|
||||||
|
Options = [
|
||||||
|
{ssl_options, [
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate2.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server2.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
Res = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-complete-bundle.pem")},
|
||||||
|
{versions, ['tlsv1.2']},
|
||||||
|
{verify, verify_none}
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_no_ssl_alert(Res, unknown_ca).
|
||||||
|
|
||||||
|
t_conn_fail_without_root_cacert(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options, [
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate2.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server2.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
Res = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")},
|
||||||
|
%% stick to tlsv1.2 for consistent error message
|
||||||
|
{versions, ['tlsv1.2']},
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate2.pem")}
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_no_ssl_alert(Res, unknown_ca).
|
||||||
|
|
||||||
|
ssl_config_verify_peer() ->
|
||||||
|
[
|
||||||
|
{verify, verify_peer},
|
||||||
|
{fail_if_no_peer_cert, true}
|
||||||
|
].
|
|
@ -0,0 +1,372 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
-module(emqx_listener_tls_verify_keyusage_SUITE).
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
-import(
|
||||||
|
emqx_test_tls_certs_helper,
|
||||||
|
[
|
||||||
|
fail_when_ssl_error/1,
|
||||||
|
fail_when_no_ssl_alert/2,
|
||||||
|
generate_tls_certs/1,
|
||||||
|
gen_host_cert/4,
|
||||||
|
emqx_start_listener/4
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
[
|
||||||
|
{group, full_chain},
|
||||||
|
{group, partial_chain}
|
||||||
|
].
|
||||||
|
|
||||||
|
all_tc() ->
|
||||||
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
|
groups() ->
|
||||||
|
[
|
||||||
|
{partial_chain, [], all_tc()},
|
||||||
|
{full_chain, [], all_tc()}
|
||||||
|
].
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
generate_tls_certs(Config),
|
||||||
|
application:ensure_all_started(esockd),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
application:stop(esockd).
|
||||||
|
|
||||||
|
init_per_group(full_chain, Config) ->
|
||||||
|
[{ssl_config, ssl_config_verify_peer_full_chain(Config)} | Config];
|
||||||
|
init_per_group(partial_chain, Config) ->
|
||||||
|
[{ssl_config, ssl_config_verify_peer_partial_chain(Config)} | Config];
|
||||||
|
init_per_group(_, Config) ->
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_group(_, Config) ->
|
||||||
|
Config.
|
||||||
|
|
||||||
|
t_conn_success_verify_peer_ext_key_usage_unset(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
%% Given listener keyusage unset
|
||||||
|
Options = [{ssl_options, ?config(ssl_config, Config)}],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
%% when client connect with cert without keyusage ext
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client1.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client1.pem")},
|
||||||
|
{verify, verify_none}
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
%% Then connection success
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_success_verify_peer_ext_key_usage_undefined(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
%% Give listener keyusage is set to undefined
|
||||||
|
Options = [
|
||||||
|
{ssl_options, [
|
||||||
|
{verify_peer_ext_key_usage, undefined}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
%% when client connect with cert without keyusages ext
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client1.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client1.pem")},
|
||||||
|
{verify, verify_none}
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
%% Then connection success
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_success_verify_peer_ext_key_usage_matched_predefined(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
%% Give listener keyusage is set to clientAuth
|
||||||
|
Options = [
|
||||||
|
{ssl_options, [
|
||||||
|
{verify_peer_ext_key_usage, "clientAuth"}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
|
||||||
|
%% When client cert has clientAuth that is matched
|
||||||
|
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth"),
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, client_key_file(DataDir, ?FUNCTION_NAME)},
|
||||||
|
{certfile, client_pem_file(DataDir, ?FUNCTION_NAME)},
|
||||||
|
{verify, verify_none}
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
%% Then connection success
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_success_verify_peer_ext_key_usage_matched_raw_oid(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
%% Give listener keyusage is set to raw OID
|
||||||
|
|
||||||
|
%% from OTP-PUB-KEY.hrl
|
||||||
|
Options = [
|
||||||
|
{ssl_options, [
|
||||||
|
{verify_peer_ext_key_usage, "OID:1.3.6.1.5.5.7.3.2"}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
%% When client cert has keyusage and matched.
|
||||||
|
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth"),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, client_key_file(DataDir, ?FUNCTION_NAME)},
|
||||||
|
{certfile, client_pem_file(DataDir, ?FUNCTION_NAME)},
|
||||||
|
{verify, verify_none}
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
%% Then connection success
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_success_verify_peer_ext_key_usage_matched_ordered_list(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
|
||||||
|
%% Give listener keyusage is clientAuth,serverAuth
|
||||||
|
Options = [
|
||||||
|
{ssl_options, [
|
||||||
|
{verify_peer_ext_key_usage, "clientAuth,serverAuth"}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
%% When client cert has the same keyusage ext list
|
||||||
|
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth,serverAuth"),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, client_key_file(DataDir, ?FUNCTION_NAME)},
|
||||||
|
{certfile, client_pem_file(DataDir, ?FUNCTION_NAME)},
|
||||||
|
{verify, verify_none}
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
%% Then connection success
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_success_verify_peer_ext_key_usage_matched_unordered_list(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
%% Give listener keyusage is clientAuth,serverAuth
|
||||||
|
Options = [
|
||||||
|
{ssl_options, [
|
||||||
|
{verify_peer_ext_key_usage, "serverAuth,clientAuth"}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
%% When client cert has the same keyusage ext list but different order
|
||||||
|
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth,serverAuth"),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, client_key_file(DataDir, ?FUNCTION_NAME)},
|
||||||
|
{certfile, client_pem_file(DataDir, ?FUNCTION_NAME)},
|
||||||
|
{verify, verify_none}
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
%% Then connection success
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_fail_verify_peer_ext_key_usage_unmatched_raw_oid(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
%% Give listener keyusage is using OID
|
||||||
|
Options = [
|
||||||
|
{ssl_options, [
|
||||||
|
{verify_peer_ext_key_usage, "OID:1.3.6.1.5.5.7.3.1"}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
|
||||||
|
%% When client cert has the keyusage but not matching OID
|
||||||
|
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth"),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, client_key_file(DataDir, ?FUNCTION_NAME)},
|
||||||
|
{certfile, client_pem_file(DataDir, ?FUNCTION_NAME)},
|
||||||
|
{verify, verify_none}
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
|
||||||
|
%% Then connecion should fail.
|
||||||
|
fail_when_no_ssl_alert(Socket, handshake_failure),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_fail_verify_peer_ext_key_usage_empty_str(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options, [
|
||||||
|
{verify_peer_ext_key_usage, ""}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
%% Give listener keyusage is empty string
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
%% When client connect with cert without keyusage
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client1.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client1.pem")},
|
||||||
|
{verify, verify_none}
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
%% Then connecion should fail.
|
||||||
|
fail_when_no_ssl_alert(Socket, handshake_failure),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_fail_client_keyusage_unmatch(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
|
||||||
|
%% Give listener keyusage is clientAuth
|
||||||
|
Options = [
|
||||||
|
{ssl_options, [
|
||||||
|
{verify_peer_ext_key_usage, "clientAuth"}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
%% When client connect with mismatch cert keyusage = codeSigning
|
||||||
|
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "codeSigning"),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, client_key_file(DataDir, ?FUNCTION_NAME)},
|
||||||
|
{certfile, client_pem_file(DataDir, ?FUNCTION_NAME)},
|
||||||
|
{verify, verify_none}
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
%% Then connecion should fail.
|
||||||
|
fail_when_no_ssl_alert(Socket, handshake_failure),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_fail_client_keyusage_incomplete(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
%% Give listener keyusage is codeSigning,clientAuth
|
||||||
|
Options = [
|
||||||
|
{ssl_options, [
|
||||||
|
{verify_peer_ext_key_usage,
|
||||||
|
"serverAuth,clientAuth,codeSigning,emailProtection,timeStamping,ocspSigning"}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
%% When client connect with cert keyusage = clientAuth
|
||||||
|
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "codeSigning"),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client1.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client1.pem")},
|
||||||
|
{verify, verify_none}
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
%% Then connection should fail
|
||||||
|
fail_when_no_ssl_alert(Socket, handshake_failure),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
%%%
|
||||||
|
%%% Helpers
|
||||||
|
%%%
|
||||||
|
gen_client_cert_ext_keyusage(Name, CA, DataDir, Usage) when is_atom(Name) ->
|
||||||
|
gen_client_cert_ext_keyusage(atom_to_list(Name), CA, DataDir, Usage);
|
||||||
|
gen_client_cert_ext_keyusage(Name, CA, DataDir, Usage) ->
|
||||||
|
gen_host_cert(Name, CA, DataDir, #{ext => "extendedKeyUsage=" ++ Usage}).
|
||||||
|
|
||||||
|
client_key_file(DataDir, Name) ->
|
||||||
|
filename:join(DataDir, Name) ++ ".key".
|
||||||
|
|
||||||
|
client_pem_file(DataDir, Name) ->
|
||||||
|
filename:join(DataDir, Name) ++ ".pem".
|
||||||
|
|
||||||
|
ssl_config_verify_peer_full_chain(Config) ->
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(?config(data_dir, Config), "intermediate1-root-bundle.pem")}
|
||||||
|
| ssl_config_verify_peer(Config)
|
||||||
|
].
|
||||||
|
ssl_config_verify_peer_partial_chain(Config) ->
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(?config(data_dir, Config), "intermediate1.pem")},
|
||||||
|
{partial_chain, true}
|
||||||
|
| ssl_config_verify_peer(Config)
|
||||||
|
].
|
||||||
|
|
||||||
|
ssl_config_verify_peer(Config) ->
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
[
|
||||||
|
{verify, verify_peer},
|
||||||
|
{fail_if_no_peer_cert, true},
|
||||||
|
{keyfile, filename:join(DataDir, "server1.key")},
|
||||||
|
{certfile, filename:join(DataDir, "server1.pem")}
|
||||||
|
%% , {log_level, debug}
|
||||||
|
].
|
|
@ -0,0 +1,708 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
-module(emqx_listener_tls_verify_partial_chain_SUITE).
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
-import(
|
||||||
|
emqx_test_tls_certs_helper,
|
||||||
|
[
|
||||||
|
emqx_start_listener/4,
|
||||||
|
fail_when_ssl_error/1,
|
||||||
|
fail_when_no_ssl_alert/2,
|
||||||
|
generate_tls_certs/1
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
||||||
|
all() -> emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
generate_tls_certs(Config),
|
||||||
|
application:ensure_all_started(esockd),
|
||||||
|
[{ssl_config, ssl_config_verify_partial_chain()} | Config].
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
application:stop(esockd).
|
||||||
|
|
||||||
|
t_conn_success_with_server_intermediate_cacert_and_client_cert(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate1.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server1.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client1.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client1.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_success_with_intermediate_cacert_bundle(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "server1-intermediate1-bundle.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server1.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client1.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client1.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_success_with_renewed_intermediate_cacert(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate1_renewed.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server1.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client1.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client1.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_fail_with_renewed_intermediate_cacert_and_client_using_old_complete_bundle(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate2_renewed.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server2.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
Res = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-complete-bundle.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_no_ssl_alert(Res, unknown_ca).
|
||||||
|
|
||||||
|
t_conn_fail_with_renewed_intermediate_cacert_and_client_using_old_bundle(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate2_renewed.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server2.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
Res = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_no_ssl_alert(Res, unknown_ca).
|
||||||
|
|
||||||
|
t_conn_success_with_old_and_renewed_intermediate_cacert_and_client_provides_renewed_client_cert(
|
||||||
|
Config
|
||||||
|
) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server2.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server2.key")},
|
||||||
|
{partial_chain, two_cacerts_from_cacertfile}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2_renewed.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ssl:close(Socket).
|
||||||
|
|
||||||
|
%% Note, this is good to have for usecase coverage
|
||||||
|
t_conn_success_with_new_intermediate_cacert_and_client_provides_renewed_client_cert_signed_by_old_intermediate(
|
||||||
|
Config
|
||||||
|
) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate2_renewed.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server2.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2_renewed.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ssl:close(Socket).
|
||||||
|
|
||||||
|
%% @doc server should build a partial_chain with old version of ca cert.
|
||||||
|
t_conn_success_with_old_and_renewed_intermediate_cacert_and_client_provides_client_cert(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server2.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server2.key")},
|
||||||
|
{partial_chain, two_cacerts_from_cacertfile}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ssl:close(Socket).
|
||||||
|
|
||||||
|
%% @doc verify when config does not allow two versions of certs from same trusted CA.
|
||||||
|
t_conn_fail_with_renewed_and_old_intermediate_cacert_and_client_using_old_bundle(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server2.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
Res = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_no_ssl_alert(Res, unknown_ca).
|
||||||
|
|
||||||
|
%% @doc verify when config (two_cacerts_from_cacertfile) allows two versions of certs from same trusted CA.
|
||||||
|
t_001_conn_success_with_old_and_renewed_intermediate_cacert_bundle_and_client_using_old_bundle(
|
||||||
|
Config
|
||||||
|
) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server2.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server2.key")},
|
||||||
|
{partial_chain, two_cacerts_from_cacertfile}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ssl:close(Socket).
|
||||||
|
|
||||||
|
%% @doc: verify even if listener has old/new intermediate2 certs,
|
||||||
|
%% client1 should not able to connect with old intermediate2 cert.
|
||||||
|
%% In this case, listener verify_fun returns {trusted_ca, Oldintermediate2Cert} but
|
||||||
|
%% OTP should still fail the validation since the client1 cert is not signed by
|
||||||
|
%% Oldintermediate2Cert (trusted CA cert).
|
||||||
|
%% @end
|
||||||
|
t_conn_fail_with_old_and_renewed_intermediate_cacert_bundle_and_client_using_all_CAcerts(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server2.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server2.key")},
|
||||||
|
{partial_chain, two_cacerts_from_cacertfile}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
Res = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client1.key")},
|
||||||
|
{certfile, filename:join(DataDir, "all-CAcerts-bundle.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_no_ssl_alert(Res, unknown_ca).
|
||||||
|
|
||||||
|
t_conn_fail_with_renewed_intermediate_cacert_other_client(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate1_renewed.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server1.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
Res = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_no_ssl_alert(Res, unknown_ca).
|
||||||
|
|
||||||
|
t_conn_fail_with_intermediate_cacert_bundle_but_incorrect_order(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate1-server1-bundle.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server1.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
Res = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client1.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client1.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_no_ssl_alert(Res, unknown_ca).
|
||||||
|
|
||||||
|
t_conn_fail_when_singed_by_other_intermediate_ca(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate1.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server1.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
Res = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_no_ssl_alert(Res, unknown_ca).
|
||||||
|
|
||||||
|
t_conn_success_with_complete_chain_that_server_root_cacert_and_client_complete_cert_chain(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "root.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server2.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-complete-bundle.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_fail_with_other_client_complete_cert_chain(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate1.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server1.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
Res = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-complete-bundle.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_no_ssl_alert(Res, unknown_ca).
|
||||||
|
|
||||||
|
t_conn_fail_with_server_intermediate_and_other_client_complete_cert_chain(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate1-root-bundle.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server1.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-complete-bundle.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_success_with_server_intermediate_cacert_and_client_complete_chain(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate2.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server2.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-complete-bundle.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_fail_with_server_intermediate_chain_and_client_other_incomplete_cert_chain(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate1.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server1.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
Res = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_no_ssl_alert(Res, unknown_ca).
|
||||||
|
|
||||||
|
t_conn_fail_with_server_intermediate_and_other_client_root_chain(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate1.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server1.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
Res = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-root-bundle.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_no_ssl_alert(Res, unknown_ca).
|
||||||
|
|
||||||
|
t_conn_success_with_server_intermediate_and_client_root_chain(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate2.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server2.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-root-bundle.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
%% @doc once rootCA cert present in cacertfile, sibling CA signed Client cert could connect.
|
||||||
|
t_conn_success_with_server_all_CA_bundle_and_client_root_chain(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "all-CAcerts-bundle.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server1.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-root-bundle.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ok = ssl:close(Socket).
|
||||||
|
|
||||||
|
t_conn_fail_with_server_two_IA_bundle_and_client_root_chain(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "two-intermediates-bundle.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server1.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server1.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
Res = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-root-bundle.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_no_ssl_alert(Res, unknown_ca).
|
||||||
|
|
||||||
|
t_conn_fail_with_server_partial_chain_false_intermediate_cacert_and_client_cert(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "intermediate1.pem")},
|
||||||
|
{certfile, filename:join(DataDir, "server1.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server1.key")},
|
||||||
|
{partial_chain, false}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options),
|
||||||
|
Res = ssl:connect(
|
||||||
|
{127, 0, 0, 1},
|
||||||
|
Port,
|
||||||
|
[
|
||||||
|
{keyfile, filename:join(DataDir, "client1.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client1.pem")}
|
||||||
|
| client_default_tls_opts()
|
||||||
|
],
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
fail_when_no_ssl_alert(Res, unknown_ca).
|
||||||
|
|
||||||
|
t_error_handling_invalid_cacertfile(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
%% trigger error
|
||||||
|
Options = [
|
||||||
|
{ssl_options,
|
||||||
|
?config(ssl_config, Config) ++
|
||||||
|
[
|
||||||
|
{cacertfile, filename:join(DataDir, "server2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "server2.pem")},
|
||||||
|
{keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
?assertException(
|
||||||
|
throw,
|
||||||
|
{error, rootfun_trusted_ca_from_cacertfile},
|
||||||
|
emqx_start_listener(?FUNCTION_NAME, ssl, Port, Options)
|
||||||
|
).
|
||||||
|
|
||||||
|
ssl_config_verify_partial_chain() ->
|
||||||
|
[
|
||||||
|
{verify, verify_peer},
|
||||||
|
{fail_if_no_peer_cert, true},
|
||||||
|
{partial_chain, true}
|
||||||
|
].
|
||||||
|
|
||||||
|
client_default_tls_opts() ->
|
||||||
|
[
|
||||||
|
{versions, ['tlsv1.2']},
|
||||||
|
{verify, verify_none}
|
||||||
|
].
|
|
@ -0,0 +1,319 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_test_tls_certs_helper).
|
||||||
|
-export([
|
||||||
|
gen_ca/2,
|
||||||
|
gen_host_cert/3,
|
||||||
|
gen_host_cert/4,
|
||||||
|
|
||||||
|
select_free_port/1,
|
||||||
|
generate_tls_certs/1,
|
||||||
|
|
||||||
|
fail_when_ssl_error/1,
|
||||||
|
fail_when_ssl_error/2,
|
||||||
|
fail_when_no_ssl_alert/2,
|
||||||
|
fail_when_no_ssl_alert/3,
|
||||||
|
|
||||||
|
emqx_start_listener/4
|
||||||
|
]).
|
||||||
|
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
%%-------------------------------------------------------------------------------
|
||||||
|
%% Start Listener
|
||||||
|
%%-------------------------------------------------------------------------------
|
||||||
|
emqx_start_listener(Name, Type, Port, Opts) when is_list(Opts) ->
|
||||||
|
emqx_start_listener(Name, Type, Port, maps:from_list(Opts));
|
||||||
|
emqx_start_listener(Name, ssl, Port, #{ssl_options := SslOptions} = Opts0) ->
|
||||||
|
Opts = Opts0#{
|
||||||
|
enable => true,
|
||||||
|
bind => {{127, 0, 0, 1}, Port},
|
||||||
|
mountpoint => <<>>,
|
||||||
|
zone => default,
|
||||||
|
ssl_options => maps:from_list(SslOptions)
|
||||||
|
},
|
||||||
|
ct:pal("start listener with ~p ~p", [Name, Opts]),
|
||||||
|
emqx_listeners:start_listener(ssl, Name, Opts).
|
||||||
|
|
||||||
|
%%-------------------------------------------------------------------------------
|
||||||
|
%% TLS certs
|
||||||
|
%%-------------------------------------------------------------------------------
|
||||||
|
gen_ca(Path, Name) ->
|
||||||
|
%% Generate ca.pem and ca.key which will be used to generate certs
|
||||||
|
%% for hosts server and clients
|
||||||
|
ECKeyFile = eckey_name(Path),
|
||||||
|
filelib:ensure_dir(ECKeyFile),
|
||||||
|
os:cmd("openssl ecparam -name secp256r1 > " ++ ECKeyFile),
|
||||||
|
Cmd = lists:flatten(
|
||||||
|
io_lib:format(
|
||||||
|
"openssl req -new -x509 -nodes "
|
||||||
|
"-newkey ec:~s "
|
||||||
|
"-keyout ~s -out ~s -days 3650 "
|
||||||
|
"-addext basicConstraints=CA:TRUE "
|
||||||
|
"-subj \"/C=SE/O=TEST CA\"",
|
||||||
|
[
|
||||||
|
ECKeyFile,
|
||||||
|
ca_key_name(Path, Name),
|
||||||
|
ca_cert_name(Path, Name)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
os:cmd(Cmd).
|
||||||
|
|
||||||
|
ca_cert_name(Path, Name) ->
|
||||||
|
filename(Path, "~s.pem", [Name]).
|
||||||
|
ca_key_name(Path, Name) ->
|
||||||
|
filename(Path, "~s.key", [Name]).
|
||||||
|
|
||||||
|
eckey_name(Path) ->
|
||||||
|
filename(Path, "ec.key", []).
|
||||||
|
|
||||||
|
gen_host_cert(H, CaName, Path) ->
|
||||||
|
gen_host_cert(H, CaName, Path, #{}).
|
||||||
|
|
||||||
|
gen_host_cert(H, CaName, Path, Opts) ->
|
||||||
|
ECKeyFile = eckey_name(Path),
|
||||||
|
CN = str(H),
|
||||||
|
HKey = filename(Path, "~s.key", [H]),
|
||||||
|
HCSR = filename(Path, "~s.csr", [H]),
|
||||||
|
HCSR2 = filename(Path, "~s.csr", [H]),
|
||||||
|
HPEM = filename(Path, "~s.pem", [H]),
|
||||||
|
HPEM2 = filename(Path, "~s_renewed.pem", [H]),
|
||||||
|
HEXT = filename(Path, "~s.extfile", [H]),
|
||||||
|
PasswordArg =
|
||||||
|
case maps:get(password, Opts, undefined) of
|
||||||
|
undefined ->
|
||||||
|
" -nodes ";
|
||||||
|
Password ->
|
||||||
|
io_lib:format(" -passout pass:'~s' ", [Password])
|
||||||
|
end,
|
||||||
|
|
||||||
|
create_file(
|
||||||
|
HEXT,
|
||||||
|
"keyUsage=digitalSignature,keyAgreement,keyCertSign\n"
|
||||||
|
"basicConstraints=CA:TRUE \n"
|
||||||
|
"~s \n"
|
||||||
|
"subjectAltName=DNS:~s\n",
|
||||||
|
[maps:get(ext, Opts, ""), CN]
|
||||||
|
),
|
||||||
|
|
||||||
|
CSR_Cmd = csr_cmd(PasswordArg, ECKeyFile, HKey, HCSR, CN),
|
||||||
|
CSR_Cmd2 = csr_cmd(PasswordArg, ECKeyFile, HKey, HCSR2, CN),
|
||||||
|
|
||||||
|
CERT_Cmd = cert_sign_cmd(
|
||||||
|
HEXT, HCSR, ca_cert_name(Path, CaName), ca_key_name(Path, CaName), HPEM
|
||||||
|
),
|
||||||
|
%% 2nd cert for testing renewed cert.
|
||||||
|
CERT_Cmd2 = cert_sign_cmd(
|
||||||
|
HEXT, HCSR2, ca_cert_name(Path, CaName), ca_key_name(Path, CaName), HPEM2
|
||||||
|
),
|
||||||
|
ct:pal(os:cmd(CSR_Cmd)),
|
||||||
|
ct:pal(os:cmd(CSR_Cmd2)),
|
||||||
|
ct:pal(os:cmd(CERT_Cmd)),
|
||||||
|
ct:pal(os:cmd(CERT_Cmd2)),
|
||||||
|
file:delete(HEXT).
|
||||||
|
|
||||||
|
cert_sign_cmd(ExtFile, CSRFile, CACert, CAKey, OutputCert) ->
|
||||||
|
lists:flatten(
|
||||||
|
io_lib:format(
|
||||||
|
"openssl x509 -req "
|
||||||
|
"-extfile ~s "
|
||||||
|
"-in ~s -CA ~s -CAkey ~s -CAcreateserial "
|
||||||
|
"-out ~s -days 500",
|
||||||
|
[
|
||||||
|
ExtFile,
|
||||||
|
CSRFile,
|
||||||
|
CACert,
|
||||||
|
CAKey,
|
||||||
|
OutputCert
|
||||||
|
]
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
|
csr_cmd(PasswordArg, ECKeyFile, HKey, HCSR, CN) ->
|
||||||
|
lists:flatten(
|
||||||
|
io_lib:format(
|
||||||
|
"openssl req -new ~s -newkey ec:~s "
|
||||||
|
"-keyout ~s -out ~s "
|
||||||
|
"-addext \"subjectAltName=DNS:~s\" "
|
||||||
|
"-addext basicConstraints=CA:TRUE "
|
||||||
|
"-addext keyUsage=digitalSignature,keyAgreement,keyCertSign "
|
||||||
|
"-subj \"/C=SE/O=TEST/CN=~s\"",
|
||||||
|
[PasswordArg, ECKeyFile, HKey, HCSR, CN, CN]
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
|
filename(Path, F, A) ->
|
||||||
|
filename:join(Path, str(io_lib:format(F, A))).
|
||||||
|
|
||||||
|
str(Arg) ->
|
||||||
|
binary_to_list(iolist_to_binary(Arg)).
|
||||||
|
|
||||||
|
create_file(Filename, Fmt, Args) ->
|
||||||
|
filelib:ensure_dir(Filename),
|
||||||
|
{ok, F} = file:open(Filename, [write]),
|
||||||
|
try
|
||||||
|
io:format(F, Fmt, Args)
|
||||||
|
after
|
||||||
|
file:close(F)
|
||||||
|
end,
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @doc get unused port from OS
|
||||||
|
-spec select_free_port(tcp | udp | ssl | quic) -> inets:port_number().
|
||||||
|
select_free_port(tcp) ->
|
||||||
|
select_free_port(gen_tcp, listen);
|
||||||
|
select_free_port(udp) ->
|
||||||
|
select_free_port(gen_udp, open);
|
||||||
|
select_free_port(ssl) ->
|
||||||
|
select_free_port(tcp);
|
||||||
|
select_free_port(quic) ->
|
||||||
|
select_free_port(udp).
|
||||||
|
|
||||||
|
select_free_port(GenModule, Fun) when
|
||||||
|
GenModule == gen_tcp orelse
|
||||||
|
GenModule == gen_udp
|
||||||
|
->
|
||||||
|
{ok, S} = GenModule:Fun(0, [{reuseaddr, true}]),
|
||||||
|
{ok, Port} = inet:port(S),
|
||||||
|
ok = GenModule:close(S),
|
||||||
|
case os:type() of
|
||||||
|
{unix, darwin} ->
|
||||||
|
%% in MacOS, still get address_in_use after close port
|
||||||
|
timer:sleep(500);
|
||||||
|
_ ->
|
||||||
|
skip
|
||||||
|
end,
|
||||||
|
ct:pal("Select free OS port: ~p", [Port]),
|
||||||
|
Port.
|
||||||
|
|
||||||
|
%% @doc fail the test if ssl_error recvd
|
||||||
|
%% post check for success conn establishment
|
||||||
|
fail_when_ssl_error(Socket) ->
|
||||||
|
fail_when_ssl_error(Socket, 1000).
|
||||||
|
fail_when_ssl_error(Socket, Timeout) ->
|
||||||
|
receive
|
||||||
|
{ssl_error, Socket, _} ->
|
||||||
|
ct:fail("Handshake failed!")
|
||||||
|
after Timeout ->
|
||||||
|
ok
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc fail the test if no ssl_error
|
||||||
|
fail_when_no_ssl_alert(Res, Alert) ->
|
||||||
|
fail_when_no_ssl_alert(Res, Alert, 1000).
|
||||||
|
|
||||||
|
fail_when_no_ssl_alert({error, {tls_alert, {Alert, _}}}, Alert, _Timeout) ->
|
||||||
|
ok;
|
||||||
|
fail_when_no_ssl_alert({error, _} = Other, Alert, _Timeout) ->
|
||||||
|
ct:fail("returned unexpected ssl_error: ~p, expected ~n", [Other, Alert]);
|
||||||
|
fail_when_no_ssl_alert({ok, Socket}, Alert, Timeout) ->
|
||||||
|
fail_when_no_ssl_alert(Socket, Alert, Timeout);
|
||||||
|
fail_when_no_ssl_alert(Socket, Alert, Timeout) ->
|
||||||
|
receive
|
||||||
|
{ssl_error, Socket, {tls_alert, {Alert, AlertInfo}}} ->
|
||||||
|
ct:pal("alert info: ~p~n", [AlertInfo]);
|
||||||
|
{ssl_error, Socket, Other} ->
|
||||||
|
ct:fail("recv unexpected ssl_error: ~p~n", [Other])
|
||||||
|
after Timeout ->
|
||||||
|
ct:fail("No expected alert: ~p from Socket: ~p ", [Alert, Socket])
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Generate TLS cert chain for tests
|
||||||
|
generate_tls_certs(Config) ->
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
gen_ca(DataDir, "root"),
|
||||||
|
gen_host_cert("intermediate1", "root", DataDir),
|
||||||
|
gen_host_cert("intermediate2", "root", DataDir),
|
||||||
|
gen_host_cert("server1", "intermediate1", DataDir),
|
||||||
|
gen_host_cert("client1", "intermediate1", DataDir),
|
||||||
|
gen_host_cert("server2", "intermediate2", DataDir),
|
||||||
|
gen_host_cert("client2", "intermediate2", DataDir),
|
||||||
|
|
||||||
|
%% Build bundles below
|
||||||
|
os:cmd(
|
||||||
|
io_lib:format("cat ~p ~p ~p > ~p", [
|
||||||
|
filename:join(DataDir, "client2.pem"),
|
||||||
|
filename:join(DataDir, "intermediate2.pem"),
|
||||||
|
filename:join(DataDir, "root.pem"),
|
||||||
|
filename:join(DataDir, "client2-complete-bundle.pem")
|
||||||
|
])
|
||||||
|
),
|
||||||
|
os:cmd(
|
||||||
|
io_lib:format("cat ~p ~p > ~p", [
|
||||||
|
filename:join(DataDir, "client2.pem"),
|
||||||
|
filename:join(DataDir, "intermediate2.pem"),
|
||||||
|
filename:join(DataDir, "client2-intermediate2-bundle.pem")
|
||||||
|
])
|
||||||
|
),
|
||||||
|
os:cmd(
|
||||||
|
io_lib:format("cat ~p ~p > ~p", [
|
||||||
|
filename:join(DataDir, "client2.pem"),
|
||||||
|
filename:join(DataDir, "root.pem"),
|
||||||
|
filename:join(DataDir, "client2-root-bundle.pem")
|
||||||
|
])
|
||||||
|
),
|
||||||
|
os:cmd(
|
||||||
|
io_lib:format("cat ~p ~p > ~p", [
|
||||||
|
filename:join(DataDir, "server1.pem"),
|
||||||
|
filename:join(DataDir, "intermediate1.pem"),
|
||||||
|
filename:join(DataDir, "server1-intermediate1-bundle.pem")
|
||||||
|
])
|
||||||
|
),
|
||||||
|
os:cmd(
|
||||||
|
io_lib:format("cat ~p ~p > ~p", [
|
||||||
|
filename:join(DataDir, "intermediate1.pem"),
|
||||||
|
filename:join(DataDir, "server1.pem"),
|
||||||
|
filename:join(DataDir, "intermediate1-server1-bundle.pem")
|
||||||
|
])
|
||||||
|
),
|
||||||
|
os:cmd(
|
||||||
|
io_lib:format("cat ~p ~p > ~p", [
|
||||||
|
filename:join(DataDir, "intermediate1_renewed.pem"),
|
||||||
|
filename:join(DataDir, "root.pem"),
|
||||||
|
filename:join(DataDir, "intermediate1_renewed-root-bundle.pem")
|
||||||
|
])
|
||||||
|
),
|
||||||
|
os:cmd(
|
||||||
|
io_lib:format("cat ~p ~p > ~p", [
|
||||||
|
filename:join(DataDir, "intermediate2.pem"),
|
||||||
|
filename:join(DataDir, "intermediate2_renewed.pem"),
|
||||||
|
filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")
|
||||||
|
])
|
||||||
|
),
|
||||||
|
os:cmd(
|
||||||
|
io_lib:format("cat ~p ~p > ~p", [
|
||||||
|
filename:join(DataDir, "intermediate1.pem"),
|
||||||
|
filename:join(DataDir, "root.pem"),
|
||||||
|
filename:join(DataDir, "intermediate1-root-bundle.pem")
|
||||||
|
])
|
||||||
|
),
|
||||||
|
os:cmd(
|
||||||
|
io_lib:format("cat ~p ~p ~p > ~p", [
|
||||||
|
filename:join(DataDir, "root.pem"),
|
||||||
|
filename:join(DataDir, "intermediate2.pem"),
|
||||||
|
filename:join(DataDir, "intermediate1.pem"),
|
||||||
|
filename:join(DataDir, "all-CAcerts-bundle.pem")
|
||||||
|
])
|
||||||
|
),
|
||||||
|
os:cmd(
|
||||||
|
io_lib:format("cat ~p ~p > ~p", [
|
||||||
|
filename:join(DataDir, "intermediate2.pem"),
|
||||||
|
filename:join(DataDir, "intermediate1.pem"),
|
||||||
|
filename:join(DataDir, "two-intermediates-bundle.pem")
|
||||||
|
])
|
||||||
|
).
|
|
@ -559,6 +559,8 @@ ssl_opts(Name, Opts) ->
|
||||||
[
|
[
|
||||||
fun ssl_opts_crl_config/2,
|
fun ssl_opts_crl_config/2,
|
||||||
fun ssl_opts_drop_unsupported/2,
|
fun ssl_opts_drop_unsupported/2,
|
||||||
|
fun ssl_partial_chain/2,
|
||||||
|
fun ssl_verify_fun/2,
|
||||||
fun ssl_server_opts/2
|
fun ssl_server_opts/2
|
||||||
],
|
],
|
||||||
SSLOpts,
|
SSLOpts,
|
||||||
|
@ -586,6 +588,12 @@ ssl_server_opts(SSLOpts, ssl_options) ->
|
||||||
ssl_server_opts(SSLOpts, dtls_options) ->
|
ssl_server_opts(SSLOpts, dtls_options) ->
|
||||||
emqx_tls_lib:to_server_opts(dtls, SSLOpts).
|
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) ->
|
ranch_opts(Type, ListenOn, Opts) ->
|
||||||
NumAcceptors = maps:get(acceptors, Opts, 4),
|
NumAcceptors = maps:get(acceptors, Opts, 4),
|
||||||
MaxConnections = maps:get(max_connections, Opts, 1024),
|
MaxConnections = maps:get(max_connections, Opts, 1024),
|
||||||
|
|
|
@ -469,6 +469,7 @@ t_create_webhook_v1_bridges_api(Config) ->
|
||||||
<<"enable">> => true,
|
<<"enable">> => true,
|
||||||
<<"hibernate_after">> => <<"5s">>,
|
<<"hibernate_after">> => <<"5s">>,
|
||||||
<<"log_level">> => <<"notice">>,
|
<<"log_level">> => <<"notice">>,
|
||||||
|
<<"partial_chain">> => false,
|
||||||
<<"reuse_sessions">> => true,
|
<<"reuse_sessions">> => true,
|
||||||
<<"secure_renegotiate">> => true,
|
<<"secure_renegotiate">> => true,
|
||||||
<<"user_lookup_fun">> =>
|
<<"user_lookup_fun">> =>
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
Port two TLS handshake validation features from emqx 4.4
|
||||||
|
|
||||||
|
- partial_chain support
|
||||||
|
- Certificate KeyUsage Validation
|
||||||
|
|
|
@ -684,6 +684,18 @@ common_ssl_opts_schema_verify.desc:
|
||||||
common_ssl_opts_schema_verify.label:
|
common_ssl_opts_schema_verify.label:
|
||||||
"""Verify peer"""
|
"""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"""
|
||||||
|
|
||||||
|
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:
|
fields_listeners_ssl.desc:
|
||||||
"""SSL listeners."""
|
"""SSL listeners."""
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue