feat: add ocsp stapling and crl support to mqtt ssl listener

This commit is contained in:
Thales Macedo Garitezi 2023-02-27 17:59:37 -03:00
parent 422597a441
commit 52263a0448
21 changed files with 1999 additions and 26 deletions

View File

@ -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."""

View File

@ -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.

View File

@ -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)
]
}}.

View File

@ -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.

View File

@ -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.

View File

@ -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) ->
)}
] ++
[
Field
|| not IsRanchListener,
Field <- [
{"gc_after_handshake",
sc(boolean(), #{
default => false,
desc => ?DESC(server_ssl_opts_schema_gc_after_handshake)
})}
|| not IsRanchListener
})},
{"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.

View File

@ -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.

View File

@ -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).

View File

@ -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-----

View File

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
rkE8Bz5a/iwO+QwdO5Q2cp5+0nm41A==
-----END PRIVATE KEY-----

View File

@ -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-----

View File

@ -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

View File

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
xBO4CmF79pAcAXvDoRB619Flx5/G
-----END PRIVATE KEY-----

View File

@ -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-----

View File

@ -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"
}
}
}

View File

@ -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-----

View File

@ -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-----

View File

@ -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() ->

View File

@ -0,0 +1 @@
Add support for OCSP stapling and CRL check for SSL MQTT listeners.

View File

@ -0,0 +1 @@
为SSL MQTT监听器增加对OCSP订书和CRL检查的支持。

View File

@ -12,6 +12,7 @@ CMD
CN
CONNACK
CoAP
CRLs
Cygwin
DES
DN
@ -41,6 +42,7 @@ Makefile
MitM
Multicast
NIF
OCSP
OTP
PEM
PINGREQ