Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(TLS): veriy client cert keyusage #10669

Merged
merged 6 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions changes/v4.4.19-en.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
- Adds a new feature to enable partial certificate chain validation for TLS listeners[#10553](https://github.com/emqx/emqx/pull/10553).
If partial_chain is set to 'true', the last certificate in cacertfile is treated as the terminal of the certificate trust-chain. That is, the TLS handshake does not require full trust-chain, and EMQX will not try to validate the chain all the way up to the root CA.

- Adds a new feature to enable client certificate extended key usage validation for TLS listeners[#10669](https://github.com/emqx/emqx/pull/10669).

## Bug fixes

- Fixed an issue where the rule engine was unable to access variables exported by `FOREACH` in the `DO` clause [#10620](https://github.com/emqx/emqx/pull/10620).
Expand Down
2 changes: 2 additions & 0 deletions changes/v4.4.19-zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

- 增加了一个新的功能,为 TLS 监听器启用部分证书链验证[#10553](https://github.com/emqx/emqx/pull/10553)。
如果 partial_chain 设置为“true”,cacertfile 中的最后一个证书将被视为证书信任链的顶端证书。 也就是说,TLS 握手不需要完整的链,并且 EMQX 不会尝试一直验证链直到根 CA。

- 增加了一个新功能,为 TLS 监听器启用客户端证书扩展密钥使用验证 [#10669](https://github.com/emqx/emqx/pull/10669)。

## 修复

Expand Down
6 changes: 6 additions & 0 deletions priv/emqx.schema
Original file line number Diff line number Diff line change
Expand Up @@ -1650,6 +1650,11 @@ end}.
{datatype, atom}
]}.

{mapping, "listener.ssl.$name.verify_peer_ext_key_usage", "emqx.listeners", [
{datatype, string},
{default, undefined}
]}.

{mapping, "listener.ssl.$name.fail_if_no_peer_cert", "emqx.listeners", [
{datatype, {enum, [true, false]}}
]}.
Expand Down Expand Up @@ -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)},
Expand Down
72 changes: 72 additions & 0 deletions src/emqx_const_v2.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -43,3 +45,73 @@ 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 verify_fun_peer_extKeyUsage/3, AllowedKeyUsages}.

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, %% valid peer cert
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, AllowedKeyUsages};
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(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).
3 changes: 2 additions & 1 deletion src/emqx_listeners.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
22 changes: 22 additions & 0 deletions src/emqx_tls_lib.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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)))).
Expand Down Expand Up @@ -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) ->
Expand All @@ -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 ->
VerifyFun = emqx_const_v2:make_tls_verify_fun(verify_cert_extKeyUsage, V),
replace(SslOpts, verify_fun, VerifyFun)
end.

replace(Opts, Key, Value) -> [{Key, Value} | proplists:delete(Key, Opts)].

%% @doc Helper, make TLS root_fun
Expand Down