feat: enhanced tls handshake
This commit is contained in:
parent
049a18416e
commit
5fa060a43c
|
@ -1650,6 +1650,11 @@ end}.
|
|||
{datatype, atom}
|
||||
]}.
|
||||
|
||||
{mapping, "listener.ssl.$name.verify_peer_ext_key_usage", "emqx.listeners", [
|
||||
{datatype, string},
|
||||
{default, ""}
|
||||
]}.
|
||||
|
||||
{mapping, "listener.ssl.$name.fail_if_no_peer_cert", "emqx.listeners", [
|
||||
{datatype, {enum, [true, false]}}
|
||||
]}.
|
||||
|
@ -2382,6 +2387,7 @@ end}.
|
|||
{cacertfile, cuttlefish:conf_get(Prefix ++ ".cacertfile", Conf, undefined)},
|
||||
{verify, cuttlefish:conf_get(Prefix ++ ".verify", Conf, undefined)},
|
||||
{partial_chain, cuttlefish:conf_get(Prefix ++ ".partial_chain", Conf, undefined)},
|
||||
{verify_peer_ext_key_usage, cuttlefish:conf_get(Prefix ++ ".verify_peer_ext_key_usage", Conf, undefined)},
|
||||
{fail_if_no_peer_cert, cuttlefish:conf_get(Prefix ++ ".fail_if_no_peer_cert", Conf, undefined)},
|
||||
{secure_renegotiate, cuttlefish:conf_get(Prefix ++ ".secure_renegotiate", Conf, undefined)},
|
||||
{reuse_sessions, cuttlefish:conf_get(Prefix ++ ".reuse_sessions", Conf, undefined)},
|
||||
|
|
|
@ -19,8 +19,10 @@
|
|||
-module(emqx_const_v2).
|
||||
|
||||
-export([ make_tls_root_fun/2
|
||||
, make_tls_verify_fun/2
|
||||
]).
|
||||
|
||||
-include_lib("public_key/include/public_key.hrl").
|
||||
%% @doc Build a root fun for verify TLS partial_chain.
|
||||
%% The `InputChain' is composed by OTP SSL with local cert store
|
||||
%% AND the cert (chain if any) from the client.
|
||||
|
@ -43,3 +45,78 @@ make_tls_root_fun(cacert_from_cacertfile, [TrustedOne, TrustedTwo]) ->
|
|||
{trusted_ca, TrustedTwo}
|
||||
end
|
||||
end.
|
||||
|
||||
make_tls_verify_fun(verify_cert_extKeyUsage, KeyUsages) ->
|
||||
AllowedKeyUsages = ext_key_opts(KeyUsages),
|
||||
fun(A, B, C) ->
|
||||
verify_fun_peer_extKeyUsage(A, B, C, AllowedKeyUsages)
|
||||
end.
|
||||
|
||||
verify_fun_peer_extKeyUsage(_, {bad_cert, invalid_ext_key_usage}, UserState, AllowedKeyUsages) ->
|
||||
%% !! Override OTP verify peer default
|
||||
%% OTP SSL is unhappy with the ext_key_usage but we will check on ower own.
|
||||
{unknown, UserState};
|
||||
verify_fun_peer_extKeyUsage(_, {bad_cert, _} = Reason, _, AllowedKeyUsages) ->
|
||||
%% OTP verify_peer default
|
||||
{fail, Reason};
|
||||
verify_fun_peer_extKeyUsage(_, {extension, _}, UserState, _AllowedKeyUsages) ->
|
||||
%% OTP verify_peer default
|
||||
{unknown, UserState};
|
||||
verify_fun_peer_extKeyUsage(_, valid, UserState, _AllowedKeyUsages) ->
|
||||
%% OTP verify_peer default
|
||||
{valid, UserState};
|
||||
verify_fun_peer_extKeyUsage(#'OTPCertificate'{tbsCertificate = #'OTPTBSCertificate'{extensions = ExtL}},
|
||||
valid_peer, %% valid peer cert
|
||||
UserState, AllowedKeyUsages) ->
|
||||
%% override OTP verify_peer default
|
||||
%% must have id-ce-extKeyUsage
|
||||
case lists:keyfind(?'id-ce-extKeyUsage', 2, ExtL) of
|
||||
#'Extension'{extnID = ?'id-ce-extKeyUsage', extnValue = VL} ->
|
||||
case do_verify_ext_key_usage(VL, AllowedKeyUsages) of
|
||||
true ->
|
||||
%% pass the check,
|
||||
%% fallback to OTP verify_peer default
|
||||
{valid, UserState};
|
||||
false ->
|
||||
{fail, extKeyUsage_unmatched}
|
||||
end;
|
||||
_ ->
|
||||
{fail, extKeyUsage_not_set}
|
||||
end.
|
||||
|
||||
%% @doc check required extkeyUsages are presented in the cert
|
||||
do_verify_ext_key_usage(_, []) ->
|
||||
%% Verify finished
|
||||
true;
|
||||
do_verify_ext_key_usage(CertExtL, [Usage | T] = _Required) ->
|
||||
case lists:member(Usage, CertExtL) of
|
||||
true ->
|
||||
do_verify_ext_key_usage(CertExtL, T);
|
||||
false ->
|
||||
false
|
||||
end.
|
||||
|
||||
%% @doc Helper tls cert extension
|
||||
-spec ext_key_opts(string()) -> [OidString::string() | public_key:oid()];
|
||||
(undefined) -> undefined.
|
||||
ext_key_opts(undefined) ->
|
||||
%% disabled
|
||||
undefined;
|
||||
ext_key_opts(Str) ->
|
||||
Usages = string:tokens(Str, ","),
|
||||
lists:map(fun("clientAuth") ->
|
||||
?'id-kp-clientAuth';
|
||||
("serverAuth") ->
|
||||
?'id-kp-serverAuth';
|
||||
("codeSigning") ->
|
||||
?'id-kp-codeSigning';
|
||||
("emailProtection") ->
|
||||
?'id-kp-emailProtection';
|
||||
("timeStamping") ->
|
||||
?'id-kp-timeStamping';
|
||||
("ocspSigning") ->
|
||||
?'id-kp-OCSPSigning';
|
||||
([$O,$I,$D,$: | OidStr]) ->
|
||||
OidList = string:tokens(OidStr, "."),
|
||||
list_to_tuple(lists:map(fun list_to_integer/1, OidList))
|
||||
end, Usages).
|
||||
|
|
|
@ -139,7 +139,8 @@ start_listener(Proto, ListenOn, Options0) when Proto == ssl; Proto == tls ->
|
|||
ListenerID = proplists:get_value(listener_id, Options0),
|
||||
Options1 = proplists:delete(listener_id, Options0),
|
||||
Options2 = emqx_ocsp_cache:inject_sni_fun(ListenerID, Options1),
|
||||
Options = emqx_tls_lib:inject_root_fun(Options2),
|
||||
Options3 = emqx_tls_lib:inject_root_fun(Options2),
|
||||
Options = emqx_tls_lib:inject_verify_fun(Options3),
|
||||
ok = maybe_register_crl_urls(Options),
|
||||
start_mqtt_listener('mqtt:ssl', ListenOn, Options);
|
||||
|
||||
|
|
|
@ -23,10 +23,13 @@
|
|||
, integral_ciphers/2
|
||||
, drop_tls13_for_old_otp/1
|
||||
, inject_root_fun/1
|
||||
, inject_verify_fun/1
|
||||
, opt_partial_chain/1
|
||||
, opt_verify_fun/1
|
||||
]).
|
||||
|
||||
-include("logger.hrl").
|
||||
-include_lib("public_key/include/public_key.hrl").
|
||||
|
||||
%% non-empty string
|
||||
-define(IS_STRING(L), (is_list(L) andalso L =/= [] andalso is_integer(hd(L)))).
|
||||
|
@ -182,6 +185,14 @@ inject_root_fun(Options) ->
|
|||
replace(Options, ssl_options, opt_partial_chain(SslOpts))
|
||||
end.
|
||||
|
||||
inject_verify_fun(Options) ->
|
||||
case proplists:get_value(ssl_options, Options) of
|
||||
undefined ->
|
||||
Options;
|
||||
SslOpts ->
|
||||
replace(Options, ssl_options, emqx_tls_lib:opt_verify_fun(SslOpts))
|
||||
end.
|
||||
|
||||
%% @doc enable TLS partial_chain validation if set.
|
||||
-spec opt_partial_chain(SslOpts :: proplists:proplist()) -> NewSslOpts :: proplists:proplist().
|
||||
opt_partial_chain(SslOpts) ->
|
||||
|
@ -196,6 +207,17 @@ opt_partial_chain(SslOpts) ->
|
|||
replace(SslOpts, partial_chain, rootfun_trusted_ca_from_cacertfile(2, SslOpts))
|
||||
end.
|
||||
|
||||
|
||||
-spec opt_verify_fun(SslOpts :: proplists:proplist()) -> NewSslOpts :: proplists:proplist().
|
||||
opt_verify_fun(SslOpts) ->
|
||||
case proplists:get_value(verify_peer_ext_key_usage, SslOpts, undefined) of
|
||||
undefined ->
|
||||
SslOpts;
|
||||
V ->
|
||||
Fun = emqx_const_v2:make_tls_verify_fun(verify_cert_extKeyUsage, V),
|
||||
replace(SslOpts, verify_fun, {Fun, #{}})
|
||||
end.
|
||||
|
||||
replace(Opts, Key, Value) -> [{Key, Value} | proplists:delete(Key, Opts)].
|
||||
|
||||
%% @doc Helper, make TLS root_fun
|
||||
|
|
|
@ -0,0 +1,234 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_listener_tls_verify_keyusage_SUITE).
|
||||
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
-import(emqx_test_tls_certs_helper, [ fail_when_ssl_error/1
|
||||
, fail_when_no_ssl_alert/2
|
||||
, generate_tls_certs/1
|
||||
, gen_host_cert/4
|
||||
]).
|
||||
|
||||
|
||||
all() ->
|
||||
[ {group, full_chain}
|
||||
, {group, partial_chain}
|
||||
].
|
||||
|
||||
all_tc() ->
|
||||
emqx_ct:all(?MODULE).
|
||||
|
||||
groups() ->
|
||||
[ {partial_chain, [], all_tc()}
|
||||
, {full_chain, [], all_tc()}
|
||||
].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
dbg:tracer(process, {fun dbg:dhandler/2,group_leader()}),
|
||||
dbg:p(all,c),
|
||||
dbg:tpl(emqx_tls_lib, opt_verify_fun, cx),
|
||||
dbg:tpl(emqx_const_v2, verify_fun_peer_extKeyUsage, cx),
|
||||
dbg:tpl(emqx_const_v2, do_verify_ext_key_usage,cx),
|
||||
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),
|
||||
Options = [{ssl_options, ?config(ssl_config, Config)}],
|
||||
emqx_listeners:start_listener(ssl, Port, Options),
|
||||
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")},
|
||||
{certfile, filename:join(DataDir, "client1.pem")}
|
||||
], 1000),
|
||||
fail_when_ssl_error(Socket),
|
||||
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),
|
||||
Options = [{ssl_options, [ {verify_peer_ext_key_usage, undefined}
|
||||
| ?config(ssl_config, Config)
|
||||
]}],
|
||||
emqx_listeners:start_listener(ssl, Port, Options),
|
||||
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")},
|
||||
{certfile, filename:join(DataDir, "client1.pem")}
|
||||
], 1000),
|
||||
fail_when_ssl_error(Socket),
|
||||
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),
|
||||
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth"),
|
||||
Options = [{ssl_options, [ {verify_peer_ext_key_usage, "clientAuth"}
|
||||
| ?config(ssl_config, Config)
|
||||
]}],
|
||||
emqx_listeners:start_listener(ssl, Port, Options),
|
||||
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, client_key_file(DataDir, ?FUNCTION_NAME)},
|
||||
{certfile, client_pem_file(DataDir, ?FUNCTION_NAME)}
|
||||
], 1000),
|
||||
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),
|
||||
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth"),
|
||||
Options = [{ssl_options, [ {verify_peer_ext_key_usage, "OID:1.3.6.1.5.5.7.3.2"} %% from OTP-PUB-KEY.hrl
|
||||
| ?config(ssl_config, Config)
|
||||
]}],
|
||||
emqx_listeners:start_listener(ssl, Port, Options),
|
||||
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, client_key_file(DataDir, ?FUNCTION_NAME)},
|
||||
{certfile, client_pem_file(DataDir, ?FUNCTION_NAME)}
|
||||
], 1000),
|
||||
fail_when_ssl_error(Socket),
|
||||
ok = ssl:close(Socket).
|
||||
|
||||
t_conn_success_verify_peer_ext_key_usage_matched_unorded_list(Config) ->
|
||||
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||
DataDir = ?config(data_dir, Config),
|
||||
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth,serverAuth"),
|
||||
Options = [{ssl_options, [ {verify_peer_ext_key_usage, "clientAuth,serverAuth"}
|
||||
| ?config(ssl_config, Config)
|
||||
]}],
|
||||
emqx_listeners:start_listener(ssl, Port, Options),
|
||||
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, client_key_file(DataDir, ?FUNCTION_NAME)},
|
||||
{certfile, client_pem_file(DataDir, ?FUNCTION_NAME)}
|
||||
], 1000),
|
||||
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),
|
||||
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth,serverAuth"),
|
||||
Options = [{ssl_options, [ {verify_peer_ext_key_usage, "serverAuth,clientAuth"}
|
||||
| ?config(ssl_config, Config)
|
||||
]}],
|
||||
emqx_listeners:start_listener(ssl, Port, Options),
|
||||
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, client_key_file(DataDir, ?FUNCTION_NAME)},
|
||||
{certfile, client_pem_file(DataDir, ?FUNCTION_NAME)}
|
||||
], 1000),
|
||||
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),
|
||||
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "clientAuth"),
|
||||
Options = [{ssl_options, [ {verify_peer_ext_key_usage, "OID:1.3.6.1.5.5.7.3.1"}
|
||||
| ?config(ssl_config, Config)
|
||||
]}],
|
||||
emqx_listeners:start_listener(ssl, Port, Options),
|
||||
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, client_key_file(DataDir, ?FUNCTION_NAME)},
|
||||
{certfile, client_pem_file(DataDir, ?FUNCTION_NAME)}
|
||||
], 1000),
|
||||
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)
|
||||
]}],
|
||||
emqx_listeners:start_listener(ssl, Port, Options),
|
||||
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")},
|
||||
{certfile, filename:join(DataDir, "client1.pem")}
|
||||
], 1000),
|
||||
fail_when_no_ssl_alert(Socket, 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),
|
||||
gen_client_cert_ext_keyusage(?FUNCTION_NAME, "intermediate1", DataDir, "codeSigning"),
|
||||
Options = [{ssl_options, [ {verify_peer_ext_key_usage, "clientAuth"}
|
||||
| ?config(ssl_config, Config)
|
||||
]}],
|
||||
emqx_listeners:start_listener(ssl, Port, Options),
|
||||
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, client_key_file(DataDir, ?FUNCTION_NAME)},
|
||||
{certfile, client_pem_file(DataDir, ?FUNCTION_NAME)}
|
||||
], 1000),
|
||||
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),
|
||||
Options = [{ssl_options, [ {verify_peer_ext_key_usage, "codeSigning,clientAuth"}
|
||||
| ?config(ssl_config, Config)
|
||||
]}],
|
||||
emqx_listeners:start_listener(ssl, Port, Options),
|
||||
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")},
|
||||
{certfile, filename:join(DataDir, "client1.pem")}
|
||||
], 1000),
|
||||
fail_when_no_ssl_alert(Socket, 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}
|
||||
].
|
||||
|
|
@ -89,8 +89,9 @@ gen_host_cert(H, CaName, Path, Opts) ->
|
|||
HEXT,
|
||||
"keyUsage=digitalSignature,keyAgreement,keyCertSign\n"
|
||||
"basicConstraints=CA:TRUE \n"
|
||||
"~s \n"
|
||||
"subjectAltName=DNS:~s\n",
|
||||
[CN]
|
||||
[maps:get(ext, Opts, ""), CN]
|
||||
),
|
||||
|
||||
CSR_Cmd = csr_cmd(PasswordArg, ECKeyFile, HKey, HCSR, CN),
|
||||
|
|
Loading…
Reference in New Issue