This reverts commit28b17a2562. This reverts commit01467246fc. This reverts commitc3f8ba5762. This reverts commit1a4a4bb3a5. This reverts commitfb30207ef3. This reverts commit337c230e79. This reverts commit3a674f44f1. This reverts commit70ffd77f99. This reverts commit03b0935564. This reverts commit650cf4b27e. This reverts commit43ad665dcf. This reverts commita29a43e5fc. This reverts commit4e9c1ec0c9. This reverts commit8eb463c58d. This reverts commit90430fa66d. This reverts commiteb1ab9adfe. This reverts commit8bc3a86f63. This reverts commitfa4357ce89. This reverts commit0b95a08d32.
This commit is contained in:
parent
1dd4b6de5e
commit
1664ea4ad4
|
|
@ -1,125 +0,0 @@
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% 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
|
|
||||||
).
|
|
||||||
|
|
@ -611,9 +611,7 @@ 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),
|
||||||
OptsWithRootFun = inject_root_fun(OptsWithSNI),
|
SSLOpts = ssl_opts(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
|
||||||
).
|
).
|
||||||
|
|
@ -637,18 +635,8 @@ 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 ->
|
wss -> tcp_opts(Opts) ++ proplists:delete(handshake_timeout, ssl_opts(Opts));
|
||||||
tcp_opts(Opts) ++
|
ws -> 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,
|
||||||
|
|
@ -974,16 +962,6 @@ 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) ->
|
||||||
|
|
|
||||||
|
|
@ -2178,22 +2178,6 @@ 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,7 +15,6 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
-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([
|
||||||
|
|
@ -24,8 +23,6 @@
|
||||||
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
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
|
@ -688,55 +685,3 @@ 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).
|
|
||||||
|
|
|
||||||
|
|
@ -1,257 +0,0 @@
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% 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}
|
|
||||||
].
|
|
||||||
|
|
@ -1,372 +0,0 @@
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% 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}
|
|
||||||
].
|
|
||||||
|
|
@ -1,708 +0,0 @@
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% 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}
|
|
||||||
].
|
|
||||||
|
|
@ -1,319 +0,0 @@
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% 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,8 +559,6 @@ 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,
|
||||||
|
|
@ -588,12 +586,6 @@ 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),
|
||||||
|
|
|
||||||
|
|
@ -421,7 +421,6 @@ 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">> =>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
Enhance TLS listener to support more flexible TLS verifications.
|
|
||||||
|
|
||||||
- partial_chain support
|
|
||||||
|
|
||||||
If the option `partial_chain` is set to `true`, allow connections with incomplete certificate chains.
|
|
||||||
|
|
||||||
Check the configuration manual document for more details.
|
|
||||||
|
|
||||||
- Certificate KeyUsage Validation
|
|
||||||
|
|
||||||
Added support for required Extended Key Usage defined in
|
|
||||||
[rfc5280](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.12).
|
|
||||||
|
|
||||||
Introduced a new option (`verify_peer_ext_key_usage`) to require specific key usages (like "serverAuth")
|
|
||||||
in peer certificates during the TLS handshake.
|
|
||||||
This strengthens security by ensuring certificates are used for their intended purposes.
|
|
||||||
|
|
||||||
example:
|
|
||||||
"serverAuth,OID:1.3.6.1.5.5.7.3.2"
|
|
||||||
|
|
||||||
Check the configuration manual document for more details.
|
|
||||||
|
|
||||||
3
mix.exs
3
mix.exs
|
|
@ -101,8 +101,7 @@ defmodule EMQXUmbrella.MixProject do
|
||||||
{:bcrypt, github: "emqx/erlang-bcrypt", tag: "0.6.2", override: true},
|
{:bcrypt, github: "emqx/erlang-bcrypt", tag: "0.6.2", override: true},
|
||||||
{:uuid, github: "okeuday/uuid", tag: "v2.0.6", override: true},
|
{:uuid, github: "okeuday/uuid", tag: "v2.0.6", override: true},
|
||||||
{:quickrand, github: "okeuday/quickrand", tag: "v2.0.6", override: true},
|
{:quickrand, github: "okeuday/quickrand", tag: "v2.0.6", override: true},
|
||||||
{:ra, "2.7.3", override: true},
|
{:ra, "2.7.3", override: true}
|
||||||
{:mimerl, "1.2.0", override: true}
|
|
||||||
] ++
|
] ++
|
||||||
emqx_apps(profile_info, version) ++
|
emqx_apps(profile_info, version) ++
|
||||||
enterprise_deps(profile_info) ++ jq_dep() ++ quicer_dep()
|
enterprise_deps(profile_info) ++ jq_dep() ++ quicer_dep()
|
||||||
|
|
|
||||||
|
|
@ -111,8 +111,7 @@
|
||||||
{ssl_verify_fun, "1.1.7"},
|
{ssl_verify_fun, "1.1.7"},
|
||||||
{rfc3339, {git, "https://github.com/emqx/rfc3339.git", {tag, "0.2.3"}}},
|
{rfc3339, {git, "https://github.com/emqx/rfc3339.git", {tag, "0.2.3"}}},
|
||||||
{bcrypt, {git, "https://github.com/emqx/erlang-bcrypt.git", {tag, "0.6.2"}}},
|
{bcrypt, {git, "https://github.com/emqx/erlang-bcrypt.git", {tag, "0.6.2"}}},
|
||||||
{ra, "2.7.3"},
|
{ra, "2.7.3"}
|
||||||
{mimerl, "1.2.0"}
|
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
{xref_ignores,
|
{xref_ignores,
|
||||||
|
|
|
||||||
|
|
@ -684,49 +684,6 @@ 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.
|
|
||||||
When local verifies a peer certificate during the x509 path validation
|
|
||||||
process, it constructs a certificate chain that starts with the peer
|
|
||||||
certificate and ends with a trust anchor.
|
|
||||||
By default, if it is set to `false`, the trust anchor is the
|
|
||||||
Root CA, and the certificate chain must be complete.
|
|
||||||
However, if the setting is set to `true` or `cacert_from_cacertfile`,
|
|
||||||
the last certificate in `cacertfile` will be used as the trust anchor
|
|
||||||
certificate (intermediate CA). This creates a partial chain
|
|
||||||
in the path validation.
|
|
||||||
Alternatively, if it is configured with `two_cacerts_from_cacertfile`,
|
|
||||||
one of the last two certificates in `cacertfile` will be used as the
|
|
||||||
trust anchor certificate, forming a partial chain. This option is
|
|
||||||
particularly useful for intermediate CA certificate rotation.
|
|
||||||
However, please note that it incurs some additional overhead, so it
|
|
||||||
should only be used for certificate rotation purposes."""
|
|
||||||
|
|
||||||
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
|
|
||||||
For additional peer certificate validation, the value defined here must present in the
|
|
||||||
'Extended Key Usage' of peer certificate defined in
|
|
||||||
[rfc5280](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.12).
|
|
||||||
|
|
||||||
Allowed values are
|
|
||||||
- `clientAuth`
|
|
||||||
- `serverAuth`
|
|
||||||
- `codeSigning`
|
|
||||||
- `emailProtection`
|
|
||||||
- `timeStamping`
|
|
||||||
- `ocspSigning`
|
|
||||||
- raw OID, for example: "OID:1.3.6.1.5.5.7.3.2" means `id-pk 2` which is equivalent to `clientAuth`
|
|
||||||
|
|
||||||
Comma-separated string is also supported for validating more than one key usages.
|
|
||||||
|
|
||||||
For example, `"serverAuth,OID:1.3.6.1.5.5.7.3.2"`"""
|
|
||||||
|
|
||||||
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."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -310,4 +310,3 @@ ElasticSearch
|
||||||
doc_as_upsert
|
doc_as_upsert
|
||||||
upsert
|
upsert
|
||||||
aliyun
|
aliyun
|
||||||
OID
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue