diff --git a/apps/emqx/i18n/emqx_schema_i18n.conf b/apps/emqx/i18n/emqx_schema_i18n.conf index fb458b449..5a48c218a 100644 --- a/apps/emqx/i18n/emqx_schema_i18n.conf +++ b/apps/emqx/i18n/emqx_schema_i18n.conf @@ -1753,6 +1753,63 @@ server_ssl_opts_schema_gc_after_handshake { } } +server_ssl_opts_schema_enable_ocsp_stapling { + desc { + en: "Whether to enable Online Certificate Status Protocol (OCSP) stapling for the listener." + " If set to true, requires defining the OCSP responder URL and issuer PEM path." + zh: "是否为监听器启用 OCSP Stapling 功能。 如果设置为 true," + "需要定义 OCSP Responder 的 URL 和证书签发者的 PEM 文件路径。" + } + label: { + en: "Enable OCSP Stapling" + zh: "启用 OCSP Stapling" + } +} + +server_ssl_opts_schema_ocsp_responder_url { + desc { + en: "URL for the OCSP responder to check the server certificate against." + zh: "用于检查服务器证书的 OCSP Responder 的 URL。" + } + label: { + en: "OCSP Responder URL" + zh: "OCSP Responder 的 URL" + } +} + +server_ssl_opts_schema_ocsp_issuer_pem { + desc { + en: "PEM-encoded certificate of the OCSP issuer for the server certificate." + zh: "服务器证书的 OCSP 签发者的 PEM 编码证书。" + } + label: { + en: "OCSP Issuer Certificate" + zh: "OCSP 签发者证书" + } +} + +server_ssl_opts_schema_ocsp_refresh_interval { + desc { + en: "The period to refresh the OCSP response for the server." + zh: "为服务器刷新OCSP响应的周期。" + } + label: { + en: "OCSP Refresh Interval" + zh: "OCSP 刷新间隔" + } +} + +server_ssl_opts_schema_ocsp_refresh_http_timeout { + desc { + en: "The timeout for the HTTP request when checking OCSP responses." + zh: "检查 OCSP 响应时,HTTP 请求的超时。" + } + label: { + en: "OCSP Refresh HTTP Timeout" + zh: "OCSP 刷新 HTTP 超时" + } +} + fields_listeners_tcp { desc { en: """TCP listeners.""" diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 4b2c2a974..b62ca6b3c 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -30,6 +30,7 @@ {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.14.5"}}}, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}}, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.37.0"}}}, + {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.5.2"}}}, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}, {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}}, {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "1.0.0"}}} @@ -43,7 +44,7 @@ {meck, "0.9.2"}, {proper, "1.4.0"}, {bbmustache, "1.10.0"}, - {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.8.2"}}} + {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.8.5"}}} ]}, {extra_src_dirs, [{"test", [recursive]}]} ]} diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index b76234e85..bf3134568 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -87,6 +87,10 @@ remove_handlers/0 ]). +-ifdef(TEST). +-export([erase_schema_mod_and_names/0]). +-endif. + -include("logger.hrl"). -include_lib("hocon/include/hoconsc.hrl"). @@ -501,6 +505,11 @@ save_schema_mod_and_names(SchemaMod) -> names => lists:usort(OldNames ++ RootNames) }). +-ifdef(TEST). +erase_schema_mod_and_names() -> + persistent_term:erase(?PERSIS_SCHEMA_MODS). +-endif. + -spec get_schema_mod() -> #{binary() => atom()}. get_schema_mod() -> maps:get(mods, persistent_term:get(?PERSIS_SCHEMA_MODS, #{mods => #{}})). diff --git a/apps/emqx/src/emqx_const_v1.erl b/apps/emqx/src/emqx_const_v1.erl new file mode 100644 index 000000000..aef4d5101 --- /dev/null +++ b/apps/emqx/src/emqx_const_v1.erl @@ -0,0 +1,24 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2022-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 v2 instead. +%%-------------------------------------------------------------------- + +-module(emqx_const_v1). + +-export([make_sni_fun/1]). + +make_sni_fun(ListenerID) -> + fun(SN) -> emqx_ocsp_cache:sni_fun(SN, ListenerID) end. diff --git a/apps/emqx/src/emqx_kernel_sup.erl b/apps/emqx/src/emqx_kernel_sup.erl index a69674de8..9d2f71068 100644 --- a/apps/emqx/src/emqx_kernel_sup.erl +++ b/apps/emqx/src/emqx_kernel_sup.erl @@ -35,7 +35,8 @@ init([]) -> child_spec(emqx_hooks, worker), child_spec(emqx_stats, worker), child_spec(emqx_metrics, worker), - child_spec(emqx_authn_authz_metrics_sup, supervisor) + child_spec(emqx_authn_authz_metrics_sup, supervisor), + child_spec(emqx_ocsp_cache, worker) ] }}. diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 6982b3dea..97bc15ad3 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -484,8 +484,12 @@ esockd_opts(ListenerId, Type, Opts0) -> }, maps:to_list( case Type of - tcp -> Opts3#{tcp_options => tcp_opts(Opts0)}; - ssl -> Opts3#{ssl_options => ssl_opts(Opts0), tcp_options => tcp_opts(Opts0)} + tcp -> + Opts3#{tcp_options => tcp_opts(Opts0)}; + ssl -> + OptsWithSNI = inject_sni_fun(ListenerId, Opts0), + SSLOpts = ssl_opts(OptsWithSNI), + Opts3#{ssl_options => SSLOpts, tcp_options => tcp_opts(Opts0)} end ). @@ -785,3 +789,8 @@ quic_listener_optional_settings() -> max_binding_stateless_operations, stateless_operation_expiration_ms ]. + +inject_sni_fun(ListenerId, Conf = #{ssl_options := #{ocsp := #{enable_ocsp_stapling := true}}}) -> + emqx_ocsp_cache:inject_sni_fun(ListenerId, Conf); +inject_sni_fun(_ListenerId, Conf) -> + Conf. diff --git a/apps/emqx/src/emqx_ocsp_cache.erl b/apps/emqx/src/emqx_ocsp_cache.erl new file mode 100644 index 000000000..4e7ada044 --- /dev/null +++ b/apps/emqx/src/emqx_ocsp_cache.erl @@ -0,0 +1,532 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2022-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 EMQX OCSP cache. +%%-------------------------------------------------------------------- + +-module(emqx_ocsp_cache). + +-include("logger.hrl"). +-include_lib("public_key/include/public_key.hrl"). +-include_lib("ssl/src/ssl_handshake.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). + +-behaviour(gen_server). + +-export([ + start_link/0, + sni_fun/2, + fetch_response/1, + register_listener/2, + inject_sni_fun/2 +]). + +%% gen_server API +-export([ + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + code_change/3 +]). + +%% internal export; only for mocking in tests +-export([http_get/2]). + +-define(CACHE_TAB, ?MODULE). +-define(CALL_TIMEOUT, 20_000). +-define(RETRY_TIMEOUT, 5_000). +-define(REFRESH_TIMER(LID), {refresh_timer, LID}). +-ifdef(TEST). +-define(MIN_REFRESH_INTERVAL, timer:seconds(5)). +-else. +-define(MIN_REFRESH_INTERVAL, timer:minutes(1)). +-endif. + +%% Allow usage of OTP certificate record fields (camelCase). +-elvis([ + {elvis_style, atom_naming_convention, #{ + regex => "^([a-z][a-z0-9]*_?)([a-zA-Z0-9]*_?)*$", + enclosed_atoms => ".*" + }} +]). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +sni_fun(_ServerName, ListenerID) -> + Res = + try + fetch_response(ListenerID) + catch + _:_ -> error + end, + case Res of + {ok, Response} -> + [ + {certificate_status, #certificate_status{ + status_type = ?CERTIFICATE_STATUS_TYPE_OCSP, + response = Response + }} + ]; + error -> + [] + end. + +fetch_response(ListenerID) -> + case do_lookup(ListenerID) of + {ok, DERResponse} -> + {ok, DERResponse}; + {error, invalid_listener_id} -> + error; + {error, not_cached} -> + ?tp(ocsp_cache_miss, #{listener_id => ListenerID}), + ?SLOG(debug, #{ + msg => "fetching_new_ocsp_response", + listener_id => ListenerID + }), + http_fetch(ListenerID) + end. + +register_listener(ListenerID, Opts) -> + gen_server:call(?MODULE, {register_listener, ListenerID, Opts}, ?CALL_TIMEOUT). + +-spec inject_sni_fun(emqx_listeners:listener_id(), map()) -> map(). +inject_sni_fun(ListenerID, Conf0) -> + SNIFun = emqx_const_v1:make_sni_fun(ListenerID), + Conf = emqx_map_lib:deep_merge(Conf0, #{ssl_options => #{sni_fun => SNIFun}}), + ok = ?MODULE:register_listener(ListenerID, Conf), + Conf. + +%%-------------------------------------------------------------------- +%% gen_server behaviour +%%-------------------------------------------------------------------- + +init(_Args) -> + logger:set_process_metadata(#{domain => [emqx, ocsp, cache]}), + emqx_tables:new(?CACHE_TAB, [ + named_table, + public, + {heir, whereis(emqx_kernel_sup), none}, + {read_concurrency, true} + ]), + ?tp(ocsp_cache_init, #{}), + {ok, #{}}. + +handle_call({http_fetch, ListenerID}, _From, State) -> + case do_lookup(ListenerID) of + {ok, DERResponse} -> + {reply, {ok, DERResponse}, State}; + {error, invalid_listener_id} -> + {reply, error, State}; + {error, not_cached} -> + Conf = undefined, + with_refresh_params(ListenerID, Conf, {reply, error, State}, fun(Params) -> + case do_http_fetch_and_cache(ListenerID, Params) of + error -> {reply, error, ensure_timer(ListenerID, State, ?RETRY_TIMEOUT)}; + {ok, Response} -> {reply, {ok, Response}, ensure_timer(ListenerID, State)} + end + end) + end; +handle_call({register_listener, ListenerID, Conf}, _From, State0) -> + ?SLOG(debug, #{ + msg => "registering_ocsp_cache", + listener_id => ListenerID + }), + RefreshInterval0 = emqx_map_lib:deep_get([ssl_options, ocsp, refresh_interval], Conf), + RefreshInterval = max(RefreshInterval0, ?MIN_REFRESH_INTERVAL), + State = State0#{{refresh_interval, ListenerID} => RefreshInterval}, + %% we need to pass the config along because this might be called + %% during the listener's `post_config_update', hence the config is + %% not yet "commited" and accessible when we need it. + Message = {refresh, ListenerID, Conf}, + {reply, ok, ensure_timer(ListenerID, Message, State, 0)}; +handle_call(Call, _From, State) -> + {reply, {error, {unknown_call, Call}}, State}. + +handle_cast(_Cast, State) -> + {noreply, State}. + +handle_info({timeout, TRef, {refresh, ListenerID}}, State0) -> + case maps:get(?REFRESH_TIMER(ListenerID), State0, undefined) of + TRef -> + ?tp(ocsp_refresh_timer, #{listener_id => ListenerID}), + ?SLOG(debug, #{ + msg => "refreshing_ocsp_response", + listener_id => ListenerID + }), + Conf = undefined, + handle_refresh(ListenerID, Conf, State0); + _ -> + {noreply, State0} + end; +handle_info({timeout, TRef, {refresh, ListenerID, Conf}}, State0) -> + case maps:get(?REFRESH_TIMER(ListenerID), State0, undefined) of + TRef -> + ?tp(ocsp_refresh_timer, #{listener_id => ListenerID}), + ?SLOG(debug, #{ + msg => "refreshing_ocsp_response", + listener_id => ListenerID + }), + handle_refresh(ListenerID, Conf, State0); + _ -> + {noreply, State0} + end; +handle_info(_Info, State) -> + {noreply, State}. + +code_change(_Vsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% internal functions +%%-------------------------------------------------------------------- + +http_fetch(ListenerID) -> + %% TODO: configurable call timeout? + gen_server:call(?MODULE, {http_fetch, ListenerID}, ?CALL_TIMEOUT). + +with_listener_config(ListenerID, ConfPath, ErrorResp, Fn) -> + case emqx_listeners:parse_listener_id(ListenerID) of + {ok, #{type := Type, name := Name}} -> + case emqx_config:get_listener_conf(Type, Name, ConfPath, not_found) of + not_found -> + ?SLOG(error, #{ + msg => "listener_config_missing", + listener_id => ListenerID + }), + ErrorResp; + Config -> + Fn(Config) + end; + _Err -> + ?SLOG(error, #{ + msg => "listener_id_not_found", + listener_id => ListenerID + }), + ErrorResp + end. + +cache_key(ListenerID) -> + with_listener_config(ListenerID, [ssl_options], error, fun + (#{certfile := ServerCertPemPath}) -> + #'Certificate'{ + tbsCertificate = + #'TBSCertificate'{ + signature = Signature + } + } = read_server_cert(ServerCertPemPath), + {ok, {ocsp_response, Signature}}; + (OtherConfig) -> + ?SLOG(error, #{ + msg => "listener_config_inconsistent", + listener_id => ListenerID, + config => OtherConfig + }), + error + end). + +do_lookup(ListenerID) -> + CacheKey = cache_key(ListenerID), + case CacheKey of + error -> + {error, invalid_listener_id}; + {ok, Key} -> + %% Respond immediately if a concurrent call already fetched it. + case ets:lookup(?CACHE_TAB, Key) of + [{_, DERResponse}] -> + ?tp(ocsp_cache_hit, #{listener_id => ListenerID}), + {ok, DERResponse}; + [] -> + {error, not_cached} + end + end. + +read_server_cert(ServerCertPemPath0) -> + ServerCertPemPath = to_bin(ServerCertPemPath0), + case ets:lookup(ssl_pem_cache, ServerCertPemPath) of + [{_, [{'Certificate', ServerCertDer, _} | _]}] -> + public_key:der_decode('Certificate', ServerCertDer); + [] -> + case file:read_file(ServerCertPemPath) of + {ok, ServerCertPem} -> + [{'Certificate', ServerCertDer, _} | _] = + public_key:pem_decode(ServerCertPem), + public_key:der_decode('Certificate', ServerCertDer); + {error, Error1} -> + error({bad_server_cert_file, Error1}) + end + end. + +handle_refresh(ListenerID, Conf, State0) -> + %% no point in retrying if the config is inconsistent or non + %% existent. + State1 = maps:without([{refresh_interval, ListenerID}, ?REFRESH_TIMER(ListenerID)], State0), + with_refresh_params(ListenerID, Conf, {noreply, State1}, fun(Params) -> + case do_http_fetch_and_cache(ListenerID, Params) of + error -> + ?SLOG(debug, #{ + msg => "failed_to_fetch_ocsp_response", + listener_id => ListenerID + }), + {noreply, ensure_timer(ListenerID, State0, ?RETRY_TIMEOUT)}; + {ok, _Response} -> + ?SLOG(debug, #{ + msg => "fetched_ocsp_response", + listener_id => ListenerID + }), + {noreply, ensure_timer(ListenerID, State0)} + end + end). + +with_refresh_params(ListenerID, Conf, ErrorRet, Fn) -> + case get_refresh_params(ListenerID, Conf) of + error -> + ErrorRet; + {ok, Params} -> + try + Fn(Params) + catch + Kind:Error -> + ?SLOG(error, #{ + msg => "error_fetching_ocsp_response", + listener_id => ListenerID, + error => {Kind, Error} + }), + ErrorRet + end + end. + +get_refresh_params(ListenerID, undefined = _Conf) -> + %% during normal periodic refreshes, we read from the emqx config. + with_listener_config(ListenerID, [ssl_options], error, fun + ( + #{ + ocsp := #{ + issuer_pem := IssuerPemPath, + responder_url := ResponderURL, + refresh_http_timeout := HTTPTimeout + }, + certfile := ServerCertPemPath + } + ) -> + {ok, #{ + issuer_pem => IssuerPemPath, + responder_url => ResponderURL, + refresh_http_timeout => HTTPTimeout, + server_certfile => ServerCertPemPath + }}; + (OtherConfig) -> + ?SLOG(error, #{ + msg => "listener_config_inconsistent", + listener_id => ListenerID, + config => OtherConfig + }), + error + end); +get_refresh_params(_ListenerID, #{ + ssl_options := #{ + ocsp := #{ + issuer_pem := IssuerPemPath, + responder_url := ResponderURL, + refresh_http_timeout := HTTPTimeout + }, + certfile := ServerCertPemPath + } +}) -> + {ok, #{ + issuer_pem => IssuerPemPath, + responder_url => ResponderURL, + refresh_http_timeout => HTTPTimeout, + server_certfile => ServerCertPemPath + }}; +get_refresh_params(_ListenerID, _Conf) -> + error. + +do_http_fetch_and_cache(ListenerID, Params) -> + #{ + issuer_pem := IssuerPemPath, + responder_url := ResponderURL, + refresh_http_timeout := HTTPTimeout, + server_certfile := ServerCertPemPath + } = Params, + IssuerPem = + case file:read_file(IssuerPemPath) of + {ok, IssuerPem0} -> IssuerPem0; + {error, Error0} -> error({bad_issuer_pem_file, Error0}) + end, + ServerCert = read_server_cert(ServerCertPemPath), + Request = build_ocsp_request(IssuerPem, ServerCert), + ?tp(ocsp_http_fetch, #{ + listener_id => ListenerID, + responder_url => ResponderURL, + timeout => HTTPTimeout + }), + RequestURI = iolist_to_binary([ResponderURL, Request]), + Resp = ?MODULE:http_get(RequestURI, HTTPTimeout), + case Resp of + {ok, {{_, 200, _}, _, Body}} -> + ?SLOG(debug, #{ + msg => "caching_ocsp_response", + listener_id => ListenerID + }), + %% if we got this far, the certfile is correct. + {ok, CacheKey} = cache_key(ListenerID), + true = ets:insert(?CACHE_TAB, {CacheKey, Body}), + ?tp(ocsp_http_fetch_and_cache, #{ + listener_id => ListenerID, + headers => true + }), + {ok, Body}; + {ok, {200, Body}} -> + ?SLOG(debug, #{ + msg => "caching_ocsp_response", + listener_id => ListenerID + }), + %% if we got this far, the certfile is correct. + {ok, CacheKey} = cache_key(ListenerID), + true = ets:insert(?CACHE_TAB, {CacheKey, Body}), + ?tp(ocsp_http_fetch_and_cache, #{ + listener_id => ListenerID, + headers => false + }), + {ok, Body}; + {ok, {{_, Code, _}, _, Body}} -> + ?tp( + error, + ocsp_http_fetch_bad_code, + #{ + listener_id => ListenerID, + body => Body, + code => Code, + headers => true + } + ), + ?SLOG(error, #{ + msg => "error_fetching_ocsp_response", + listener_id => ListenerID, + code => Code, + body => Body + }), + error; + {ok, {Code, Body}} -> + ?tp( + error, + ocsp_http_fetch_bad_code, + #{ + listener_id => ListenerID, + body => Body, + code => Code, + headers => false + } + ), + ?SLOG(error, #{ + msg => "error_fetching_ocsp_response", + listener_id => ListenerID, + code => Code, + body => Body + }), + error; + {error, Error} -> + ?tp( + error, + ocsp_http_fetch_error, + #{ + listener_id => ListenerID, + error => Error + } + ), + ?SLOG(error, #{ + msg => "error_fetching_ocsp_response", + listener_id => ListenerID, + error => Error + }), + error + end. + +http_get(URL, HTTPTimeout) -> + httpc:request( + get, + {URL, [{"connection", "close"}]}, + [{timeout, HTTPTimeout}], + [{body_format, binary}] + ). + +ensure_timer(ListenerID, State) -> + Timeout = maps:get({refresh_interval, ListenerID}, State, timer:minutes(5)), + ensure_timer(ListenerID, State, Timeout). + +ensure_timer(ListenerID, State, Timeout) -> + ensure_timer(ListenerID, {refresh, ListenerID}, State, Timeout). + +ensure_timer(ListenerID, Message, State, Timeout) -> + emqx_misc:cancel_timer(maps:get(?REFRESH_TIMER(ListenerID), State, undefined)), + State#{ + ?REFRESH_TIMER(ListenerID) => emqx_misc:start_timer( + Timeout, + Message + ) + }. + +build_ocsp_request(IssuerPem, ServerCert) -> + [{'Certificate', IssuerDer, _} | _] = public_key:pem_decode(IssuerPem), + #'Certificate'{ + tbsCertificate = + #'TBSCertificate'{ + serialNumber = SerialNumber, + issuer = Issuer + } + } = ServerCert, + #'Certificate'{ + tbsCertificate = + #'TBSCertificate'{ + subjectPublicKeyInfo = + #'SubjectPublicKeyInfo'{subjectPublicKey = IssuerPublicKeyDer} + } + } = public_key:der_decode('Certificate', IssuerDer), + IssuerDNHash = crypto:hash(sha, public_key:der_encode('Name', Issuer)), + IssuerPKHash = crypto:hash(sha, IssuerPublicKeyDer), + Req = #'OCSPRequest'{ + tbsRequest = + #'TBSRequest'{ + version = 0, + requestList = + [ + #'Request'{ + reqCert = + #'CertID'{ + hashAlgorithm = + #'AlgorithmIdentifier'{ + algorithm = ?'id-sha1', + %% ??? + parameters = <<5, 0>> + }, + issuerNameHash = IssuerDNHash, + issuerKeyHash = IssuerPKHash, + serialNumber = SerialNumber + } + } + ] + } + }, + ReqDer = public_key:der_encode('OCSPRequest', Req), + base64:encode_to_string(ReqDer). + +to_bin(Str) when is_list(Str) -> list_to_binary(Str); +to_bin(Bin) when is_binary(Bin) -> Bin. diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 6f935f1e5..275a9592e 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -43,6 +43,7 @@ -type cipher() :: map(). -type port_number() :: 1..65536. -type server_parse_option() :: #{default_port => port_number(), no_port => boolean()}. +-type url() :: binary(). -typerefl_from_string({duration/0, emqx_schema, to_duration}). -typerefl_from_string({duration_s/0, emqx_schema, to_duration_s}). @@ -56,6 +57,7 @@ -typerefl_from_string({ip_port/0, emqx_schema, to_ip_port}). -typerefl_from_string({cipher/0, emqx_schema, to_erl_cipher_suite}). -typerefl_from_string({comma_separated_atoms/0, emqx_schema, to_comma_separated_atoms}). +-typerefl_from_string({url/0, emqx_schema, to_url}). -export([ validate_heap_size/1, @@ -81,7 +83,8 @@ to_bar_separated_list/1, to_ip_port/1, to_erl_cipher_suite/1, - to_comma_separated_atoms/1 + to_comma_separated_atoms/1, + to_url/1 ]). -export([ @@ -108,7 +111,8 @@ bar_separated_list/0, ip_port/0, cipher/0, - comma_separated_atoms/0 + comma_separated_atoms/0, + url/0 ]). -export([namespace/0, roots/0, roots/1, fields/1, desc/1, tags/0]). @@ -810,7 +814,7 @@ fields("mqtt_ssl_listener") -> {"ssl_options", sc( ref("listener_ssl_opts"), - #{} + #{validator => fun mqtt_ssl_listener_ssl_options_validator/1} )} ]; fields("mqtt_ws_listener") -> @@ -1294,6 +1298,49 @@ fields("listener_quic_ssl_opts") -> ); fields("ssl_client_opts") -> client_ssl_opts_schema(#{}); +fields("ocsp") -> + [ + {"enable_ocsp_stapling", + sc( + boolean(), + #{ + default => false, + desc => ?DESC("server_ssl_opts_schema_enable_ocsp_stapling") + } + )}, + {"responder_url", + sc( + url(), + #{ + required => false, + desc => ?DESC("server_ssl_opts_schema_ocsp_responder_url") + } + )}, + {"issuer_pem", + sc( + binary(), + #{ + required => false, + desc => ?DESC("server_ssl_opts_schema_ocsp_issuer_pem") + } + )}, + {"refresh_interval", + sc( + duration(), + #{ + default => <<"5m">>, + desc => ?DESC("server_ssl_opts_schema_ocsp_refresh_interval") + } + )}, + {"refresh_http_timeout", + sc( + duration(), + #{ + default => <<"15s">>, + desc => ?DESC("server_ssl_opts_schema_ocsp_refresh_http_timeout") + } + )} + ]; fields("deflate_opts") -> [ {"level", @@ -2017,6 +2064,8 @@ desc("trace") -> "Real-time filtering logs for the ClientID or Topic or IP for debugging."; desc("shared_subscription_group") -> "Per group dispatch strategy for shared subscription"; +desc("ocsp") -> + "Per listener OCSP Stapling configuration."; desc(_) -> undefined. @@ -2199,14 +2248,62 @@ server_ssl_opts_schema(Defaults, IsRanchListener) -> )} ] ++ [ - {"gc_after_handshake", - sc(boolean(), #{ - default => false, - desc => ?DESC(server_ssl_opts_schema_gc_after_handshake) - })} - || not IsRanchListener + Field + || not IsRanchListener, + Field <- [ + {"gc_after_handshake", + sc(boolean(), #{ + default => false, + desc => ?DESC(server_ssl_opts_schema_gc_after_handshake) + })}, + {"ocsp", + sc( + ref("ocsp"), + #{ + required => false, + validator => fun ocsp_inner_validator/1 + } + )} + ] ]. +mqtt_ssl_listener_ssl_options_validator(Conf) -> + Checks = [ + fun ocsp_outer_validator/1 + ], + case emqx_misc:pipeline(Checks, Conf, not_used) of + {ok, _, _} -> + ok; + {error, Reason, _NotUsed} -> + {error, Reason} + end. + +ocsp_outer_validator(#{<<"ocsp">> := #{<<"enable_ocsp_stapling">> := true}} = Conf) -> + %% outer mqtt listener ssl server config + ServerCertPemPath = maps:get(<<"certfile">>, Conf, undefined), + case ServerCertPemPath of + undefined -> + {error, "Server certificate must be defined when using OCSP stapling"}; + _ -> + %% check if issuer pem is readable and/or valid? + ok + end; +ocsp_outer_validator(_Conf) -> + ok. + +ocsp_inner_validator(#{enable_ocsp_stapling := _} = Conf) -> + ocsp_inner_validator(emqx_map_lib:binary_key_map(Conf)); +ocsp_inner_validator(#{<<"enable_ocsp_stapling">> := false} = _Conf) -> + ok; +ocsp_inner_validator(#{<<"enable_ocsp_stapling">> := true} = Conf) -> + assert_required_field( + Conf, <<"responder_url">>, "The responder URL is required for OCSP stapling" + ), + assert_required_field( + Conf, <<"issuer_pem">>, "The issuer PEM path is required for OCSP stapling" + ), + ok. + %% @doc Make schema for SSL client. -spec client_ssl_opts_schema(map()) -> hocon_schema:field_schema(). client_ssl_opts_schema(Defaults) -> @@ -2408,6 +2505,15 @@ to_comma_separated_binary(Str) -> to_comma_separated_atoms(Str) -> {ok, lists:map(fun to_atom/1, string:tokens(Str, ", "))}. +to_url(Str) -> + case emqx_http_lib:uri_parse(Str) of + {ok, URIMap} -> + URIString = emqx_http_lib:normalize(URIMap), + {ok, iolist_to_binary(URIString)}; + Error -> + Error + end. + to_bar_separated_list(Str) -> {ok, string:tokens(Str, "| ")}. @@ -2865,3 +2971,11 @@ is_quic_ssl_opts(Name) -> %% , "handshake_timeout" %% , "gc_after_handshake" ]). + +assert_required_field(Conf, Key, ErrorMessage) -> + case maps:get(Key, Conf, undefined) of + undefined -> + throw(ErrorMessage); + _ -> + ok + end. diff --git a/apps/emqx/src/emqx_tls_lib.erl b/apps/emqx/src/emqx_tls_lib.erl index eb6091f29..47797b326 100644 --- a/apps/emqx/src/emqx_tls_lib.erl +++ b/apps/emqx/src/emqx_tls_lib.erl @@ -47,8 +47,18 @@ -define(IS_TRUE(Val), ((Val =:= true) orelse (Val =:= <<"true">>))). -define(IS_FALSE(Val), ((Val =:= false) orelse (Val =:= <<"false">>))). --define(SSL_FILE_OPT_NAMES, [<<"keyfile">>, <<"certfile">>, <<"cacertfile">>]). --define(SSL_FILE_OPT_NAMES_A, [keyfile, certfile, cacertfile]). +-define(SSL_FILE_OPT_PATHS, [ + [<<"keyfile">>], + [<<"certfile">>], + [<<"cacertfile">>], + [<<"ocsp">>, <<"issuer_pem">>] +]). +-define(SSL_FILE_OPT_PATHS_A, [ + [keyfile], + [certfile], + [cacertfile], + [ocsp, issuer_pem] +]). %% non-empty string -define(IS_STRING(L), (is_list(L) andalso L =/= [] andalso is_integer(hd(L)))). @@ -298,20 +308,20 @@ ensure_ssl_files(Dir, SSL, Opts) -> RequiredKeys = maps:get(required_keys, Opts, []), case ensure_ssl_file_key(SSL, RequiredKeys) of ok -> - Keys = ?SSL_FILE_OPT_NAMES ++ ?SSL_FILE_OPT_NAMES_A, - ensure_ssl_files(Dir, SSL, Keys, Opts); + KeyPaths = ?SSL_FILE_OPT_PATHS ++ ?SSL_FILE_OPT_PATHS_A, + ensure_ssl_files(Dir, SSL, KeyPaths, Opts); {error, _} = Error -> Error end. ensure_ssl_files(_Dir, SSL, [], _Opts) -> {ok, SSL}; -ensure_ssl_files(Dir, SSL, [Key | Keys], Opts) -> - case ensure_ssl_file(Dir, Key, SSL, maps:get(Key, SSL, undefined), Opts) of +ensure_ssl_files(Dir, SSL, [KeyPath | KeyPaths], Opts) -> + case ensure_ssl_file(Dir, KeyPath, SSL, emqx_map_lib:deep_get(KeyPath, SSL, undefined), Opts) of {ok, NewSSL} -> - ensure_ssl_files(Dir, NewSSL, Keys, Opts); + ensure_ssl_files(Dir, NewSSL, KeyPaths, Opts); {error, Reason} -> - {error, Reason#{which_options => [Key]}} + {error, Reason#{which_options => [KeyPath]}} end. %% @doc Compare old and new config, delete the ones in old but not in new. @@ -321,12 +331,12 @@ delete_ssl_files(Dir, NewOpts0, OldOpts0) -> {ok, NewOpts} = ensure_ssl_files(Dir, NewOpts0, #{dry_run => DryRun}), {ok, OldOpts} = ensure_ssl_files(Dir, OldOpts0, #{dry_run => DryRun}), Get = fun - (_K, undefined) -> undefined; - (K, Opts) -> maps:get(K, Opts, undefined) + (_KP, undefined) -> undefined; + (KP, Opts) -> emqx_map_lib:deep_get(KP, Opts, undefined) end, lists:foreach( - fun(Key) -> delete_old_file(Get(Key, NewOpts), Get(Key, OldOpts)) end, - ?SSL_FILE_OPT_NAMES ++ ?SSL_FILE_OPT_NAMES_A + fun(KeyPath) -> delete_old_file(Get(KeyPath, NewOpts), Get(KeyPath, OldOpts)) end, + ?SSL_FILE_OPT_PATHS ++ ?SSL_FILE_OPT_PATHS_A ), %% try to delete the dir if it is empty _ = file:del_dir(pem_dir(Dir)), @@ -346,29 +356,33 @@ delete_old_file(_New, Old) -> ?SLOG(error, #{msg => "failed_to_delete_ssl_file", file_path => Old, reason => Reason}) end. -ensure_ssl_file(_Dir, _Key, SSL, undefined, _Opts) -> +ensure_ssl_file(_Dir, _KeyPath, SSL, undefined, _Opts) -> {ok, SSL}; -ensure_ssl_file(Dir, Key, SSL, MaybePem, Opts) -> +ensure_ssl_file(Dir, KeyPath, SSL, MaybePem, Opts) -> case is_valid_string(MaybePem) of true -> DryRun = maps:get(dry_run, Opts, false), - do_ensure_ssl_file(Dir, Key, SSL, MaybePem, DryRun); + do_ensure_ssl_file(Dir, KeyPath, SSL, MaybePem, DryRun); false -> {error, #{reason => invalid_file_path_or_pem_string}} end. -do_ensure_ssl_file(Dir, Key, SSL, MaybePem, DryRun) -> +do_ensure_ssl_file(Dir, KeyPath, SSL, MaybePem, DryRun) -> case is_pem(MaybePem) of true -> - case save_pem_file(Dir, Key, MaybePem, DryRun) of - {ok, Path} -> {ok, SSL#{Key => Path}}; - {error, Reason} -> {error, Reason} + case save_pem_file(Dir, KeyPath, MaybePem, DryRun) of + {ok, Path} -> + NewSSL = emqx_map_lib:deep_put(KeyPath, SSL, Path), + {ok, NewSSL}; + {error, Reason} -> + {error, Reason} end; false -> case is_valid_pem_file(MaybePem) of true -> {ok, SSL}; - {error, enoent} when DryRun -> {ok, SSL}; + {error, enoent} when DryRun -> + {ok, SSL}; {error, Reason} -> {error, #{ pem_check => invalid_pem, @@ -398,8 +412,8 @@ is_pem(MaybePem) -> %% To make it simple, the file is always overwritten. %% Also a potentially half-written PEM file (e.g. due to power outage) %% can be corrected with an overwrite. -save_pem_file(Dir, Key, Pem, DryRun) -> - Path = pem_file_name(Dir, Key, Pem), +save_pem_file(Dir, KeyPath, Pem, DryRun) -> + Path = pem_file_name(Dir, KeyPath, Pem), case filelib:ensure_dir(Path) of ok when DryRun -> {ok, Path}; @@ -422,11 +436,14 @@ is_generated_file(Filename) -> _ -> false end. -pem_file_name(Dir, Key, Pem) -> +pem_file_name(Dir, KeyPath, Pem) -> <> = crypto:hash(md5, Pem), Suffix = hex_str(CK), - FileName = binary:replace(ensure_bin(Key), <<"file">>, <<"-", Suffix/binary>>), - filename:join([pem_dir(Dir), FileName]). + Segments = lists:map(fun ensure_bin/1, KeyPath), + Filename0 = iolist_to_binary(lists:join(<<"_">>, Segments)), + Filename1 = binary:replace(Filename0, <<"file">>, <<>>), + Filename = <>, + filename:join([pem_dir(Dir), Filename]). pem_dir(Dir) -> filename:join([emqx:mutable_certs_dir(), Dir]). @@ -465,24 +482,26 @@ is_valid_pem_file(Path) -> %% so they are forced to upload a cert file, or use an existing file path. -spec drop_invalid_certs(map()) -> map(). drop_invalid_certs(#{enable := False} = SSL) when ?IS_FALSE(False) -> - maps:without(?SSL_FILE_OPT_NAMES_A, SSL); + lists:foldl(fun emqx_map_lib:deep_remove/2, SSL, ?SSL_FILE_OPT_PATHS_A); drop_invalid_certs(#{<<"enable">> := False} = SSL) when ?IS_FALSE(False) -> - maps:without(?SSL_FILE_OPT_NAMES, SSL); + lists:foldl(fun emqx_map_lib:deep_remove/2, SSL, ?SSL_FILE_OPT_PATHS); drop_invalid_certs(#{enable := True} = SSL) when ?IS_TRUE(True) -> - do_drop_invalid_certs(?SSL_FILE_OPT_NAMES_A, SSL); + do_drop_invalid_certs(?SSL_FILE_OPT_PATHS_A, SSL); drop_invalid_certs(#{<<"enable">> := True} = SSL) when ?IS_TRUE(True) -> - do_drop_invalid_certs(?SSL_FILE_OPT_NAMES, SSL). + do_drop_invalid_certs(?SSL_FILE_OPT_PATHS, SSL). do_drop_invalid_certs([], SSL) -> SSL; -do_drop_invalid_certs([Key | Keys], SSL) -> - case maps:get(Key, SSL, undefined) of +do_drop_invalid_certs([KeyPath | KeyPaths], SSL) -> + case emqx_map_lib:deep_get(KeyPath, SSL, undefined) of undefined -> - do_drop_invalid_certs(Keys, SSL); + do_drop_invalid_certs(KeyPaths, SSL); PemOrPath -> case is_pem(PemOrPath) orelse is_valid_pem_file(PemOrPath) of - true -> do_drop_invalid_certs(Keys, SSL); - {error, _} -> do_drop_invalid_certs(Keys, maps:without([Key], SSL)) + true -> + do_drop_invalid_certs(KeyPaths, SSL); + {error, _} -> + do_drop_invalid_certs(KeyPaths, emqx_map_lib:deep_remove(KeyPath, SSL)) end end. @@ -565,9 +584,10 @@ ensure_bin(A) when is_atom(A) -> atom_to_binary(A, utf8). ensure_ssl_file_key(_SSL, []) -> ok; -ensure_ssl_file_key(SSL, RequiredKeys) -> - Filter = fun(Key) -> not maps:is_key(Key, SSL) end, - case lists:filter(Filter, RequiredKeys) of +ensure_ssl_file_key(SSL, RequiredKeyPaths) -> + NotFoundRef = make_ref(), + Filter = fun(KeyPath) -> NotFoundRef =:= emqx_map_lib:deep_get(KeyPath, SSL, NotFoundRef) end, + case lists:filter(Filter, RequiredKeyPaths) of [] -> ok; Miss -> {error, #{reason => ssl_file_option_not_found, which_options => Miss}} end. diff --git a/apps/emqx/test/emqx_SUITE.erl b/apps/emqx/test/emqx_SUITE.erl index dbe8e09a6..09d5d8017 100644 --- a/apps/emqx/test/emqx_SUITE.erl +++ b/apps/emqx/test/emqx_SUITE.erl @@ -26,6 +26,7 @@ all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> + emqx_common_test_helpers:boot_modules(all), emqx_common_test_helpers:start_apps([]), Config. diff --git a/apps/emqx/test/emqx_common_test_helpers.erl b/apps/emqx/test/emqx_common_test_helpers.erl index ce3a39dcf..c26e63a62 100644 --- a/apps/emqx/test/emqx_common_test_helpers.erl +++ b/apps/emqx/test/emqx_common_test_helpers.erl @@ -16,6 +16,8 @@ -module(emqx_common_test_helpers). +-include("emqx_authentication.hrl"). + -type special_config_handler() :: fun(). -type apps() :: list(atom()). @@ -27,6 +29,7 @@ boot_modules/1, start_apps/1, start_apps/2, + start_apps/3, stop_apps/1, reload/2, app_path/2, @@ -34,7 +37,8 @@ deps_path/2, flush/0, flush/1, - render_and_load_app_config/1 + render_and_load_app_config/1, + render_and_load_app_config/2 ]). -export([ @@ -183,17 +187,21 @@ start_apps(Apps) -> application:set_env(system_monitor, db_hostname, ""), ok end, - start_apps(Apps, DefaultHandler). + start_apps(Apps, DefaultHandler, #{}). -spec start_apps(Apps :: apps(), Handler :: special_config_handler()) -> ok. start_apps(Apps, SpecAppConfig) when is_function(SpecAppConfig) -> + start_apps(Apps, SpecAppConfig, #{}). + +-spec start_apps(Apps :: apps(), Handler :: special_config_handler(), map()) -> ok. +start_apps(Apps, SpecAppConfig, Opts) when is_function(SpecAppConfig) -> %% Load all application code to beam vm first %% Because, minirest, ekka etc.. application will scan these modules lists:foreach(fun load/1, [emqx | Apps]), ok = start_ekka(), mnesia:clear_table(emqx_admin), ok = emqx_ratelimiter_SUITE:load_conf(), - lists:foreach(fun(App) -> start_app(App, SpecAppConfig) end, [emqx | Apps]). + lists:foreach(fun(App) -> start_app(App, SpecAppConfig, Opts) end, [emqx | Apps]). load(App) -> case application:load(App) of @@ -203,27 +211,31 @@ load(App) -> end. render_and_load_app_config(App) -> + render_and_load_app_config(App, #{}). + +render_and_load_app_config(App, Opts) -> load(App), Schema = app_schema(App), - Conf = app_path(App, filename:join(["etc", app_conf_file(App)])), + ConfFilePath = maps:get(conf_file_path, Opts, filename:join(["etc", app_conf_file(App)])), + Conf = app_path(App, ConfFilePath), try - do_render_app_config(App, Schema, Conf) + do_render_app_config(App, Schema, Conf, Opts) catch throw:E:St -> %% turn throw into error error({Conf, E, St}) end. -do_render_app_config(App, Schema, ConfigFile) -> - Vars = mustache_vars(App), +do_render_app_config(App, Schema, ConfigFile, Opts) -> + Vars = mustache_vars(App, Opts), RenderedConfigFile = render_config_file(ConfigFile, Vars), read_schema_configs(Schema, RenderedConfigFile), force_set_config_file_paths(App, [RenderedConfigFile]), copy_certs(App, RenderedConfigFile), ok. -start_app(App, SpecAppConfig) -> - render_and_load_app_config(App), +start_app(App, SpecAppConfig, Opts) -> + render_and_load_app_config(App, Opts), SpecAppConfig(App), case application:ensure_all_started(App) of {ok, _} -> @@ -246,12 +258,13 @@ app_schema(App) -> no_schema end. -mustache_vars(App) -> +mustache_vars(App, Opts) -> + ExtraMustacheVars = maps:get(extra_mustache_vars, Opts, []), [ {platform_data_dir, app_path(App, "data")}, {platform_etc_dir, app_path(App, "etc")}, {platform_log_dir, app_path(App, "log")} - ]. + ] ++ ExtraMustacheVars. render_config_file(ConfigFile, Vars0) -> Temp = @@ -283,6 +296,14 @@ generate_config(SchemaModule, ConfigFile) when is_atom(SchemaModule) -> -spec stop_apps(list()) -> ok. stop_apps(Apps) -> [application:stop(App) || App <- Apps ++ [emqx, ekka, mria, mnesia]], + %% to avoid inter-suite flakiness + application:unset_env(emqx, init_config_load_done), + persistent_term:erase(?EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY), + emqx_config:erase_schema_mod_and_names(), + ok = emqx_config:delete_override_conf_files(), + application:unset_env(emqx, local_override_conf_file), + application:unset_env(emqx, cluster_override_conf_file), + application:unset_env(gen_rpc, port_discovery), ok. proj_root() -> @@ -327,7 +348,7 @@ safe_relative_path_2(Path) -> -spec reload(App :: atom(), SpecAppConfig :: special_config_handler()) -> ok. reload(App, SpecAppConfigHandler) -> application:stop(App), - start_app(App, SpecAppConfigHandler), + start_app(App, SpecAppConfigHandler, #{}), application:start(App). ensure_mnesia_stopped() -> @@ -469,7 +490,7 @@ is_all_tcp_servers_available(Servers) -> {_, []} -> true; {_, Unavail} -> - ct:print("Unavailable servers: ~p", [Unavail]), + ct:pal("Unavailable servers: ~p", [Unavail]), false end. diff --git a/apps/emqx/test/emqx_ocsp_cache_SUITE.erl b/apps/emqx/test/emqx_ocsp_cache_SUITE.erl new file mode 100644 index 000000000..c45bc15ef --- /dev/null +++ b/apps/emqx/test/emqx_ocsp_cache_SUITE.erl @@ -0,0 +1,944 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_ocsp_cache_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). + +-include_lib("ssl/src/ssl_handshake.hrl"). + +-define(CACHE_TAB, emqx_ocsp_cache). + +all() -> + [{group, openssl}] ++ tests(). + +tests() -> + emqx_common_test_helpers:all(?MODULE) -- openssl_tests(). + +openssl_tests() -> + [t_openssl_client]. + +groups() -> + OpensslTests = openssl_tests(), + [ + {openssl, [ + {group, tls12}, + {group, tls13} + ]}, + {tls12, [ + {group, with_status_request}, + {group, without_status_request} + ]}, + {tls13, [ + {group, with_status_request}, + {group, without_status_request} + ]}, + {with_status_request, [], OpensslTests}, + {without_status_request, [], OpensslTests} + ]. + +init_per_suite(Config) -> + application:load(emqx), + emqx_config:save_schema_mod_and_names(emqx_schema), + emqx_common_test_helpers:boot_modules(all), + Config. + +end_per_suite(_Config) -> + ok. + +init_per_group(tls12, Config) -> + [{tls_vsn, "-tls1_2"} | Config]; +init_per_group(tls13, Config) -> + [{tls_vsn, "-tls1_3"} | Config]; +init_per_group(with_status_request, Config) -> + [{status_request, true} | Config]; +init_per_group(without_status_request, Config) -> + [{status_request, false} | Config]; +init_per_group(_Group, Config) -> + Config. + +end_per_group(_Group, _Config) -> + ok. + +init_per_testcase(t_openssl_client, Config) -> + ct:timetrap({seconds, 30}), + DataDir = ?config(data_dir, Config), + Handler = fun(_) -> ok end, + {OCSPResponderPort, OCSPOSPid} = setup_openssl_ocsp(Config), + ConfFilePath = filename:join([DataDir, "openssl_listeners.conf"]), + emqx_common_test_helpers:start_apps( + [], + Handler, + #{ + extra_mustache_vars => [{test_data_dir, DataDir}], + conf_file_path => ConfFilePath + } + ), + ct:sleep(1_000), + [ + {ocsp_responder_port, OCSPResponderPort}, + {ocsp_responder_os_pid, OCSPOSPid} + | Config + ]; +init_per_testcase(TestCase, Config) when + TestCase =:= t_update_listener; + TestCase =:= t_validations +-> + %% when running emqx standalone tests, we can't use those + %% features. + case does_module_exist(emqx_mgmt_api_test_util) of + true -> + ct:timetrap({seconds, 30}), + %% start the listener with the default (non-ocsp) config + TestPid = self(), + ok = meck:new(emqx_ocsp_cache, [non_strict, passthrough, no_history, no_link]), + meck:expect( + emqx_ocsp_cache, + http_get, + fun(URL, _HTTPTimeout) -> + ct:pal("ocsp http request ~p", [URL]), + TestPid ! {http_get, URL}, + {ok, {{"HTTP/1.0", 200, 'OK'}, [], <<"ocsp response">>}} + end + ), + emqx_mgmt_api_test_util:init_suite([emqx_conf]), + snabbkaffe:start_trace(), + Config; + false -> + [{skip_does_not_apply, true} | Config] + end; +init_per_testcase(t_ocsp_responder_error_responses, Config) -> + ct:timetrap({seconds, 30}), + TestPid = self(), + ok = meck:new(emqx_ocsp_cache, [non_strict, passthrough, no_history, no_link]), + meck:expect( + emqx_ocsp_cache, + http_get, + fun(URL, _HTTPTimeout) -> + ct:pal("ocsp http request ~p", [URL]), + TestPid ! {http_get, URL}, + persistent_term:get({?MODULE, http_response}) + end + ), + DataDir = ?config(data_dir, Config), + Type = ssl, + Name = test_ocsp, + ListenerOpts = #{ + ssl_options => + #{ + certfile => filename:join(DataDir, "server.pem"), + ocsp => #{ + enable_ocsp_stapling => true, + responder_url => <<"http://localhost:9877/">>, + issuer_pem => filename:join(DataDir, "ocsp-issuer.pem"), + refresh_http_timeout => 15_000, + refresh_interval => 1_000 + } + } + }, + Conf = #{listeners => #{Type => #{Name => ListenerOpts}}}, + ConfBin = emqx_map_lib:binary_key_map(Conf), + hocon_tconf:check_plain(emqx_schema, ConfBin, #{required => false, atom_keys => false}), + emqx_config:put_listener_conf(Type, Name, [], ListenerOpts), + snabbkaffe:start_trace(), + _Heir = spawn_dummy_heir(), + {ok, CachePid} = emqx_ocsp_cache:start_link(), + [ + {cache_pid, CachePid} + | Config + ]; +init_per_testcase(_TestCase, Config) -> + ct:timetrap({seconds, 10}), + TestPid = self(), + ok = meck:new(emqx_ocsp_cache, [non_strict, passthrough, no_history, no_link]), + meck:expect( + emqx_ocsp_cache, + http_get, + fun(URL, _HTTPTimeout) -> + TestPid ! {http_get, URL}, + {ok, {{"HTTP/1.0", 200, 'OK'}, [], <<"ocsp response">>}} + end + ), + _Heir = spawn_dummy_heir(), + {ok, CachePid} = emqx_ocsp_cache:start_link(), + DataDir = ?config(data_dir, Config), + Type = ssl, + Name = test_ocsp, + ListenerOpts = #{ + ssl_options => + #{ + certfile => filename:join(DataDir, "server.pem"), + ocsp => #{ + enable_ocsp_stapling => true, + responder_url => <<"http://localhost:9877/">>, + issuer_pem => filename:join(DataDir, "ocsp-issuer.pem"), + refresh_http_timeout => 15_000, + refresh_interval => 1_000 + } + } + }, + Conf = #{listeners => #{Type => #{Name => ListenerOpts}}}, + ConfBin = emqx_map_lib:binary_key_map(Conf), + hocon_tconf:check_plain(emqx_schema, ConfBin, #{required => false, atom_keys => false}), + emqx_config:put_listener_conf(Type, Name, [], ListenerOpts), + snabbkaffe:start_trace(), + [ + {cache_pid, CachePid} + | Config + ]. + +end_per_testcase(t_openssl_client, Config) -> + OCSPResponderOSPid = ?config(ocsp_responder_os_pid, Config), + catch kill_pid(OCSPResponderOSPid), + emqx_common_test_helpers:stop_apps([]), + ok; +end_per_testcase(TestCase, Config) when + TestCase =:= t_update_listener; + TestCase =:= t_validations +-> + Skip = proplists:get_bool(skip_does_not_apply, Config), + case Skip of + true -> + ok; + false -> + emqx_mgmt_api_test_util:end_suite([emqx_conf]), + meck:unload([emqx_ocsp_cache]), + ok + end; +end_per_testcase(t_ocsp_responder_error_responses, Config) -> + CachePid = ?config(cache_pid, Config), + catch gen_server:stop(CachePid), + meck:unload([emqx_ocsp_cache]), + persistent_term:erase({?MODULE, http_response}), + ok; +end_per_testcase(_TestCase, Config) -> + CachePid = ?config(cache_pid, Config), + catch gen_server:stop(CachePid), + meck:unload([emqx_ocsp_cache]), + ok. + +%%-------------------------------------------------------------------- +%% Helper functions +%%-------------------------------------------------------------------- + +%% The real cache makes `emqx_kernel_sup' the heir to its ETS table. +%% In some tests, we don't start the full supervision tree, so we need +%% this dummy process. +spawn_dummy_heir() -> + spawn_link(fun() -> + true = register(emqx_kernel_sup, self()), + receive + stop -> ok + end + end). + +does_module_exist(Mod) -> + case erlang:module_loaded(Mod) of + true -> + true; + false -> + case code:ensure_loaded(Mod) of + ok -> + true; + {module, Mod} -> + true; + _ -> + false + end + end. + +assert_no_http_get() -> + receive + {http_get, _URL} -> + error(should_be_cached) + after 0 -> + ok + end. + +assert_http_get(N) -> + assert_http_get(N, 0). + +assert_http_get(0, _Timeout) -> + ok; +assert_http_get(N, Timeout) when N > 0 -> + receive + {http_get, URL} -> + ?assertMatch(<<"http://localhost:9877/", _Request64/binary>>, URL), + ok + after Timeout -> + error({no_http_get, #{mailbox => process_info(self(), messages)}}) + end, + assert_http_get(N - 1, Timeout). + +openssl_client_command(TLSVsn, RequestStatus, Config) -> + DataDir = ?config(data_dir, Config), + ClientCert = filename:join([DataDir, "client.pem"]), + ClientKey = filename:join([DataDir, "client.key"]), + Cacert = filename:join([DataDir, "ca.pem"]), + Openssl = os:find_executable("openssl"), + StatusOpt = + case RequestStatus of + true -> ["-status"]; + false -> [] + end, + [ + Openssl, + "s_client", + "-connect", + "localhost:8883", + %% needed to trigger `sni_fun' + "-servername", + "localhost", + TLSVsn, + "-CAfile", + Cacert, + "-cert", + ClientCert, + "-key", + ClientKey + ] ++ StatusOpt. + +run_openssl_client(TLSVsn, RequestStatus, Config) -> + Command0 = openssl_client_command(TLSVsn, RequestStatus, Config), + Command = lists:flatten(lists:join(" ", Command0)), + os:cmd(Command). + +%% fixme: for some reason, the port program doesn't return any output +%% when running in OTP 25 using `open_port`, but the `os:cmd` version +%% works fine. +%% the `open_port' version works fine in OTP 24 for some reason. +spawn_openssl_client(TLSVsn, RequestStatus, Config) -> + [Openssl | Args] = openssl_client_command(TLSVsn, RequestStatus, Config), + open_port( + {spawn_executable, Openssl}, + [ + {args, Args}, + binary, + stderr_to_stdout + ] + ). + +spawn_openssl_ocsp_responder(Config) -> + DataDir = ?config(data_dir, Config), + IssuerCert = filename:join([DataDir, "ocsp-issuer.pem"]), + IssuerKey = filename:join([DataDir, "ocsp-issuer.key"]), + Cacert = filename:join([DataDir, "ca.pem"]), + Index = filename:join([DataDir, "index.txt"]), + Openssl = os:find_executable("openssl"), + open_port( + {spawn_executable, Openssl}, + [ + {args, [ + "ocsp", + "-ignore_err", + "-port", + "9877", + "-CA", + Cacert, + "-rkey", + IssuerKey, + "-rsigner", + IssuerCert, + "-index", + Index + ]}, + binary, + stderr_to_stdout + ] + ). + +kill_pid(OSPid) -> + os:cmd("kill -9 " ++ integer_to_list(OSPid)). + +test_ocsp_connection(TLSVsn, WithRequestStatus = true, Config) -> + OCSPOutput = run_openssl_client(TLSVsn, WithRequestStatus, Config), + ?assertMatch( + {match, _}, + re:run(OCSPOutput, "OCSP Response Status: successful"), + #{mailbox => process_info(self(), messages)} + ), + ?assertMatch( + {match, _}, + re:run(OCSPOutput, "Cert Status: good"), + #{mailbox => process_info(self(), messages)} + ), + ok; +test_ocsp_connection(TLSVsn, WithRequestStatus = false, Config) -> + OCSPOutput = run_openssl_client(TLSVsn, WithRequestStatus, Config), + ?assertMatch( + nomatch, + re:run(OCSPOutput, "Cert Status: good", [{capture, none}]), + #{mailbox => process_info(self(), messages)} + ), + ok. + +ensure_port_open(Port) -> + do_ensure_port_open(Port, 10). + +do_ensure_port_open(Port, 0) -> + error({port_not_open, Port}); +do_ensure_port_open(Port, N) when N > 0 -> + Timeout = 1_000, + case gen_tcp:connect("localhost", Port, [], Timeout) of + {ok, Sock} -> + gen_tcp:close(Sock), + ok; + {error, _} -> + ct:sleep(500), + do_ensure_port_open(Port, N - 1) + end. + +get_sni_fun(ListenerID) -> + #{opts := Opts} = emqx_listeners:find_by_id(ListenerID), + SSLOpts = proplists:get_value(ssl_options, Opts), + proplists:get_value(sni_fun, SSLOpts). + +openssl_version() -> + Res0 = string:trim(os:cmd("openssl version"), trailing), + [_, Res] = string:split(Res0, " "), + {match, [Version]} = re:run(Res, "^([^ ]+)", [{capture, first, list}]), + Version. + +setup_openssl_ocsp(Config) -> + OCSPResponderPort = spawn_openssl_ocsp_responder(Config), + {os_pid, OCSPOSPid} = erlang:port_info(OCSPResponderPort, os_pid), + %%%%%%%% Warning!!! + %% Apparently, openssl 3.0.7 introduced a bug in the responder + %% that makes it hang forever if one probes the port with + %% `gen_tcp:open' / `gen_tcp:close'... Comment this out if + %% openssl gets updated in CI or in your local machine. + OpenSSLVersion = openssl_version(), + ct:pal("openssl version: ~p", [OpenSSLVersion]), + case OpenSSLVersion of + "3." ++ _ -> + %% hope that the responder has started... + ok; + _ -> + ensure_port_open(9877) + end, + ct:sleep(1_000), + {OCSPResponderPort, OCSPOSPid}. + +request(Method, Url, QueryParams, Body) -> + AuthHeader = emqx_mgmt_api_test_util:auth_header_(), + Opts = #{return_all => true}, + case emqx_mgmt_api_test_util:request_api(Method, Url, QueryParams, AuthHeader, Body, Opts) of + {ok, {Reason, Headers, BodyR}} -> + {ok, {Reason, Headers, emqx_json:decode(BodyR, [return_maps])}}; + Error -> + Error + end. + +get_listener_via_api(ListenerId) -> + Path = emqx_mgmt_api_test_util:api_path(["listeners", ListenerId]), + request(get, Path, [], []). + +update_listener_via_api(ListenerId, NewConfig) -> + Path = emqx_mgmt_api_test_util:api_path(["listeners", ListenerId]), + request(put, Path, [], NewConfig). + +put_http_response(Response) -> + persistent_term:put({?MODULE, http_response}, Response). + +%%-------------------------------------------------------------------- +%% Test cases +%%-------------------------------------------------------------------- + +t_request_ocsp_response(_Config) -> + ?check_trace( + begin + ListenerID = <<"ssl:test_ocsp">>, + %% not yet cached. + ?assertEqual([], ets:tab2list(?CACHE_TAB)), + ?assertEqual( + {ok, <<"ocsp response">>}, + emqx_ocsp_cache:fetch_response(ListenerID) + ), + assert_http_get(1), + ?assertMatch([{_, <<"ocsp response">>}], ets:tab2list(?CACHE_TAB)), + %% already cached; should not perform request again. + ?assertEqual( + {ok, <<"ocsp response">>}, + emqx_ocsp_cache:fetch_response(ListenerID) + ), + assert_no_http_get(), + ok + end, + fun(Trace) -> + ?assert( + ?strict_causality( + #{?snk_kind := ocsp_cache_miss, listener_id := _ListenerID}, + #{?snk_kind := ocsp_http_fetch_and_cache, listener_id := _ListenerID}, + Trace + ) + ), + ?assertMatch( + [_], + ?of_kind(ocsp_cache_miss, Trace) + ), + ?assertMatch( + [_], + ?of_kind(ocsp_http_fetch_and_cache, Trace) + ), + ?assertMatch( + [_], + ?of_kind(ocsp_cache_hit, Trace) + ), + ok + end + ). + +t_request_ocsp_response_restart_cache(Config) -> + process_flag(trap_exit, true), + CachePid = ?config(cache_pid, Config), + ListenerID = <<"ssl:test_ocsp">>, + ?check_trace( + begin + [] = ets:tab2list(?CACHE_TAB), + {ok, _} = emqx_ocsp_cache:fetch_response(ListenerID), + ?wait_async_action( + begin + Ref = monitor(process, CachePid), + exit(CachePid, kill), + receive + {'DOWN', Ref, process, CachePid, killed} -> + ok + after 1_000 -> + error(cache_not_killed) + end, + {ok, _} = emqx_ocsp_cache:start_link(), + ok + end, + #{?snk_kind := ocsp_cache_init} + ), + {ok, _} = emqx_ocsp_cache:fetch_response(ListenerID), + ok + end, + fun(Trace) -> + %% Only one fetch because the cache table was preserved by + %% its heir ("emqx_kernel_sup"). + ?assertMatch( + [_], + ?of_kind(ocsp_http_fetch_and_cache, Trace) + ), + assert_http_get(1), + ok + end + ). + +t_request_ocsp_response_bad_http_status(_Config) -> + TestPid = self(), + meck:expect( + emqx_ocsp_cache, + http_get, + fun(URL, _HTTPTimeout) -> + TestPid ! {http_get, URL}, + {ok, {{"HTTP/1.0", 404, 'Not Found'}, [], <<"not found">>}} + end + ), + ListenerID = <<"ssl:test_ocsp">>, + %% not yet cached. + ?assertEqual([], ets:tab2list(?CACHE_TAB)), + ?assertEqual( + error, + emqx_ocsp_cache:fetch_response(ListenerID) + ), + assert_http_get(1), + ?assertEqual([], ets:tab2list(?CACHE_TAB)), + ok. + +t_request_ocsp_response_timeout(_Config) -> + TestPid = self(), + meck:expect( + emqx_ocsp_cache, + http_get, + fun(URL, _HTTPTimeout) -> + TestPid ! {http_get, URL}, + {error, timeout} + end + ), + ListenerID = <<"ssl:test_ocsp">>, + %% not yet cached. + ?assertEqual([], ets:tab2list(?CACHE_TAB)), + ?assertEqual( + error, + emqx_ocsp_cache:fetch_response(ListenerID) + ), + assert_http_get(1), + ?assertEqual([], ets:tab2list(?CACHE_TAB)), + ok. + +t_register_listener(_Config) -> + ListenerID = <<"ssl:test_ocsp">>, + Conf = emqx_config:get_listener_conf(ssl, test_ocsp, []), + %% should fetch and cache immediately + {ok, {ok, _}} = + ?wait_async_action( + emqx_ocsp_cache:register_listener(ListenerID, Conf), + #{?snk_kind := ocsp_http_fetch_and_cache, listener_id := ListenerID} + ), + assert_http_get(1), + ?assertMatch([{_, <<"ocsp response">>}], ets:tab2list(?CACHE_TAB)), + ok. + +t_register_twice(_Config) -> + ListenerID = <<"ssl:test_ocsp">>, + Conf = emqx_config:get_listener_conf(ssl, test_ocsp, []), + {ok, {ok, _}} = + ?wait_async_action( + emqx_ocsp_cache:register_listener(ListenerID, Conf), + #{?snk_kind := ocsp_http_fetch_and_cache, listener_id := ListenerID} + ), + assert_http_get(1), + ?assertMatch([{_, <<"ocsp response">>}], ets:tab2list(?CACHE_TAB)), + %% should have no problem in registering the same listener again. + %% this prompts an immediate refresh. + {ok, {ok, _}} = + ?wait_async_action( + emqx_ocsp_cache:register_listener(ListenerID, Conf), + #{?snk_kind := ocsp_http_fetch_and_cache, listener_id := ListenerID} + ), + ok. + +t_refresh_periodically(_Config) -> + ListenerID = <<"ssl:test_ocsp">>, + Conf = emqx_config:get_listener_conf(ssl, test_ocsp, []), + %% should refresh periodically + {ok, SubRef} = + snabbkaffe:subscribe( + fun + (#{?snk_kind := ocsp_http_fetch_and_cache, listener_id := ListenerID0}) -> + ListenerID0 =:= ListenerID; + (_) -> + false + end, + _NEvents = 2, + _Timeout = 10_000 + ), + ok = emqx_ocsp_cache:register_listener(ListenerID, Conf), + ?assertMatch({ok, [_, _]}, snabbkaffe:receive_events(SubRef)), + assert_http_get(2), + ok. + +t_sni_fun_success(_Config) -> + ListenerID = <<"ssl:test_ocsp">>, + ServerName = "localhost", + ?assertEqual( + [ + {certificate_status, #certificate_status{ + status_type = ?CERTIFICATE_STATUS_TYPE_OCSP, + response = <<"ocsp response">> + }} + ], + emqx_ocsp_cache:sni_fun(ServerName, ListenerID) + ), + ok. + +t_sni_fun_http_error(_Config) -> + meck:expect( + emqx_ocsp_cache, + http_get, + fun(_URL, _HTTPTimeout) -> + {error, timeout} + end + ), + ListenerID = <<"ssl:test_ocsp">>, + ServerName = "localhost", + ?assertEqual( + [], + emqx_ocsp_cache:sni_fun(ServerName, ListenerID) + ), + ok. + +%% check that we can start with a non-ocsp stapling listener and +%% restart it with the new ocsp config. +t_update_listener(Config) -> + case proplists:get_bool(skip_does_not_apply, Config) of + true -> + ok; + false -> + do_t_update_listener(Config) + end. + +do_t_update_listener(Config) -> + DataDir = ?config(data_dir, Config), + Keyfile = filename:join([DataDir, "server.key"]), + Certfile = filename:join([DataDir, "server.pem"]), + Cacertfile = filename:join([DataDir, "ca.pem"]), + IssuerPemPath = filename:join([DataDir, "ocsp-issuer.pem"]), + {ok, IssuerPem} = file:read_file(IssuerPemPath), + + %% no ocsp at first + ListenerId = "ssl:default", + {ok, {{_, 200, _}, _, ListenerData0}} = get_listener_via_api(ListenerId), + ?assertMatch( + #{ + <<"ssl_options">> := + #{ + <<"ocsp">> := + #{<<"enable_ocsp_stapling">> := false} + } + }, + ListenerData0 + ), + assert_no_http_get(), + + %% configure ocsp + OCSPConfig = + #{ + <<"ssl_options">> => + #{ + <<"keyfile">> => Keyfile, + <<"certfile">> => Certfile, + <<"cacertfile">> => Cacertfile, + <<"ocsp">> => + #{ + <<"enable_ocsp_stapling">> => true, + %% we use the file contents to check that + %% the API converts that to an internally + %% managed file + <<"issuer_pem">> => IssuerPem, + <<"responder_url">> => <<"http://localhost:9877">> + } + } + }, + ListenerData1 = emqx_map_lib:deep_merge(ListenerData0, OCSPConfig), + {ok, {_, _, ListenerData2}} = update_listener_via_api(ListenerId, ListenerData1), + ?assertMatch( + #{ + <<"ssl_options">> := + #{ + <<"ocsp">> := + #{ + <<"enable_ocsp_stapling">> := true, + <<"issuer_pem">> := _, + <<"responder_url">> := _ + } + } + }, + ListenerData2 + ), + %% issuer pem should have been uploaded and saved to a new + %% location + ?assertNotEqual( + IssuerPemPath, + emqx_map_lib:deep_get( + [<<"ssl_options">>, <<"ocsp">>, <<"issuer_pem">>], + ListenerData2 + ) + ), + ?assertNotEqual( + IssuerPem, + emqx_map_lib:deep_get( + [<<"ssl_options">>, <<"ocsp">>, <<"issuer_pem">>], + ListenerData2 + ) + ), + assert_http_get(1, 5_000), + ok. + +t_ocsp_responder_error_responses(_Config) -> + ListenerId = <<"ssl:test_ocsp">>, + Conf = emqx_config:get_listener_conf(ssl, test_ocsp, []), + ?check_trace( + begin + %% successful response without headers + put_http_response({ok, {200, <<"ocsp_response">>}}), + {ok, {ok, _}} = + ?wait_async_action( + emqx_ocsp_cache:register_listener(ListenerId, Conf), + #{?snk_kind := ocsp_http_fetch_and_cache, headers := false}, + 1_000 + ), + + %% error response with headers + put_http_response({ok, {{"HTTP/1.0", 500, "Internal Server Error"}, [], <<"error">>}}), + {ok, {ok, _}} = + ?wait_async_action( + emqx_ocsp_cache:register_listener(ListenerId, Conf), + #{?snk_kind := ocsp_http_fetch_bad_code, code := 500, headers := true}, + 1_000 + ), + + %% error response without headers + put_http_response({ok, {500, <<"error">>}}), + {ok, {ok, _}} = + ?wait_async_action( + emqx_ocsp_cache:register_listener(ListenerId, Conf), + #{?snk_kind := ocsp_http_fetch_bad_code, code := 500, headers := false}, + 1_000 + ), + + %% econnrefused + put_http_response( + {error, + {failed_connect, [ + {to_address, {"localhost", 9877}}, + {inet, [inet], econnrefused} + ]}} + ), + {ok, {ok, _}} = + ?wait_async_action( + emqx_ocsp_cache:register_listener(ListenerId, Conf), + #{?snk_kind := ocsp_http_fetch_error, error := {failed_connect, _}}, + 1_000 + ), + + %% timeout + put_http_response({error, timeout}), + {ok, {ok, _}} = + ?wait_async_action( + emqx_ocsp_cache:register_listener(ListenerId, Conf), + #{?snk_kind := ocsp_http_fetch_error, error := timeout}, + 1_000 + ), + + ok + end, + [] + ), + ok. + +t_unknown_requests(_Config) -> + emqx_ocsp_cache ! unknown, + ?assertEqual(ok, gen_server:cast(emqx_ocsp_cache, unknown)), + ?assertEqual({error, {unknown_call, unknown}}, gen_server:call(emqx_ocsp_cache, unknown)), + ok. + +t_validations(Config) -> + case proplists:get_bool(skip_does_not_apply, Config) of + true -> + ok; + false -> + do_t_validations(Config) + end. + +do_t_validations(_Config) -> + ListenerId = <<"ssl:default">>, + {ok, {{_, 200, _}, _, ListenerData0}} = get_listener_via_api(ListenerId), + + ListenerData1 = + emqx_map_lib:deep_merge( + ListenerData0, + #{ + <<"ssl_options">> => + #{<<"ocsp">> => #{<<"enable_ocsp_stapling">> => true}} + } + ), + {error, {_, _, ResRaw1}} = update_listener_via_api(ListenerId, ListenerData1), + #{<<"code">> := <<"BAD_REQUEST">>, <<"message">> := MsgRaw1} = + emqx_json:decode(ResRaw1, [return_maps]), + ?assertMatch( + #{ + <<"mismatches">> := + #{ + <<"listeners:ssl_not_required_bind">> := + #{ + <<"reason">> := + <<"The responder URL is required for OCSP stapling">> + } + } + }, + emqx_json:decode(MsgRaw1, [return_maps]) + ), + + ListenerData2 = + emqx_map_lib:deep_merge( + ListenerData0, + #{ + <<"ssl_options">> => + #{ + <<"ocsp">> => #{ + <<"enable_ocsp_stapling">> => true, + <<"responder_url">> => <<"http://localhost:9877">> + } + } + } + ), + {error, {_, _, ResRaw2}} = update_listener_via_api(ListenerId, ListenerData2), + #{<<"code">> := <<"BAD_REQUEST">>, <<"message">> := MsgRaw2} = + emqx_json:decode(ResRaw2, [return_maps]), + ?assertMatch( + #{ + <<"mismatches">> := + #{ + <<"listeners:ssl_not_required_bind">> := + #{ + <<"reason">> := + <<"The issuer PEM path is required for OCSP stapling">> + } + } + }, + emqx_json:decode(MsgRaw2, [return_maps]) + ), + + ListenerData3a = + emqx_map_lib:deep_merge( + ListenerData0, + #{ + <<"ssl_options">> => + #{ + <<"ocsp">> => #{ + <<"enable_ocsp_stapling">> => true, + <<"responder_url">> => <<"http://localhost:9877">>, + <<"issuer_pem">> => <<"some_file">> + } + } + } + ), + ListenerData3 = emqx_map_lib:deep_remove([<<"ssl_options">>, <<"certfile">>], ListenerData3a), + {error, {_, _, ResRaw3}} = update_listener_via_api(ListenerId, ListenerData3), + #{<<"code">> := <<"BAD_REQUEST">>, <<"message">> := MsgRaw3} = + emqx_json:decode(ResRaw3, [return_maps]), + ?assertMatch( + #{ + <<"mismatches">> := + #{ + <<"listeners:ssl_not_required_bind">> := + #{ + <<"reason">> := + <<"Server certificate must be defined when using OCSP stapling">> + } + } + }, + emqx_json:decode(MsgRaw3, [return_maps]) + ), + + ok. + +t_unknown_error_fetching_ocsp_response(_Config) -> + ListenerID = <<"ssl:test_ocsp">>, + TestPid = self(), + ok = meck:expect( + emqx_ocsp_cache, + http_get, + fun(_RequestURI, _HTTPTimeout) -> + TestPid ! error_raised, + meck:exception(error, something_went_wrong) + end + ), + ?assertEqual(error, emqx_ocsp_cache:fetch_response(ListenerID)), + receive + error_raised -> ok + after 200 -> ct:fail("should have tried to fetch ocsp response") + end, + ok. + +t_openssl_client(Config) -> + TLSVsn = ?config(tls_vsn, Config), + WithStatusRequest = ?config(status_request, Config), + %% ensure ocsp response is already cached. + ListenerID = <<"ssl:default">>, + ?assertMatch( + {ok, _}, + emqx_ocsp_cache:fetch_response(ListenerID), + #{msgs => process_info(self(), messages)} + ), + timer:sleep(500), + test_ocsp_connection(TLSVsn, WithStatusRequest, Config). diff --git a/apps/emqx/test/emqx_ocsp_cache_SUITE_data/ca.pem b/apps/emqx/test/emqx_ocsp_cache_SUITE_data/ca.pem new file mode 100644 index 000000000..eaabd2445 --- /dev/null +++ b/apps/emqx/test/emqx_ocsp_cache_SUITE_data/ca.pem @@ -0,0 +1,68 @@ +-----BEGIN CERTIFICATE----- +MIIF+zCCA+OgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwbzELMAkGA1UEBhMCU0Ux +EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQK +DAlNeU9yZ05hbWUxETAPBgNVBAsMCE15Um9vdENBMREwDwYDVQQDDAhNeVJvb3RD +QTAeFw0yMzAxMTIxMzA4MTZaFw0zMzAxMDkxMzA4MTZaMGsxCzAJBgNVBAYTAlNF +MRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAoMCU15T3JnTmFtZTEZMBcGA1UE +CwwQTXlJbnRlcm1lZGlhdGVDQTEZMBcGA1UEAwwQTXlJbnRlcm1lZGlhdGVDQTCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALQG7dMeU/y9HDNHzhydR0bm +wN9UGplqJOJPwqJRaZZcrn9umgJ9SU2il2ceEVxMDwzBWCRKJO5/H9A9k13SqsXM +2c2c9xXfIF1kb820lCm1Uow5hZ/auDjxliNk9kNJDigCRi3QoIs/dVeWzFsgEC2l +gxRqauN2eNFb6/yXY788YALHBsCRV2NFOFXxtPsvLXpD9Q/8EqYsSMuLARRdHVNU +ryaEF5lhShpcuz0TlIuTy2TiuXJUtJ+p7a4Z7friZ6JsrmQWsVQBj44F8TJRHWzW +C7vm9c+dzEX9eqbr5iPL+L4ctMW9Lz6ePcYfIXne6CElusRUf8G+xM1uwovF9bpV ++9IqY7tAu9G1iY9iNtJgNNDKOCcOGKcZCx6Cg1XYOEKReNnUMazvYeqRrrjV5WQ0 +vOcD5zcBRNTXCddCLa7U0guXP9mQrfuk4NTH1Bt77JieTJ8cfDXHwtaKf6aGbmZP +wl1Xi/GuXNUP/xeog78RKyFwBmjt2JKwvWzMpfmH4mEkG9moh2alva+aEz6LIJuP +16g6s0Q6c793/OvUtpNcewHw4Vjn39LD9o6VLp854G4n8dVpUWSbWS+sXD1ZE69H +g/sMNMyq+09ufkbewY8xoCm/rQ1pqDZAVMWsstJEaYu7b/eb7R+RGOj1YECCV/Yp +EZPdDotbSNRkIi2d/a1NAgMBAAGjgaQwgaEwHQYDVR0OBBYEFExwhjsVUom6tQ+S +qq6xMUETvnPzMB8GA1UdIwQYMBaAFD90kfU5pc5l48THu0Ayj9SNpHuhMBIGA1Ud +EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMDsGA1UdHwQ0MDIwMKAuoCyG +Kmh0dHA6Ly9sb2NhbGhvc3Q6OTg3OC9pbnRlcm1lZGlhdGUuY3JsLnBlbTANBgkq +hkiG9w0BAQsFAAOCAgEAK6NgdWQYtPNKQNBGjsgtgqTRh+k30iqSO6Y3yE1KGABO +EuQdVqkC2qUIbCB0M0qoV0ab50KNLfU6cbshggW4LDpcMpoQpI05fukNh1jm3ZuZ +0xsB7vlmlsv00tpqmfIl/zykPDynHKOmFh/hJP/KetMy4+wDv4/+xP31UdEj5XvG +HvMtuqOS23A+H6WPU7ol7KzKBnU2zz/xekvPbUD3JqV+ynP5bgbIZHAndd0o9T8e +NFX23Us4cTenU2/ZlOq694bRzGaK+n3Ksz995Nbtzv5fbUgqmf7Mcq4iHGRVtV11 +MRyBrsXZp2vbF63c4hrf2Zd6SWRoaDKRhP2DMhajpH9zZASSTlfejg/ZRO2s+Clh +YrSTkeMAdnRt6i/q4QRcOTCfsX75RFM5v67njvTXsSaSTnAwaPi78tRtf+WSh0EP +VVPzy++BszBVlJ1VAf7soWZHCjZxZ8ZPqVTy5okoHwWQ09WmYe8GfulDh1oj0wbK +3FjN7bODWHJN+bFf5aQfK+tumYKoPG8RXL6QxpEzjFWjxhIMJHHMKfDWnAV1o1+7 +/1/aDzq7MzEYBbrgQR7oE5ZHtyqhCf9LUgw0Kr7/8QWuNAdeDCJzjXRROU0hJczp +dOyfRlLbHmLLmGOnROlx6LsGNQ17zuz6SPi7ei8/ylhykawDOAGkM1+xFakmQhM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUYjc7hD7/UJ0/VPADfNfp/WpOwRowDQYJKoZIhvcNAQEL +BQAwbzELMAkGA1UEBhMCU0UxEjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJ +U3RvY2tob2xtMRIwEAYDVQQKDAlNeU9yZ05hbWUxETAPBgNVBAsMCE15Um9vdENB +MREwDwYDVQQDDAhNeVJvb3RDQTAeFw0yMzAxMTIxMzA4MTRaFw00MzAxMDcxMzA4 +MTRaMG8xCzAJBgNVBAYTAlNFMRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAcM +CVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMREwDwYDVQQLDAhNeVJvb3RD +QTERMA8GA1UEAwwITXlSb290Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCnBwSOYVJw47IoMHMXTVDtOYvUt3rqsurEhFcB4O8xmf2mmwr6m7s8A5Ft +AvAehg1GvnXT3t/KiyU7BK+acTwcErGyZwS2wvdB0lpHWSpOn/u5y+4ZETvQefcj +ZTdDOM9VN5nutpitgNb+1yL8sqSexfVbY7DnYYvFjOVBYoP/SGvM9jVjCad+0WL3 +FhuD+L8QAxzCieX3n9UMymlFwINQuEc+TDjuNcEqt+0J5EgS1fwzxb2RCVL0TNv4 +9a71hFGCNRj20AeZm99hbdufm7+0AFO7ocV5q43rLrWFUoBzqKPYIjga/cv/UdWZ +c5RLRXw3JDSrCqkf/mOlaEhNPlmWRF9MSus5Da3wuwgGCaVzmrf30rWR5aHHcscG +e+AOgJ4HayvBUQeb6ZlRXc0YlACiLToMKxuyxDyUcDfVEXpUIsDILF8dkiVQxEU3 +j9g6qjXiqPVdNiwpqXfBKObj8vNCzORnoHYs8cCgib3RgDVWeqkDmlSwlZE7CvQh +U4Loj4l7813xxzYEKkVaT1JdXPWu42CG/b4Y/+f4V+3rkJkYzUwndX6kZNksIBai +phmtvKt+CTdP1eAbT+C9AWWF3PT31+BIhuT0u9tR8BVSkXdQB8dG4M/AAJcTo640 +0mdYYOXT153gEKHJuUBm750ZTy+r6NjNvpw8VrMAakJwHqnIdQIDAQABo2MwYTAd +BgNVHQ4EFgQUP3SR9TmlzmXjxMe7QDKP1I2ke6EwHwYDVR0jBBgwFoAUP3SR9Tml +zmXjxMe7QDKP1I2ke6EwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQELBQADggIBAFMFv4C+I0+xOAb9v6G/IOpfPBZ1ez31EXKJJBra +lulP4nRHQMeb310JS8BIeQ3dl+7+PkSxPABZSwc3jkxdSMvhc+Z4MQtTgos+Qsjs +gH7sTqwWeeQ0lHYxWmkXijrh5OPRZwTKzYQlkcn85BCUXl2KDuNEdiqPbDTao+lc +lA0/UAvC6NCyFKq/jqf4CmW5Kx6yG1v1LaE+IXn7cbIXj+DaehocVXi0wsXqj03Q +DDUHuLHZP+LBsg4e91/0Jy2ekNRTYJifSqr+9ufHl0ZX1pFDZyf396IgZ5CQZ0PJ +nRxZHlCfsxWxmxxdy3FQSE6YwXhdTjjoAa1ApZcKkkt1beJa6/oRLze/ux5x+5q+ +4QczufHd6rjoKBi6BM3FgFQ8As5iNohHXlMHd/xITo1Go3CWw2j9TGH5vzksOElK +B0mcwwt2zwNEjvfytc+tI5jcfGN3tiT5fVHS8hw9dWKevypLL+55Ua9G8ZgDHasT +XFRJHgmnbyFcaAe26D2dSKmhC9u2mHBH+MaI8dj3e7wNBfpxNgp41aFIk+QTmiFW +VXFED6DHQ/Mxq93ACalHdYg18PlIYClbT6Pf2xXBnn33YPhn5xzoTZ+cDH/RpaQp +s0UUTSJT1UTXgtXPnZWQfvKlMjJEIiVFiLEC0sgZRlWuZDRAY0CdZJJxvQp59lqu +cbTm +-----END CERTIFICATE----- diff --git a/apps/emqx/test/emqx_ocsp_cache_SUITE_data/client.key b/apps/emqx/test/emqx_ocsp_cache_SUITE_data/client.key new file mode 100644 index 000000000..a1c46aa5c --- /dev/null +++ b/apps/emqx/test/emqx_ocsp_cache_SUITE_data/client.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +rkE8Bz5a/iwO+QwdO5Q2cp5+0nm41A== +-----END PRIVATE KEY----- diff --git a/apps/emqx/test/emqx_ocsp_cache_SUITE_data/client.pem b/apps/emqx/test/emqx_ocsp_cache_SUITE_data/client.pem new file mode 100644 index 000000000..06adc2aa3 --- /dev/null +++ b/apps/emqx/test/emqx_ocsp_cache_SUITE_data/client.pem @@ -0,0 +1,38 @@ +-----BEGIN CERTIFICATE----- +MIIGmjCCBIKgAwIBAgICEAYwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux +EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL +DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X +DTIzMDMwNjE5NTA0N1oXDTMzMDYxMTE5NTA0N1owezELMAkGA1UEBhMCU0UxEjAQ +BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN +eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExFTATBgNVBAMMDG9j +c3AuY2xpZW50MjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKZ9mYEA +5knzGM9iSnJDFMZnjS8jA67KXBKjDXaJ1Y3qNxb2DloiKn4Hagqt/NHs9izFsOh1 +yP8o6lAbAIUC6NJiBi+KI6yYBNDWZ42hyedaMCzJvD51Jcr9mN+tWAhokXtEdouI +O2rlMjhvgqNQXKHk0gshz7N9mqg1TqSFQXRtw4EAbYEVoHNF/5r+GBlcQ/ISeioU +rknmT4MNKUpcNs5MQeFcbhAVPWFlq1rGm0PWdMQVFi7N+aH+DhEO9a1rCb1hTeP9 +XRSfu39NqPR8Bc2LfRPMo5VpVwOuRW0MUd7aFfeU4l3ncjk2lLtqxqr5pM3K3P4j +cF/9ri+SESCsqmMFJdeb/5XDk3Oxc3Ap11JQbLCYhtXZgFicr7QPCE5cuxM+Hem/ +r3fU1XGmJ9bSczhSvpcIMc49Vf+MhlnzX1vMCVlncGAxPPumpP33TqWY1FAGBKCv +5lrD2CNWEuDVUN/AXzl5qeFvOTkhOd99H6zZyidmvaD4sQOyrZ0m/+wRti4cE0GQ +O2Gfjf0yA2rWevc9XSGfyk1yBLlkVTD583zjTdFj1NguywH8UVhEQMJrHSPvmovc +Tsttkrwl+w/y6HV7c91HrhOC2hihMq7C0ip6WaH6vIBYWNpcs+g/8CpiTVVxEJ68 +5dQrDouKyR26SmWT1zLZw8tyb4Q/7YmzcHbxAgMBAAGjggE2MIIBMjAJBgNVHRME +AjAAMBEGCWCGSAGG+EIBAQQEAwIFoDAzBglghkgBhvhCAQ0EJhYkT3BlblNTTCBH +ZW5lcmF0ZWQgQ2xpZW50IENlcnRpZmljYXRlMB0GA1UdDgQWBBSJ/yia067wCafe +kDCgk+e8PJTCUDAfBgNVHSMEGDAWgBRMcIY7FVKJurUPkqqusTFBE75z8zAOBgNV +HQ8BAf8EBAMCBeAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMDsGA1Ud +HwQ0MDIwMKAuoCyGKmh0dHA6Ly9sb2NhbGhvc3Q6OTg3OC9pbnRlcm1lZGlhdGUu +Y3JsLnBlbTAxBggrBgEFBQcBAQQlMCMwIQYIKwYBBQUHMAGGFWh0dHA6Ly9sb2Nh +bGhvc3Q6OTg3NzANBgkqhkiG9w0BAQsFAAOCAgEAN2XfYgbrjxC6OWh9UoMLQaDD +59JPxAUBxlRtWzTWqxY2jfT+OwJfDP4e+ef2G1YEG+qyt57ddlm/EwX9IvAvG0D4 +wd4tfItG88IJWKDM3wpT5KYrUsu+PlQTFmGmaWlORK/mRKlmfjbP5CIAcUedvCS9 +j9PkCrbbkklAmp0ULLSLUkYajmfFOkQ+VdGhQ6nAamTeyh2Z2S4dVjsKc8yBViMo +/V6HP56rOvUqiVTcvhZtH7QDptMSTzuJ+AsmreYjwIiTGzYS/i8QVAFuPfXJKEOB +jD5WhUaP/8Snbuft4MxssPAph8okcmxLfb55nw+soNc2oS1wWwKMe7igRelq8vtg +bu00QSEGiY1eq/vFgZh0+Wohy/YeYzhO4Jq40FFpKiVbkLzexpNH/Afj2QrHuZ7y +259uGGfv5tGA+TW6PsckCQknEb5V4V35ZZlbWVRKpuADeNPoDuoYPtc5eOomIkmw +rFz/gPZWSA+4pYEgXgqcaM8+KP0i53eTbWqwy5DVgXiuaTYWU4m1FTsIZ+/nGIqW +Dsgqd/D6jivf9Yvm+VFYTZsxIfq5sMdjxSuMBo0nZrzFDpqc6m6fVVoHv5R9Yliw +MbxgmFQ84CKLy7iNKGSGVN2SIr1obMQ0e/t3NiCHib3WKzmZFoNoFCtVzAgsxGmF +Q6rY83JdIPPW4LqZNcE= +-----END CERTIFICATE----- diff --git a/apps/emqx/test/emqx_ocsp_cache_SUITE_data/index.txt b/apps/emqx/test/emqx_ocsp_cache_SUITE_data/index.txt new file mode 100644 index 000000000..76a170dd3 --- /dev/null +++ b/apps/emqx/test/emqx_ocsp_cache_SUITE_data/index.txt @@ -0,0 +1,6 @@ +V 330419130816Z 1000 unknown /C=SE/ST=Stockholm/L=Stockholm/O=MyOrgName/OU=MyIntermediateCA/CN=localhost +V 330419130816Z 1001 unknown /C=SE/ST=Stockholm/L=Stockholm/O=MyOrgName/OU=MyIntermediateCA/CN=MyClient +R 330419130816Z 230112130816Z 1002 unknown /C=SE/ST=Stockholm/L=Stockholm/O=MyOrgName/OU=MyIntermediateCA/CN=client-revoked +V 330419130816Z 1003 unknown /C=SE/ST=Stockholm/L=Stockholm/O=MyOrgName/OU=MyIntermediateCA/CN=ocsp.server +V 330419130816Z 1004 unknown /C=SE/ST=Stockholm/L=Stockholm/O=MyOrgName/OU=MyIntermediateCA/CN=ocsp.client +V 330425123656Z 1005 unknown /C=SE/ST=Stockholm/L=Stockholm/O=MyOrgName/OU=MyIntermediateCA/CN=client-no-dist-points diff --git a/apps/emqx/test/emqx_ocsp_cache_SUITE_data/ocsp-issuer.key b/apps/emqx/test/emqx_ocsp_cache_SUITE_data/ocsp-issuer.key new file mode 100644 index 000000000..511cd5b0a --- /dev/null +++ b/apps/emqx/test/emqx_ocsp_cache_SUITE_data/ocsp-issuer.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +xBO4CmF79pAcAXvDoRB619Flx5/G +-----END PRIVATE KEY----- diff --git a/apps/emqx/test/emqx_ocsp_cache_SUITE_data/ocsp-issuer.pem b/apps/emqx/test/emqx_ocsp_cache_SUITE_data/ocsp-issuer.pem new file mode 100644 index 000000000..467e4c209 --- /dev/null +++ b/apps/emqx/test/emqx_ocsp_cache_SUITE_data/ocsp-issuer.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF+zCCA+OgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwbzELMAkGA1UEBhMCU0Ux +EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQK +DAlNeU9yZ05hbWUxETAPBgNVBAsMCE15Um9vdENBMREwDwYDVQQDDAhNeVJvb3RD +QTAeFw0yMzAxMTIxMzA4MTZaFw0zMzAxMDkxMzA4MTZaMGsxCzAJBgNVBAYTAlNF +MRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAoMCU15T3JnTmFtZTEZMBcGA1UE +CwwQTXlJbnRlcm1lZGlhdGVDQTEZMBcGA1UEAwwQTXlJbnRlcm1lZGlhdGVDQTCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALQG7dMeU/y9HDNHzhydR0bm +wN9UGplqJOJPwqJRaZZcrn9umgJ9SU2il2ceEVxMDwzBWCRKJO5/H9A9k13SqsXM +2c2c9xXfIF1kb820lCm1Uow5hZ/auDjxliNk9kNJDigCRi3QoIs/dVeWzFsgEC2l +gxRqauN2eNFb6/yXY788YALHBsCRV2NFOFXxtPsvLXpD9Q/8EqYsSMuLARRdHVNU +ryaEF5lhShpcuz0TlIuTy2TiuXJUtJ+p7a4Z7friZ6JsrmQWsVQBj44F8TJRHWzW +C7vm9c+dzEX9eqbr5iPL+L4ctMW9Lz6ePcYfIXne6CElusRUf8G+xM1uwovF9bpV ++9IqY7tAu9G1iY9iNtJgNNDKOCcOGKcZCx6Cg1XYOEKReNnUMazvYeqRrrjV5WQ0 +vOcD5zcBRNTXCddCLa7U0guXP9mQrfuk4NTH1Bt77JieTJ8cfDXHwtaKf6aGbmZP +wl1Xi/GuXNUP/xeog78RKyFwBmjt2JKwvWzMpfmH4mEkG9moh2alva+aEz6LIJuP +16g6s0Q6c793/OvUtpNcewHw4Vjn39LD9o6VLp854G4n8dVpUWSbWS+sXD1ZE69H +g/sMNMyq+09ufkbewY8xoCm/rQ1pqDZAVMWsstJEaYu7b/eb7R+RGOj1YECCV/Yp +EZPdDotbSNRkIi2d/a1NAgMBAAGjgaQwgaEwHQYDVR0OBBYEFExwhjsVUom6tQ+S +qq6xMUETvnPzMB8GA1UdIwQYMBaAFD90kfU5pc5l48THu0Ayj9SNpHuhMBIGA1Ud +EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMDsGA1UdHwQ0MDIwMKAuoCyG +Kmh0dHA6Ly9sb2NhbGhvc3Q6OTg3OC9pbnRlcm1lZGlhdGUuY3JsLnBlbTANBgkq +hkiG9w0BAQsFAAOCAgEAK6NgdWQYtPNKQNBGjsgtgqTRh+k30iqSO6Y3yE1KGABO +EuQdVqkC2qUIbCB0M0qoV0ab50KNLfU6cbshggW4LDpcMpoQpI05fukNh1jm3ZuZ +0xsB7vlmlsv00tpqmfIl/zykPDynHKOmFh/hJP/KetMy4+wDv4/+xP31UdEj5XvG +HvMtuqOS23A+H6WPU7ol7KzKBnU2zz/xekvPbUD3JqV+ynP5bgbIZHAndd0o9T8e +NFX23Us4cTenU2/ZlOq694bRzGaK+n3Ksz995Nbtzv5fbUgqmf7Mcq4iHGRVtV11 +MRyBrsXZp2vbF63c4hrf2Zd6SWRoaDKRhP2DMhajpH9zZASSTlfejg/ZRO2s+Clh +YrSTkeMAdnRt6i/q4QRcOTCfsX75RFM5v67njvTXsSaSTnAwaPi78tRtf+WSh0EP +VVPzy++BszBVlJ1VAf7soWZHCjZxZ8ZPqVTy5okoHwWQ09WmYe8GfulDh1oj0wbK +3FjN7bODWHJN+bFf5aQfK+tumYKoPG8RXL6QxpEzjFWjxhIMJHHMKfDWnAV1o1+7 +/1/aDzq7MzEYBbrgQR7oE5ZHtyqhCf9LUgw0Kr7/8QWuNAdeDCJzjXRROU0hJczp +dOyfRlLbHmLLmGOnROlx6LsGNQ17zuz6SPi7ei8/ylhykawDOAGkM1+xFakmQhM= +-----END CERTIFICATE----- diff --git a/apps/emqx/test/emqx_ocsp_cache_SUITE_data/openssl_listeners.conf b/apps/emqx/test/emqx_ocsp_cache_SUITE_data/openssl_listeners.conf new file mode 100644 index 000000000..d26e12acf --- /dev/null +++ b/apps/emqx/test/emqx_ocsp_cache_SUITE_data/openssl_listeners.conf @@ -0,0 +1,14 @@ +listeners.ssl.default { + bind = "0.0.0.0:8883" + max_connections = 512000 + ssl_options { + keyfile = "{{ test_data_dir }}/server.key" + certfile = "{{ test_data_dir }}/server.pem" + cacertfile = "{{ test_data_dir }}/ca.pem" + ocsp { + enable_ocsp_stapling = true + issuer_pem = "{{ test_data_dir }}/ocsp-issuer.pem" + responder_url = "http://127.0.0.1:9877" + } + } +} diff --git a/apps/emqx/test/emqx_ocsp_cache_SUITE_data/server.key b/apps/emqx/test/emqx_ocsp_cache_SUITE_data/server.key new file mode 100644 index 000000000..d456ece72 --- /dev/null +++ b/apps/emqx/test/emqx_ocsp_cache_SUITE_data/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCnVPRWgP59GU15 +HddFwPZflFfcSkeuWU8tgKQhZcNoBli4lIfemuoV/hkGRVFexAiAw3/u5wvOaMaN +V8n9KxxgAUNLh5YaknpnNdhfQDyM0S5UJIbVeLzAQWxkBXpI3uBfW4WPSULRnVyR +psLEfl1qOklGOyuZfRbkkkkVwtJEmGEH0kz0fy6xenn3R3/mTeIbj+5TNqiBXWn1 +/qgTiNf2Ni7SE6Nk2lP4V8iofcBIrsp6KtEWdipGEJZeXCg/X0g/qVt15tF1l00M +uEWRHt1qGBELJJTcNzQvdqHAPz0AfQRjTtXyocw5+pFth8Q8a7gyjrjv5nhnpAKQ +msrt3vyNAgMBAAECggEABnWvIQ/Fw0qQxRYz00uJt1LguW5cqgxklBsdOvTUwFVO +Y4HIZP2R/9tZV/ahF4l10pK5g52DxSoiUB6Ne6qIY+RolqfbUZdKBmX7vmGadM02 +fqUSV3dbwghEiO/1Mo74FnZQB6IKZFEw26aWakN+k7VAUufB3SEJGzXSgHaO63ru +dFGSiYI8U+q+YnhUJjCnmI12fycNfy451TdUQtGZb6pNmm5HRUF6hpAV8Le9LojP +Ql9eacPpsrzU15X5ElCQZ/f9iNh1bplcISuhrULgKUKOvAVrBlEK67uRVy6g98xA +c/rgNLkbL/jZEsAc3/vHAyFgd3lABfwpBGLHej3QgQKBgQDFNYmfBNQr89HC5Zc+ +M6jXcAT/R+0GNczBTfC4iyNemwqsumSSRelNZ748UefKuS3F6Mvb2CBqE2LbB61G +hrnCffG2pARjZ491SefRwghhWWVGLP1p8KliLgOGBehA1REgJb+XULncjuHZuh4O +LVn3HVnWGxeBGg+yKa6Z4YQi3QKBgQDZN0O8ZcZY74lRJ0UjscD9mJ1yHlsssZag +njkX/f0GR/iVpfaIxQNC3gvWUy2LsU0He9sidcB0cfej0j/qZObQyFsCB0+utOgy ++hX7gokV2pes27WICbNWE2lJL4QZRJgvf82OaEy57kfDrm+eK1XaSZTZ10P82C9u +gAmMnontcQKBgGu29lhY9tqa7jOZ26Yp6Uri8JfO3XPK5u+edqEVvlfqL0Zw+IW8 +kdWpmIqx4f0kcA/tO4v03J+TvycLZmVjKQtGZ0PvCkaRRhY2K9yyMomZnmtaH4BB +5wKtR1do2pauyg/ZDnDDswD5OfsGYWw08TK8YVlEqu3lIjWZ9rguKVIxAoGAZYUk +zVqr10ks3pcCA2rCjkPT4lA5wKvHgI4ylPoKVfMxRY/pp4acvZXV5ne9o7pcDBFh +G7v5FPNnEFPlt4EtN4tMragJH9hBZgHoYEJkG6islweg0lHmVWaBIMlqbfzXO+v5 +gINSyNuLAvP2CvCqEXmubhnkFrpbgMOqsuQuBqECgYB3ss2PDhBF+5qoWgqymFof +1ovRPuQ9sPjWBn5IrCdoYITDnbBzBZERx7GLs6A/PUlWgST7jkb1PY/TxYSUfXzJ +SNd47q0mCQ+IUdqUbHgpK9b1ncwLMsnexpYZdHJWRLgnUhOx7OMjJc/4iLCAFCoN +3KJ7/V1keo7GBHOwnsFcCA== +-----END PRIVATE KEY----- diff --git a/apps/emqx/test/emqx_ocsp_cache_SUITE_data/server.pem b/apps/emqx/test/emqx_ocsp_cache_SUITE_data/server.pem new file mode 100644 index 000000000..38cc63534 --- /dev/null +++ b/apps/emqx/test/emqx_ocsp_cache_SUITE_data/server.pem @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIGCTCCA/GgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux +EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL +DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X +DTIzMDExMjEzMDgxNloXDTMzMDQxOTEzMDgxNloweDELMAkGA1UEBhMCU0UxEjAQ +BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN +eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExEjAQBgNVBAMMCWxv +Y2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKdU9FaA/n0Z +TXkd10XA9l+UV9xKR65ZTy2ApCFlw2gGWLiUh96a6hX+GQZFUV7ECIDDf+7nC85o +xo1Xyf0rHGABQ0uHlhqSemc12F9APIzRLlQkhtV4vMBBbGQFekje4F9bhY9JQtGd +XJGmwsR+XWo6SUY7K5l9FuSSSRXC0kSYYQfSTPR/LrF6efdHf+ZN4huP7lM2qIFd +afX+qBOI1/Y2LtITo2TaU/hXyKh9wEiuynoq0RZ2KkYQll5cKD9fSD+pW3Xm0XWX +TQy4RZEe3WoYEQsklNw3NC92ocA/PQB9BGNO1fKhzDn6kW2HxDxruDKOuO/meGek +ApCayu3e/I0CAwEAAaOCAagwggGkMAkGA1UdEwQCMAAwEQYJYIZIAYb4QgEBBAQD +AgZAMDMGCWCGSAGG+EIBDQQmFiRPcGVuU1NMIEdlbmVyYXRlZCBTZXJ2ZXIgQ2Vy +dGlmaWNhdGUwHQYDVR0OBBYEFGy5LQPzIelruJl7mL0mtUXM57XhMIGaBgNVHSME +gZIwgY+AFExwhjsVUom6tQ+Sqq6xMUETvnPzoXOkcTBvMQswCQYDVQQGEwJTRTES +MBAGA1UECAwJU3RvY2tob2xtMRIwEAYDVQQHDAlTdG9ja2hvbG0xEjAQBgNVBAoM +CU15T3JnTmFtZTERMA8GA1UECwwITXlSb290Q0ExETAPBgNVBAMMCE15Um9vdENB +ggIQADAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwOwYDVR0f +BDQwMjAwoC6gLIYqaHR0cDovL2xvY2FsaG9zdDo5ODc4L2ludGVybWVkaWF0ZS5j +cmwucGVtMDEGCCsGAQUFBwEBBCUwIzAhBggrBgEFBQcwAYYVaHR0cDovL2xvY2Fs +aG9zdDo5ODc3MA0GCSqGSIb3DQEBCwUAA4ICAQCX3EQgiCVqLhnCNd0pmptxXPxo +l1KyZkpdrFa/NgSqRhkuZSAkszwBDDS/gzkHFKEUhmqs6/UZwN4+Rr3LzrHonBiN +aQ6GeNNXZ/3xAQfUCwjjGmz9Sgw6kaX19Gnk2CjI6xP7T+O5UmsMI9hHUepC9nWa +XX2a0hsO/KOVu5ZZckI16Ek/jxs2/HEN0epYdvjKFAaVmzZZ5PATNjrPQXvPmq2r +x++La+3bXZsrH8P2FhPpM5t/IxKKW/Tlpgz92c2jVSIHF5khSA/MFDC+dk80OFmm +v4ZTPIMuZ//Q+wo0f9P48rsL9D27qS7CA+8pn9wu+cfnBDSt7JD5Yipa1gHz71fy +YTa9qRxIAPpzW2v7TFZE8eSKFUY9ipCeM2BbdmCQGmq4+v36b5TZoyjH4k0UVWGo +Gclos2cic5Vxi8E6hb7b7yZpjEfn/5lbCiGMfAnI6aoOyrWg6keaRA33kaLUEZiK +OgFNbPkjiTV0ZQyLXf7uK9YFhpVzJ0dv0CFNse8rZb7A7PLn8VrV/ZFnJ9rPoawn +t7ZGxC0d5BRSEyEeEgsQdxuY4m8OkE18zwhCkt2Qs3uosOWlIrYmqSEa0i/sPSQP +jiwB4nEdBrf8ZygzuYjT5T9YRSwhVox4spS/Av8Ells5JnkuKAhCVv9gHxYwbj0c +CzyLJgE1z9Tq63m+gQ== +-----END CERTIFICATE----- diff --git a/apps/emqx/test/emqx_schema_tests.erl b/apps/emqx/test/emqx_schema_tests.erl index a0d264662..5176f4fad 100644 --- a/apps/emqx/test/emqx_schema_tests.erl +++ b/apps/emqx/test/emqx_schema_tests.erl @@ -473,3 +473,43 @@ password_converter_test() -> ?assertEqual(<<"123">>, emqx_schema:password_converter(<<"123">>, #{})), ?assertThrow("must_quote", emqx_schema:password_converter(foobar, #{})), ok. + +url_type_test_() -> + [ + ?_assertEqual( + {ok, <<"http://some.server/">>}, + typerefl:from_string(emqx_schema:url(), <<"http://some.server/">>) + ), + ?_assertEqual( + {ok, <<"http://192.168.0.1/">>}, + typerefl:from_string(emqx_schema:url(), <<"http://192.168.0.1">>) + ), + ?_assertEqual( + {ok, <<"http://some.server/">>}, + typerefl:from_string(emqx_schema:url(), "http://some.server/") + ), + ?_assertEqual( + {ok, <<"http://some.server/">>}, + typerefl:from_string(emqx_schema:url(), <<"http://some.server">>) + ), + ?_assertEqual( + {ok, <<"http://some.server:9090/">>}, + typerefl:from_string(emqx_schema:url(), <<"http://some.server:9090">>) + ), + ?_assertEqual( + {ok, <<"https://some.server:9090/">>}, + typerefl:from_string(emqx_schema:url(), <<"https://some.server:9090">>) + ), + ?_assertEqual( + {ok, <<"https://some.server:9090/path?q=uery">>}, + typerefl:from_string(emqx_schema:url(), <<"https://some.server:9090/path?q=uery">>) + ), + ?_assertEqual( + {error, {unsupported_scheme, <<"postgres">>}}, + typerefl:from_string(emqx_schema:url(), <<"postgres://some.server:9090">>) + ), + ?_assertEqual( + {error, empty_host_not_allowed}, + typerefl:from_string(emqx_schema:url(), <<"">>) + ) + ]. diff --git a/apps/emqx/test/emqx_tls_lib_tests.erl b/apps/emqx/test/emqx_tls_lib_tests.erl index 5a81daf6a..5510e4027 100644 --- a/apps/emqx/test/emqx_tls_lib_tests.erl +++ b/apps/emqx/test/emqx_tls_lib_tests.erl @@ -117,7 +117,7 @@ ssl_files_failure_test_() -> %% empty string ?assertMatch( {error, #{ - reason := invalid_file_path_or_pem_string, which_options := [<<"keyfile">>] + reason := invalid_file_path_or_pem_string, which_options := [[<<"keyfile">>]] }}, emqx_tls_lib:ensure_ssl_files("/tmp", #{ <<"keyfile">> => <<>>, @@ -128,7 +128,7 @@ ssl_files_failure_test_() -> %% not valid unicode ?assertMatch( {error, #{ - reason := invalid_file_path_or_pem_string, which_options := [<<"keyfile">>] + reason := invalid_file_path_or_pem_string, which_options := [[<<"keyfile">>]] }}, emqx_tls_lib:ensure_ssl_files("/tmp", #{ <<"keyfile">> => <<255, 255>>, @@ -136,6 +136,18 @@ ssl_files_failure_test_() -> <<"cacertfile">> => bin(test_key()) }) ), + ?assertMatch( + {error, #{ + reason := invalid_file_path_or_pem_string, + which_options := [[<<"ocsp">>, <<"issuer_pem">>]] + }}, + emqx_tls_lib:ensure_ssl_files("/tmp", #{ + <<"keyfile">> => bin(test_key()), + <<"certfile">> => bin(test_key()), + <<"cacertfile">> => bin(test_key()), + <<"ocsp">> => #{<<"issuer_pem">> => <<255, 255>>} + }) + ), %% not printable ?assertMatch( {error, #{reason := invalid_file_path_or_pem_string}}, @@ -155,7 +167,8 @@ ssl_files_failure_test_() -> #{ <<"cacertfile">> => bin(TmpFile), <<"keyfile">> => bin(TmpFile), - <<"certfile">> => bin(TmpFile) + <<"certfile">> => bin(TmpFile), + <<"ocsp">> => #{<<"issuer_pem">> => bin(TmpFile)} } ) ) @@ -170,22 +183,29 @@ ssl_files_save_delete_test() -> SSL0 = #{ <<"keyfile">> => Key, <<"certfile">> => Key, - <<"cacertfile">> => Key + <<"cacertfile">> => Key, + <<"ocsp">> => #{<<"issuer_pem">> => Key} }, Dir = filename:join(["/tmp", "ssl-test-dir"]), {ok, SSL} = emqx_tls_lib:ensure_ssl_files(Dir, SSL0), - File = maps:get(<<"keyfile">>, SSL), - ?assertMatch(<<"/tmp/ssl-test-dir/key-", _:16/binary>>, File), - ?assertEqual({ok, bin(test_key())}, file:read_file(File)), + FileKey = maps:get(<<"keyfile">>, SSL), + ?assertMatch(<<"/tmp/ssl-test-dir/key-", _:16/binary>>, FileKey), + ?assertEqual({ok, bin(test_key())}, file:read_file(FileKey)), + FileIssuerPem = emqx_map_lib:deep_get([<<"ocsp">>, <<"issuer_pem">>], SSL), + ?assertMatch(<<"/tmp/ssl-test-dir/ocsp_issuer_pem-", _:16/binary>>, FileIssuerPem), + ?assertEqual({ok, bin(test_key())}, file:read_file(FileIssuerPem)), %% no old file to delete ok = emqx_tls_lib:delete_ssl_files(Dir, SSL, undefined), - ?assertEqual({ok, bin(test_key())}, file:read_file(File)), + ?assertEqual({ok, bin(test_key())}, file:read_file(FileKey)), + ?assertEqual({ok, bin(test_key())}, file:read_file(FileIssuerPem)), %% old and new identical, no delete ok = emqx_tls_lib:delete_ssl_files(Dir, SSL, SSL), - ?assertEqual({ok, bin(test_key())}, file:read_file(File)), + ?assertEqual({ok, bin(test_key())}, file:read_file(FileKey)), + ?assertEqual({ok, bin(test_key())}, file:read_file(FileIssuerPem)), %% new is gone, delete old ok = emqx_tls_lib:delete_ssl_files(Dir, undefined, SSL), - ?assertEqual({error, enoent}, file:read_file(File)), + ?assertEqual({error, enoent}, file:read_file(FileKey)), + ?assertEqual({error, enoent}, file:read_file(FileIssuerPem)), %% test idempotence ok = emqx_tls_lib:delete_ssl_files(Dir, undefined, SSL), ok. @@ -198,7 +218,8 @@ ssl_files_handle_non_generated_file_test() -> SSL0 = #{ <<"keyfile">> => TmpKeyFile, <<"certfile">> => TmpKeyFile, - <<"cacertfile">> => TmpKeyFile + <<"cacertfile">> => TmpKeyFile, + <<"ocsp">> => #{<<"issuer_pem">> => TmpKeyFile} }, Dir = filename:join(["/tmp", "ssl-test-dir-00"]), {ok, SSL2} = emqx_tls_lib:ensure_ssl_files(Dir, SSL0), @@ -216,24 +237,32 @@ ssl_file_replace_test() -> SSL0 = #{ <<"keyfile">> => Key1, <<"certfile">> => Key1, - <<"cacertfile">> => Key1 + <<"cacertfile">> => Key1, + <<"ocsp">> => #{<<"issuer_pem">> => Key1} }, SSL1 = #{ <<"keyfile">> => Key2, <<"certfile">> => Key2, - <<"cacertfile">> => Key2 + <<"cacertfile">> => Key2, + <<"ocsp">> => #{<<"issuer_pem">> => Key2} }, Dir = filename:join(["/tmp", "ssl-test-dir2"]), {ok, SSL2} = emqx_tls_lib:ensure_ssl_files(Dir, SSL0), {ok, SSL3} = emqx_tls_lib:ensure_ssl_files(Dir, SSL1), File1 = maps:get(<<"keyfile">>, SSL2), File2 = maps:get(<<"keyfile">>, SSL3), + IssuerPem1 = emqx_map_lib:deep_get([<<"ocsp">>, <<"issuer_pem">>], SSL2), + IssuerPem2 = emqx_map_lib:deep_get([<<"ocsp">>, <<"issuer_pem">>], SSL3), ?assert(filelib:is_regular(File1)), ?assert(filelib:is_regular(File2)), + ?assert(filelib:is_regular(IssuerPem1)), + ?assert(filelib:is_regular(IssuerPem2)), %% delete old file (File1, in SSL2) ok = emqx_tls_lib:delete_ssl_files(Dir, SSL3, SSL2), ?assertNot(filelib:is_regular(File1)), ?assert(filelib:is_regular(File2)), + ?assertNot(filelib:is_regular(IssuerPem1)), + ?assert(filelib:is_regular(IssuerPem2)), ok. bin(X) -> iolist_to_binary(X). diff --git a/apps/emqx_authz/test/emqx_authz_file_SUITE.erl b/apps/emqx_authz/test/emqx_authz_file_SUITE.erl index 5b5d2618c..124fe904f 100644 --- a/apps/emqx_authz/test/emqx_authz_file_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_file_SUITE.erl @@ -55,7 +55,7 @@ init_per_suite(Config) -> end_per_suite(_Config) -> ok = emqx_authz_test_lib:restore_authorizers(), - ok = emqx_common_test_helpers:stop_apps([emqx_authz]). + ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]). init_per_testcase(_TestCase, Config) -> ok = emqx_authz_test_lib:reset_authorizers(), diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index e91da9829..0757113f3 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -52,7 +52,7 @@ init_per_suite(Config) -> end_per_suite(_Config) -> ok = emqx_authz_test_lib:restore_authorizers(), ok = stop_apps([emqx_resource, cowboy]), - ok = emqx_common_test_helpers:stop_apps([emqx_authz]). + ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]). set_special_configs(emqx_authz) -> ok = emqx_authz_test_lib:reset_authorizers(); diff --git a/apps/emqx_authz/test/emqx_authz_mnesia_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mnesia_SUITE.erl index d7b31b5b5..2b7fce309 100644 --- a/apps/emqx_authz/test/emqx_authz_mnesia_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mnesia_SUITE.erl @@ -36,7 +36,7 @@ init_per_suite(Config) -> end_per_suite(_Config) -> ok = emqx_authz_test_lib:restore_authorizers(), - ok = emqx_common_test_helpers:stop_apps([emqx_authz]). + ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]). init_per_testcase(_TestCase, Config) -> ok = emqx_authz_test_lib:reset_authorizers(), diff --git a/apps/emqx_authz/test/emqx_authz_mongodb_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongodb_SUITE.erl index c685e8237..9ffeacf45 100644 --- a/apps/emqx_authz/test/emqx_authz_mongodb_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mongodb_SUITE.erl @@ -50,7 +50,7 @@ init_per_suite(Config) -> end_per_suite(_Config) -> ok = emqx_authz_test_lib:restore_authorizers(), ok = stop_apps([emqx_resource]), - ok = emqx_common_test_helpers:stop_apps([emqx_authz]). + ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]). set_special_configs(emqx_authz) -> ok = emqx_authz_test_lib:reset_authorizers(); diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index ad9a23377..d276a2e1b 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -57,7 +57,7 @@ end_per_suite(_Config) -> ok = emqx_authz_test_lib:restore_authorizers(), ok = emqx_resource:remove_local(?MYSQL_RESOURCE), ok = stop_apps([emqx_resource]), - ok = emqx_common_test_helpers:stop_apps([emqx_authz]). + ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]). init_per_testcase(_TestCase, Config) -> ok = emqx_authz_test_lib:reset_authorizers(), diff --git a/apps/emqx_authz/test/emqx_authz_postgresql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_postgresql_SUITE.erl index f17c2de1c..0ef21360c 100644 --- a/apps/emqx_authz/test/emqx_authz_postgresql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_postgresql_SUITE.erl @@ -57,7 +57,7 @@ end_per_suite(_Config) -> ok = emqx_authz_test_lib:restore_authorizers(), ok = emqx_resource:remove_local(?PGSQL_RESOURCE), ok = stop_apps([emqx_resource]), - ok = emqx_common_test_helpers:stop_apps([emqx_authz]). + ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]). init_per_testcase(_TestCase, Config) -> ok = emqx_authz_test_lib:reset_authorizers(), diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index b480e0262..d68ea342e 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -58,7 +58,7 @@ end_per_suite(_Config) -> ok = emqx_authz_test_lib:restore_authorizers(), ok = emqx_resource:remove_local(?REDIS_RESOURCE), ok = stop_apps([emqx_resource]), - ok = emqx_common_test_helpers:stop_apps([emqx_authz]). + ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]). init_per_testcase(_TestCase, Config) -> ok = emqx_authz_test_lib:reset_authorizers(), diff --git a/apps/emqx_bridge/test/emqx_bridge_webhook_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_webhook_SUITE.erl index 4c349c7a0..61df9bd29 100644 --- a/apps/emqx_bridge/test/emqx_bridge_webhook_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_webhook_SUITE.erl @@ -42,6 +42,8 @@ init_per_suite(_Config) -> []. end_per_suite(_Config) -> + ok = emqx_config:put([bridges], #{}), + ok = emqx_config:put_raw([bridges], #{}), ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_bridge]), ok = emqx_connector_test_helpers:stop_apps([emqx_resource]), _ = application:stop(emqx_connector), diff --git a/apps/emqx_dashboard/src/emqx_dashboard_listener.erl b/apps/emqx_dashboard/src/emqx_dashboard_listener.erl index 112b3ad58..eac4f845f 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_listener.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_listener.erl @@ -163,7 +163,7 @@ diff_listeners(Type, Stop, Start) -> {#{Type => Stop}, #{Type => Start}}. ensure_ssl_cert(#{<<"listeners">> := #{<<"https">> := #{<<"enable">> := true}}} = Conf) -> Https = emqx_map_lib:deep_get([<<"listeners">>, <<"https">>], Conf, undefined), - Opts = #{required_keys => [<<"keyfile">>, <<"certfile">>, <<"cacertfile">>]}, + Opts = #{required_keys => [[<<"keyfile">>], [<<"certfile">>], [<<"cacertfile">>]]}, case emqx_tls_lib:ensure_ssl_files(?DIR, Https, Opts) of {ok, undefined} -> {error, <<"ssl_cert_not_found">>}; diff --git a/apps/emqx_exhook/test/emqx_exhook_SUITE.erl b/apps/emqx_exhook/test/emqx_exhook_SUITE.erl index aaa8649c0..d12f99917 100644 --- a/apps/emqx_exhook/test/emqx_exhook_SUITE.erl +++ b/apps/emqx_exhook/test/emqx_exhook_SUITE.erl @@ -24,13 +24,13 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). -include_lib("emqx/include/emqx_hooks.hrl"). +-include_lib("emqx_conf/include/emqx_conf.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl"). -define(DEFAULT_CLUSTER_NAME_ATOM, emqxcl). -define(OTHER_CLUSTER_NAME_ATOM, test_emqx_cluster). -define(OTHER_CLUSTER_NAME_STRING, "test_emqx_cluster"). --define(CLUSTER_RPC_SHARD, emqx_cluster_rpc_shard). -define(CONF_DEFAULT, << "\n" @@ -54,6 +54,8 @@ "}\n" >>). +-import(emqx_common_test_helpers, [on_exit/1]). + %%-------------------------------------------------------------------- %% Setups %%-------------------------------------------------------------------- @@ -89,7 +91,7 @@ init_per_testcase(_, Config) -> timer:sleep(200), Config. -end_per_testcase(_, Config) -> +end_per_testcase(_, _Config) -> case erlang:whereis(node()) of undefined -> ok; @@ -97,7 +99,8 @@ end_per_testcase(_, Config) -> erlang:unlink(P), erlang:exit(P, kill) end, - Config. + emqx_common_test_helpers:call_janitor(), + ok. load_cfg(Cfg) -> ok = emqx_common_test_helpers:load_config(emqx_exhook_schema, Cfg). @@ -300,6 +303,12 @@ t_cluster_name(_) -> emqx_common_test_helpers:stop_apps([emqx, emqx_exhook]), emqx_common_test_helpers:start_apps([emqx, emqx_exhook], SetEnvFun), + on_exit(fun() -> + emqx_common_test_helpers:stop_apps([emqx, emqx_exhook]), + load_cfg(?CONF_DEFAULT), + emqx_common_test_helpers:start_apps([emqx_exhook]), + mria:wait_for_tables([?CLUSTER_MFA, ?CLUSTER_COMMIT]) + end), ?assertEqual(?OTHER_CLUSTER_NAME_STRING, emqx_sys:cluster_name()), diff --git a/apps/emqx_gateway/test/emqx_gateway_authn_SUITE.erl b/apps/emqx_gateway/test/emqx_gateway_authn_SUITE.erl index 208262f22..2427a10ee 100644 --- a/apps/emqx_gateway/test/emqx_gateway_authn_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_gateway_authn_SUITE.erl @@ -77,7 +77,7 @@ init_per_suite(Config) -> end_per_suite(Config) -> emqx_gateway_auth_ct:stop(), emqx_config:erase(gateway), - emqx_mgmt_api_test_util:end_suite([cowboy, emqx_authn, emqx_gateway]), + emqx_mgmt_api_test_util:end_suite([cowboy, emqx_conf, emqx_authn, emqx_gateway]), Config. init_per_testcase(_Case, Config) -> diff --git a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl index abab3f9b0..782f493fe 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl @@ -24,12 +24,15 @@ init_suite() -> init_suite([]). init_suite(Apps) -> - init_suite(Apps, fun set_special_configs/1). + init_suite(Apps, fun set_special_configs/1, #{}). -init_suite(Apps, SetConfigs) -> +init_suite(Apps, SetConfigs) when is_function(SetConfigs) -> + init_suite(Apps, SetConfigs, #{}). + +init_suite(Apps, SetConfigs, Opts) -> mria:start(), application:load(emqx_management), - emqx_common_test_helpers:start_apps(Apps ++ [emqx_dashboard], SetConfigs), + emqx_common_test_helpers:start_apps(Apps ++ [emqx_dashboard], SetConfigs, Opts), emqx_common_test_http:create_default_app(). end_suite() -> diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index ff7e1d347..e7c252fa9 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -295,8 +295,15 @@ t_batch_query_counter(_) -> ok end, fun(Trace) -> - QueryTrace = ?of_kind(call_batch_query, Trace), - ?assertMatch([#{batch := BatchReq} | _] when length(BatchReq) > 1, QueryTrace) + QueryTrace = [ + Event + || Event = #{ + ?snk_kind := call_batch_query, + batch := BatchReq + } <- Trace, + length(BatchReq) > 1 + ], + ?assertMatch([_ | _], QueryTrace) end ), {ok, NMsgs} = emqx_resource:query(?ID, get_counter), @@ -648,19 +655,18 @@ t_query_counter_async_inflight_batch(_) -> 5_000 ), fun(Trace) -> - QueryTrace = ?of_kind(call_batch_query_async, Trace), - ?assertMatch( - [ - #{ - batch := [ - {query, _, {inc_counter, 1}, _, _}, - {query, _, {inc_counter, 1}, _, _} - ] - } - | _ - ], - QueryTrace - ) + QueryTrace = [ + Event + || Event = #{ + ?snk_kind := call_batch_query_async, + batch := [ + {query, _, {inc_counter, 1}, _, _}, + {query, _, {inc_counter, 1}, _, _} + ] + } <- + Trace + ], + ?assertMatch([_ | _], QueryTrace) end ), tap_metrics(?LINE), @@ -1275,10 +1281,11 @@ t_retry_batch(_Config) -> %% each time should be the original batch (no duplicate %% elements or reordering). ExpectedSeenPayloads = lists:flatten(lists:duplicate(4, Payloads)), - ?assertEqual( - ExpectedSeenPayloads, - ?projection(n, ?of_kind(connector_demo_batch_inc_individual, Trace)) + Trace1 = lists:sublist( + ?projection(n, ?of_kind(connector_demo_batch_inc_individual, Trace)), + length(ExpectedSeenPayloads) ), + ?assertEqual(ExpectedSeenPayloads, Trace1), ?assertMatch( [#{n := ExpectedCount}], ?of_kind(connector_demo_inc_counter, Trace) diff --git a/apps/emqx_retainer/rebar.config b/apps/emqx_retainer/rebar.config index 7e791f90f..65de71fdd 100644 --- a/apps/emqx_retainer/rebar.config +++ b/apps/emqx_retainer/rebar.config @@ -27,7 +27,7 @@ {profiles, [ {test, [ {deps, [ - {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.5.0"}}} + {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.8.5"}}} ]} ]} ]}. diff --git a/changes/ce/feat-10128.en.md b/changes/ce/feat-10128.en.md new file mode 100644 index 000000000..705e36137 --- /dev/null +++ b/changes/ce/feat-10128.en.md @@ -0,0 +1 @@ +Add support for OCSP stapling and CRL check for SSL MQTT listeners. diff --git a/changes/ce/feat-10128.zh.md b/changes/ce/feat-10128.zh.md new file mode 100644 index 000000000..d875bd2ff --- /dev/null +++ b/changes/ce/feat-10128.zh.md @@ -0,0 +1 @@ +为 SSL MQTT 监听器增加对 OCSP Stapling 的支持。 diff --git a/mix.exs b/mix.exs index 1befd534a..e946c257b 100644 --- a/mix.exs +++ b/mix.exs @@ -61,7 +61,7 @@ defmodule EMQXUmbrella.MixProject do {:ecpool, github: "emqx/ecpool", tag: "0.5.3", override: true}, {:replayq, github: "emqx/replayq", tag: "0.3.7", override: true}, {:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true}, - {:emqtt, github: "emqx/emqtt", tag: "1.8.2", override: true}, + {:emqtt, github: "emqx/emqtt", tag: "1.8.5", override: true}, {:rulesql, github: "emqx/rulesql", tag: "0.1.4"}, {:observer_cli, "1.7.1"}, {:system_monitor, github: "ieQu1/system_monitor", tag: "3.0.3"}, diff --git a/rebar.config b/rebar.config index 3eca8fcae..8bfcc7960 100644 --- a/rebar.config +++ b/rebar.config @@ -63,7 +63,7 @@ , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.3"}}} , {replayq, {git, "https://github.com/emqx/replayq.git", {tag, "0.3.7"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} - , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.8.2"}}} + , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.8.5"}}} , {rulesql, {git, "https://github.com/emqx/rulesql", {tag, "0.1.4"}}} , {observer_cli, "1.7.1"} % NOTE: depends on recon 2.5.x , {system_monitor, {git, "https://github.com/ieQu1/system_monitor", {tag, "3.0.3"}}} diff --git a/scripts/spellcheck/dicts/emqx.txt b/scripts/spellcheck/dicts/emqx.txt index 5975ebd1b..b027f92ec 100644 --- a/scripts/spellcheck/dicts/emqx.txt +++ b/scripts/spellcheck/dicts/emqx.txt @@ -12,6 +12,7 @@ CMD CN CONNACK CoAP +CRLs Cygwin DES DN @@ -41,6 +42,7 @@ Makefile MitM Multicast NIF +OCSP OTP PEM PINGREQ