From 52263a044865ba6d7262d2945c83d69432b9af6f Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 27 Feb 2023 17:59:37 -0300 Subject: [PATCH] feat: add ocsp stapling and crl support to mqtt ssl listener --- apps/emqx/i18n/emqx_schema_i18n.conf | 57 ++ apps/emqx/src/emqx_const_v1.erl | 24 + apps/emqx/src/emqx_kernel_sup.erl | 3 +- apps/emqx/src/emqx_listeners.erl | 13 +- apps/emqx/src/emqx_ocsp_cache.erl | 521 ++++++++++ apps/emqx/src/emqx_schema.erl | 122 ++- apps/emqx/test/emqx_common_test_helpers.erl | 37 +- apps/emqx/test/emqx_ocsp_cache_SUITE.erl | 908 ++++++++++++++++++ .../test/emqx_ocsp_cache_SUITE_data/ca.pem | 68 ++ .../emqx_ocsp_cache_SUITE_data/client.key | 52 + .../emqx_ocsp_cache_SUITE_data/client.pem | 38 + .../test/emqx_ocsp_cache_SUITE_data/index.txt | 6 + .../ocsp-issuer.key | 52 + .../ocsp-issuer.pem | 34 + .../openssl_listeners.conf | 14 + .../emqx_ocsp_cache_SUITE_data/server.key | 28 + .../emqx_ocsp_cache_SUITE_data/server.pem | 35 + .../test/emqx_mgmt_api_test_util.erl | 9 +- changes/ce/feat-10067.en.md | 1 + changes/ce/feat-10067.zh.md | 1 + scripts/spellcheck/dicts/emqx.txt | 2 + 21 files changed, 1999 insertions(+), 26 deletions(-) create mode 100644 apps/emqx/src/emqx_const_v1.erl create mode 100644 apps/emqx/src/emqx_ocsp_cache.erl create mode 100644 apps/emqx/test/emqx_ocsp_cache_SUITE.erl create mode 100644 apps/emqx/test/emqx_ocsp_cache_SUITE_data/ca.pem create mode 100644 apps/emqx/test/emqx_ocsp_cache_SUITE_data/client.key create mode 100644 apps/emqx/test/emqx_ocsp_cache_SUITE_data/client.pem create mode 100644 apps/emqx/test/emqx_ocsp_cache_SUITE_data/index.txt create mode 100644 apps/emqx/test/emqx_ocsp_cache_SUITE_data/ocsp-issuer.key create mode 100644 apps/emqx/test/emqx_ocsp_cache_SUITE_data/ocsp-issuer.pem create mode 100644 apps/emqx/test/emqx_ocsp_cache_SUITE_data/openssl_listeners.conf create mode 100644 apps/emqx/test/emqx_ocsp_cache_SUITE_data/server.key create mode 100644 apps/emqx/test/emqx_ocsp_cache_SUITE_data/server.pem create mode 100644 changes/ce/feat-10067.en.md create mode 100644 changes/ce/feat-10067.zh.md diff --git a/apps/emqx/i18n/emqx_schema_i18n.conf b/apps/emqx/i18n/emqx_schema_i18n.conf index b57698327..0690919bb 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 OCSP stapling for the listener. If set to true," + " requires defining the OCSP responder URL and issuer PEM path." + zh: "是否为监听器启用OCSP装订功能。 如果设置为 true," + "需要定义OCSP响应者URL和发行者PEM路径。" + } + label: { + en: "Enable OCSP Stapling" + zh: "启用OCSP订书机" + } +} + +server_ssl_opts_schema_ocsp_responder_url { + desc { + en: "URL for the OCSP responder to check the server certificate against." + zh: "用于检查服务器证书的OCSP响应器的URL。" + } + label: { + en: "OCSP Responder URL" + zh: "OCSP响应者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/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..2fe3aa5d5 --- /dev/null +++ b/apps/emqx/src/emqx_ocsp_cache.erl @@ -0,0 +1,521 @@ +%%-------------------------------------------------------------------- +%% 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. + +-define(WITH_LISTENER_CONFIG(ListenerID, ConfPath, Pattern, ErrorResp, Action), + 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); + Pattern -> + Action; + OtherConfig -> + ?SLOG(error, #{ + msg => "listener_config_inconsistent", + listener_id => ListenerID, + config => OtherConfig + }), + (ErrorResp) + end; + _Err -> + ?SLOG(error, #{ + msg => "listener_id_not_found", + listener_id => ListenerID + }), + (ErrorResp) + end +). + +%% 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]}), + _ = ets:new(?CACHE_TAB, [ + named_table, + protected, + {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). + +cache_key(ListenerID) -> + ?WITH_LISTENER_CONFIG( + ListenerID, + [ssl_options], + #{certfile := ServerCertPemPath}, + error, + begin + #'Certificate'{ + tbsCertificate = + #'TBSCertificate'{ + signature = Signature + } + } = read_server_cert(ServerCertPemPath), + {ok, {ocsp_response, Signature}} + 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} -> + Fn(Params) + end. + +get_refresh_params(ListenerID, undefined = _Conf) -> + %% during normal periodic refreshes, we read from the emqx config. + ?WITH_LISTENER_CONFIG( + ListenerID, + [ssl_options], + #{ + ocsp := #{ + issuer_pem := IssuerPemPath, + responder_url := ResponderURL, + refresh_http_timeout := HTTPTimeout + }, + certfile := ServerCertPemPath + }, + error, + {ok, #{ + issuer_pem => IssuerPemPath, + responder_url => ResponderURL, + refresh_http_timeout => HTTPTimeout, + server_certfile => ServerCertPemPath + }} + ); +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..6412711a6 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -810,7 +810,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 +1294,56 @@ 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( + binary(), + #{ + required => false, + validator => fun ocsp_responder_url_validator/1, + converter => fun + (undefined, _Opts) -> + undefined; + (URL, _Opts) -> + uri_string:normalize(URL) + end, + 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 +2067,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 +2251,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) -> @@ -2865,3 +2965,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/test/emqx_common_test_helpers.erl b/apps/emqx/test/emqx_common_test_helpers.erl index 8353c2895..c26e63a62 100644 --- a/apps/emqx/test/emqx_common_test_helpers.erl +++ b/apps/emqx/test/emqx_common_test_helpers.erl @@ -29,6 +29,7 @@ boot_modules/1, start_apps/1, start_apps/2, + start_apps/3, stop_apps/1, reload/2, app_path/2, @@ -36,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([ @@ -185,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 @@ -205,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, _} -> @@ -248,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 = @@ -337,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() -> @@ -479,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..1f5e34548 --- /dev/null +++ b/apps/emqx/test/emqx_ocsp_cache_SUITE.erl @@ -0,0 +1,908 @@ +%%-------------------------------------------------------------------- +%% 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(), + {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 + ), + {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 +%%-------------------------------------------------------------------- + +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). + +spawn_openssl_client(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, + open_port( + {spawn_executable, Openssl}, + [ + {args, + [ + "s_client", + "-connect", + "localhost:8883", + %% needed to trigger `sni_fun' + "-servername", + "localhost", + TLSVsn, + "-CAfile", + Cacert, + "-cert", + ClientCert, + "-key", + ClientKey + ] ++ StatusOpt}, + 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) -> + ClientPort = spawn_openssl_client(TLSVsn, WithRequestStatus, Config), + {os_pid, ClientOSPid} = erlang:port_info(ClientPort, os_pid), + try + timer:sleep(timer:seconds(1)), + {messages, Messages} = process_info(self(), messages), + OCSPOutput0 = [ + Output + || {_Port, {data, Output}} <- Messages, + re:run(Output, "OCSP response:") =/= nomatch + ], + ?assertMatch( + [_], + OCSPOutput0, + #{all_messages => Messages} + ), + [OCSPOutput] = OCSPOutput0, + ?assertMatch( + {match, _}, + re:run(OCSPOutput, "OCSP Response Status: successful"), + #{all_messages => Messages} + ), + ?assertMatch( + {match, _}, + re:run(OCSPOutput, "Cert Status: good"), + #{all_messages => Messages} + ), + ok + after + catch kill_pid(ClientOSPid) + end; +test_ocsp_connection(TLSVsn, WithRequestStatus = false, Config) -> + ClientPort = spawn_openssl_client(TLSVsn, WithRequestStatus, Config), + {os_pid, ClientOSPid} = erlang:port_info(ClientPort, os_pid), + try + timer:sleep(timer:seconds(1)), + {messages, Messages} = process_info(self(), messages), + OCSPOutput = [ + Output + || {_Port, {data, Output}} <- Messages, + re:run(Output, "OCSP response:") =/= nomatch + ], + ?assertEqual( + [], + OCSPOutput, + #{all_messages => Messages} + ), + ok + after + catch kill_pid(ClientOSPid) + end. + +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) -> + ?assertMatch( + [_, _], + ?of_kind(ocsp_http_fetch_and_cache, Trace) + ), + assert_http_get(2), + 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"]), + IssuerPem = filename:join([DataDir, "ocsp-issuer.pem"]), + + %% 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, + <<"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 + ), + 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_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_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/changes/ce/feat-10067.en.md b/changes/ce/feat-10067.en.md new file mode 100644 index 000000000..705e36137 --- /dev/null +++ b/changes/ce/feat-10067.en.md @@ -0,0 +1 @@ +Add support for OCSP stapling and CRL check for SSL MQTT listeners. diff --git a/changes/ce/feat-10067.zh.md b/changes/ce/feat-10067.zh.md new file mode 100644 index 000000000..d0efe4d5b --- /dev/null +++ b/changes/ce/feat-10067.zh.md @@ -0,0 +1 @@ +为SSL MQTT监听器增加对OCSP订书和CRL检查的支持。 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