feat(tls): port partial_chain, part 1
This commit is contained in:
parent
002dc8541b
commit
0b95a08d32
|
@ -0,0 +1,126 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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.
|
||||||
|
%%
|
||||||
|
%% @doc Never update this module, create a v3 instead.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-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.
|
||||||
|
%% @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) ->
|
||||||
|
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 cert
|
||||||
|
valid_peer,
|
||||||
|
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
|
||||||
|
).
|
|
@ -610,7 +610,9 @@ esockd_opts(ListenerId, Type, Name, Opts0) ->
|
||||||
ssl ->
|
ssl ->
|
||||||
OptsWithCRL = inject_crl_config(Opts0),
|
OptsWithCRL = inject_crl_config(Opts0),
|
||||||
OptsWithSNI = inject_sni_fun(ListenerId, OptsWithCRL),
|
OptsWithSNI = inject_sni_fun(ListenerId, OptsWithCRL),
|
||||||
SSLOpts = ssl_opts(OptsWithSNI),
|
OptsWithRootFun = inject_root_fun(OptsWithSNI),
|
||||||
|
OptsWithVerifyFun = inject_verify_fun(OptsWithRootFun),
|
||||||
|
SSLOpts = ssl_opts(OptsWithVerifyFun),
|
||||||
Opts3#{ssl_options => SSLOpts, tcp_options => tcp_opts(Opts0)}
|
Opts3#{ssl_options => SSLOpts, tcp_options => tcp_opts(Opts0)}
|
||||||
end
|
end
|
||||||
).
|
).
|
||||||
|
@ -956,6 +958,16 @@ quic_listener_optional_settings() ->
|
||||||
stateless_operation_expiration_ms
|
stateless_operation_expiration_ms
|
||||||
].
|
].
|
||||||
|
|
||||||
|
inject_root_fun(#{ssl_options := SslOpts} = Opts) ->
|
||||||
|
Opts#{ssl_options := emqx_tls_lib:opt_partial_chain(SslOpts)};
|
||||||
|
inject_root_fun(Opts) ->
|
||||||
|
Opts.
|
||||||
|
|
||||||
|
inject_verify_fun(#{ssl_options := SslOpts} = Opts) ->
|
||||||
|
Opts#{ssl_options := emqx_tls_lib:opt_verify_fun(SslOpts)};
|
||||||
|
inject_verify_fun(Opts) ->
|
||||||
|
Opts.
|
||||||
|
|
||||||
inject_sni_fun(ListenerId, Conf = #{ssl_options := #{ocsp := #{enable_ocsp_stapling := true}}}) ->
|
inject_sni_fun(ListenerId, Conf = #{ssl_options := #{ocsp := #{enable_ocsp_stapling := true}}}) ->
|
||||||
emqx_ocsp_cache:inject_sni_fun(ListenerId, Conf);
|
emqx_ocsp_cache:inject_sni_fun(ListenerId, Conf);
|
||||||
inject_sni_fun(_ListenerId, Conf) ->
|
inject_sni_fun(_ListenerId, Conf) ->
|
||||||
|
|
|
@ -23,6 +23,8 @@
|
||||||
default_ciphers/0,
|
default_ciphers/0,
|
||||||
selected_ciphers/1,
|
selected_ciphers/1,
|
||||||
integral_ciphers/2,
|
integral_ciphers/2,
|
||||||
|
opt_partial_chain/1,
|
||||||
|
opt_verify_fun/1,
|
||||||
all_ciphers_set_cached/0
|
all_ciphers_set_cached/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
@ -679,3 +681,55 @@ ensure_ssl_file_key(SSL, RequiredKeyPaths) ->
|
||||||
[] -> ok;
|
[] -> ok;
|
||||||
Miss -> {error, #{reason => ssl_file_option_not_found, which_options => Miss}}
|
Miss -> {error, #{reason => ssl_file_option_not_found, which_options => Miss}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%% @doc enable TLS partial_chain validation if set.
|
||||||
|
-spec opt_partial_chain(SslOpts :: map()) -> NewSslOpts :: map().
|
||||||
|
opt_partial_chain(#{partial_chain := false} = SslOpts) ->
|
||||||
|
maps:remove(partial_chain, SslOpts);
|
||||||
|
opt_partial_chain(#{partial_chain := true} = SslOpts) ->
|
||||||
|
SslOpts#{partial_chain := rootfun_trusted_ca_from_cacertfile(1, SslOpts)};
|
||||||
|
opt_partial_chain(#{partial_chain := cacert_from_cacertfile} = SslOpts) ->
|
||||||
|
SslOpts#{partial_chain := rootfun_trusted_ca_from_cacertfile(1, SslOpts)};
|
||||||
|
opt_partial_chain(#{partial_chain := two_cacerts_from_cacertfile} = SslOpts) ->
|
||||||
|
SslOpts#{partial_chain := rootfun_trusted_ca_from_cacertfile(2, SslOpts)};
|
||||||
|
opt_partial_chain(SslOpts) ->
|
||||||
|
SslOpts.
|
||||||
|
|
||||||
|
%% @doc make verify_fun if set.
|
||||||
|
-spec opt_verify_fun(SslOpts :: map()) -> NewSslOpts :: map().
|
||||||
|
opt_verify_fun(#{verify_peer_ext_key_usage := V} = SslOpts) ->
|
||||||
|
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).
|
||||||
|
|
Loading…
Reference in New Issue