chore(oscp_crl): port OCSP and CRL features to CE repo (re4.4)

This commit is contained in:
Thales Macedo Garitezi 2022-11-03 10:10:16 -03:00
parent 5bba281c2b
commit af4141bef4
43 changed files with 2267 additions and 19 deletions

View File

@ -3,7 +3,7 @@ version: '3.9'
services:
erlang:
container_name: erlang
image: ghcr.io/emqx/emqx-builder/4.4-19:24.1.5-3-ubuntu20.04
image: ghcr.io/emqx/emqx-builder/4.4-20:24.3.4.2-1-ubuntu20.04
env_file:
- conf.env
environment:

View File

@ -15,8 +15,8 @@ PROFILE="$1"
VSN="$2"
OLD_VSN="$3"
PACKAGE_PATH="$4"
FROM_OTP_VSN="${5:-24.1.5-3}"
TO_OTP_VSN="${6:-24.1.5-3}"
FROM_OTP_VSN="${5:-24.3.4.2-1}"
TO_OTP_VSN="${6:-24.3.4.2-1}"
TEMPDIR=$(mktemp -d)
trap '{ rm -rf -- "$TEMPDIR"; }' EXIT

View File

@ -29,7 +29,7 @@ jobs:
- uses: actions/checkout@v2
- uses: erlef/setup-beam@v1
with:
otp-version: "24.1.5"
otp-version: "24.3.4.2"
- name: prepare
id: prepare
run: |

View File

@ -1 +1 @@
erlang 24.1.5-3
erlang 24.3.4.2-1

View File

@ -3,7 +3,7 @@ REBAR = $(CURDIR)/rebar3
BUILD = $(CURDIR)/build
SCRIPTS = $(CURDIR)/scripts
export EMQX_RELUP ?= true
export EMQX_DEFAULT_BUILDER = ghcr.io/emqx/emqx-builder/4.4-19:24.1.5-3-alpine3.15.1
export EMQX_DEFAULT_BUILDER = ghcr.io/emqx/emqx-builder/4.4-20:24.3.4.2-1-alpine3.15.1
export EMQX_DEFAULT_RUNNER = alpine:3.15.1
export OTP_VSN ?= $(shell $(CURDIR)/scripts/get-otp-vsn.sh)
export PKG_VSN ?= $(shell $(CURDIR)/pkg-vsn.sh)

View File

@ -19,7 +19,7 @@
-include("emqx_auth_ldap.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("eldap/include/eldap.hrl").
-include_lib("eldap2/include/eldap.hrl").
-include_lib("emqx/include/logger.hrl").
-export([ check_acl/5

View File

@ -19,7 +19,7 @@
-include("emqx_auth_ldap.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("eldap/include/eldap.hrl").
-include_lib("eldap2/include/eldap.hrl").
-include_lib("emqx/include/logger.hrl").
-import(proplists, [get_value/2]).

2
build
View File

@ -268,7 +268,7 @@ make_docker() {
## Name Default Example
## ---------------------------------------------------------------------
## EMQX_BASE_IMAGE current os centos:7
## EMQX_ZIP_PACKAGE _packages/<current-zip-target> /tmp/emqx-4.4.0-otp24.1.5-3-el7-amd64.zip
## EMQX_ZIP_PACKAGE _packages/<current-zip-target> /tmp/emqx-4.4.0-otp24.3.4.2-1-el7-amd64.zip
## EMQX_IMAGE_TAG emqx/emqx:<current-vns-rel> emqx/emqx:testing-tag
##
make_docker_testing() {

View File

@ -1,4 +1,4 @@
ARG BUILD_FROM=ghcr.io/emqx/emqx-builder/4.4-19:24.1.5-3-alpine3.15.1
ARG BUILD_FROM=ghcr.io/emqx/emqx-builder/4.4-20:24.3.4.2-1-alpine3.15.1
ARG RUN_FROM=alpine:3.15.1
FROM ${BUILD_FROM} AS builder

View File

@ -1521,6 +1521,27 @@ listener.ssl.external.certfile = {{ platform_etc_dir }}/certs/cert.pem
## Value: File
listener.ssl.external.cacertfile = {{ platform_etc_dir }}/certs/cacert.pem
## URL for the OCSP responder to check the server certificate against.
##
## Value: String
## listener.ssl.external.ocsp_responder_url = http://my.ocsp.responder.com
## Path to the file containing PEM-encoded certificate of the OCSP
## issuer for the server certificate.
##
## Value: File
## listener.ssl.external.ocsp_issuer_pem = {{ platform_etc_dir }}/certs/ocsp-issuer.pem
## The period to refresh the OCSP response for the server.
##
## Value: Duration
## listener.ssl.external.ocsp_refresh_interval = 5m
## The timeout for the HTTP request when checking OCSP responses.
##
## Value: Duration
## listener.ssl.external.ocsp_refresh_http_timeout = 15s
## The Ephemeral Diffie-Helman key exchange is a very effective way of
## ensuring Forward Secrecy by exchanging a set of keys that never hit
## the wire. Since the DH key is effectively signed by the private key,

View File

@ -852,6 +852,14 @@ end}.
{datatype, string}
]}.
{mapping, "clientid_enrichment_module", "emqx.clientid_enrichment_module", [
{datatype, atom}
]}.
{mapping, "special_auth_module", "emqx.special_auth_module", [
{datatype, atom}
]}.
%% @doc Allow anonymous authentication.
{mapping, "allow_anonymous", "emqx.allow_anonymous", [
{default, false},
@ -1664,6 +1672,49 @@ end}.
{datatype, {duration, ms}}
]}.
{mapping, "listener.ssl.$name.ocsp_responder_url", "emqx.listeners", [
{datatype, string}
]}.
{mapping, "listener.ssl.$name.ocsp_issuer_pem", "emqx.listeners", [
{datatype, file}
]}.
{mapping, "listener.ssl.$name.ocsp_refresh_interval", "emqx.listeners", [
{default, "5m"},
{datatype, {duration, ms}}
]}.
{mapping, "listener.ssl.$name.ocsp_refresh_http_timeout", "emqx.listeners", [
{default, "15000ms"},
{datatype, {duration, ms}}
]}.
{mapping, "listener.ssl.$name.enable_crl_cache", "emqx.listeners", [
{default, false},
{datatype, {enum, [true, false]}}
]}.
{mapping, "listener.ssl.$name.crl_cache_http_timeout", "emqx.listeners", [
{default, "15s"},
{datatype, {duration, ms}}
]}.
{mapping, "crl_cache.refresh_interval", "emqx.crl_cache_refresh_interval", [
{default, "15m"},
{datatype, {duration, ms}}
]}.
{mapping, "crl_cache.urls", "emqx.crl_cache_urls", [
{default, ""},
{datatype, string}
]}.
{translation, "emqx.crl_cache_urls", fun(Conf) ->
Val = cuttlefish:conf_get("crl_cache.urls", Conf),
string:tokens(Val, ", ")
end}.
%%--------------------------------------------------------------------
%% MQTT/WebSocket Listeners
@ -2184,6 +2235,10 @@ end}.
{supported_subprotocols, string:tokens(cuttlefish:conf_get(Prefix ++ ".supported_subprotocols", Conf, ""), ", ")},
{peer_cert_as_username, cuttlefish:conf_get(Prefix ++ ".peer_cert_as_username", Conf, undefined)},
{peer_cert_as_clientid, cuttlefish:conf_get(Prefix ++ ".peer_cert_as_clientid", Conf, undefined)},
{ocsp_responder_url, cuttlefish:conf_get(Prefix ++ ".ocsp_responder_url", Conf, undefined)},
{ocsp_issuer_pem, cuttlefish:conf_get(Prefix ++ ".ocsp_issuer_pem", Conf, undefined)},
{ocsp_refresh_interval, cuttlefish:conf_get(Prefix ++ ".ocsp_refresh_interval", Conf, undefined)},
{ocsp_refresh_http_timeout, cuttlefish:conf_get(Prefix ++ ".ocsp_refresh_http_timeout", Conf, undefined)},
{compress, cuttlefish:conf_get(Prefix ++ ".compress", Conf, undefined)},
{idle_timeout, cuttlefish:conf_get(Prefix ++ ".idle_timeout", Conf, undefined)},
{max_frame_size, cuttlefish:conf_get(Prefix ++ ".max_frame_size", Conf, undefined)},
@ -2271,7 +2326,18 @@ end}.
undefined -> undefined;
_ -> {fun emqx_psk:lookup/3, <<>>}
end,
Filter([{versions, Versions},
CRLCheck = case cuttlefish:conf_get(Prefix ++ ".enable_crl_cache", Conf, false) of
true ->
HTTPTimeout = cuttlefish:conf_get(Prefix ++ ".crl_cache_http_timeout", Conf, timer:seconds(15)),
%% {crl_check, true} doesn't work
[ {crl_check, peer}
, {crl_cache, {ssl_crl_cache, {internal, [{http, HTTPTimeout}]}}}
];
false ->
[]
end,
Filter(CRLCheck ++
[{versions, Versions},
{ciphers, Ciphers},
{user_lookup_fun, UserLookupFun},
{handshake_timeout, cuttlefish:conf_get(Prefix ++ ".handshake_timeout", Conf, undefined)},

View File

@ -8,7 +8,7 @@
## i.e. will not work if docker command has to be executed with sudo
## example:
## ./scripts/buildx.sh --profile emqx --pkgtype zip --builder ghcr.io/emqx/emqx-builder/4.4-19:24.1.5-3-debian10 --arch arm64
## ./scripts/buildx.sh --profile emqx --pkgtype zip --builder ghcr.io/emqx/emqx-builder/4.4-20:24.3.4.2-1-debian10 --arch arm64
set -euo pipefail
@ -20,7 +20,7 @@ help() {
echo "--arch amd64|arm64: Target arch to build the EMQ X package for"
echo "--src_dir <SRC_DIR>: EMQ X source ode in this dir, default to PWD"
echo "--builder <BUILDER>: Builder image to pull"
echo " E.g. ghcr.io/emqx/emqx-builder/4.4-19:24.1.5-3-debian11"
echo " E.g. ghcr.io/emqx/emqx-builder/4.4-20:24.3.4.2-1-debian11"
echo "--ssh: Pass ssh agent to the builder."
echo " Also configures git in container to use ssh instead of https to clone deps"
}

View File

@ -9,7 +9,7 @@ set -euo pipefail
## i.e. if we are to release a new enterprise without cutting a new opensource release
## we should tag rel-e4.4.X in the opensource repo, and merge this tag to enterprise
## then cut a release from the enterprise repo.
latest_release="$(git describe --abbrev=0 --tags --match 'rel-e4.4.*' --match '[v|e]4.4*' --exclude '*beta*' --exclude '*alpha*' --exclude '*rc*')"
latest_release="$(git describe --abbrev=0 --tags --match 'rel-e4.4.*' --match '[v|e]4.4*' --exclude '*beta*' --exclude '*alpha*' --exclude '*rc*' --exclude '*gocsp*')"
echo "Compare base: $latest_release"
bad_app_count=0

View File

@ -18,7 +18,7 @@ case "${VSN_MATCH}" in
PKG_VSN='*'
;;
*)
echo "$0 ERROR: second arg must "
echo "$0 ERROR: second arg must be either 'vsn_exact' or 'vsn_matcher'"
exit 1
;;
esac

View File

@ -48,7 +48,7 @@ esac
## e4.3.11
## rel-v4.4.3
## rel-e4.4.3
PREV_TAG="${PREV_TAG:-$(git describe --tag --abbrev=0 --match "[${TAG_PREFIX}|rel-]*" --exclude '*rc*' --exclude '*alpha*' --exclude '*beta*')}"
PREV_TAG="${PREV_TAG:-$(git describe --tag --abbrev=0 --match "[${TAG_PREFIX}|rel-]*" --exclude '*rc*' --exclude '*alpha*' --exclude '*beta*' --exclude '*gocsp*')}"
shift 1
# bash 3.2 treat empty array as unbound, so we can't use 'ESCRIPT_ARGS=()' here,

View File

@ -79,7 +79,10 @@ compile(topic, Topic) ->
end.
pattern(Words) ->
lists:member(<<"%u">>, Words) orelse lists:member(<<"%c">>, Words).
lists:member(<<"%u">>, Words)
orelse lists:member(<<"%c">>, Words)
orelse lists:member(<<"%cida">>, Words)
orelse lists:member(<<"%cna">>, Words).
bin(L) when is_list(L) ->
list_to_binary(L);
@ -165,5 +168,13 @@ feed_var(#{username := undefined}, [<<"%u">>|_Words], _Acc) ->
nomatch;
feed_var(ClientInfo = #{username := Username}, [<<"%u">>|Words], Acc) ->
feed_var(ClientInfo, Words, [Username|Acc]);
feed_var(#{clientid_alias := undefined}, [<<"%cida">>|_Words], _Acc) ->
nomatch;
feed_var(ClientInfo = #{clientid_alias := Alias}, [<<"%cida">>|Words], Acc) ->
feed_var(ClientInfo, Words, [Alias|Acc]);
feed_var(#{common_name_alias := undefined}, [<<"%cna">>|_Words], _Acc) ->
nomatch;
feed_var(ClientInfo = #{common_name_alias := Alias}, [<<"%cna">>|Words], Acc) ->
feed_var(ClientInfo, Words, [Alias|Acc]);
feed_var(ClientInfo, [W|Words], Acc) ->
feed_var(ClientInfo, Words, [W|Acc]).

View File

@ -49,6 +49,8 @@ start(_Type, _Args) ->
ok = emqx_plugins:init(),
_ = emqx_plugins:load(),
_ = start_ce_modules(),
set_clientid_enrichment_module(),
_ = set_special_auth_module(),
register(emqx, self()),
print_vsn(),
{ok, Sup}.
@ -78,6 +80,33 @@ start_ce_modules() ->
ok.
-endif.
set_clientid_enrichment_module() ->
case emqx:get_env(clientid_enrichment_module) of
undefined ->
ok;
Mod ->
case erlang:function_exported(Mod, enrich_clientid_alias, 2) of
true ->
persistent_term:put(clientid_enrichment_module, Mod);
false ->
ok
end
end.
set_special_auth_module() ->
case emqx:get_env(special_auth_module) of
undefined ->
ok;
Mod ->
case erlang:function_exported(Mod, check_authn, 2) of
true ->
persistent_term:put(special_auth_module, Mod),
emqx:hook('client.authenticate', fun Mod:check_authn/2, []);
false ->
ok
end
end.
%%--------------------------------------------------------------------
%% Print Banner
%%--------------------------------------------------------------------

View File

@ -313,6 +313,7 @@ handle_in(?CONNECT_PACKET(ConnPkt) = Packet, Channel) ->
fun set_log_meta/2,
fun check_banned/2,
fun count_flapping_event/2,
fun enrich_clientid_alias/2,
fun auth_connect/2
], ConnPkt, Channel#channel{conn_state = connecting}) of
{ok, NConnPkt, NChannel = #channel{clientinfo = ClientInfo}} ->
@ -1353,6 +1354,17 @@ check_banned(_ConnPkt, #channel{clientinfo = ClientInfo = #{zone := Zone}}) ->
false -> ok
end.
%%--------------------------------------------------------------------
%% Enrich ClientID Alias
enrich_clientid_alias(Packet, Channel) ->
case persistent_term:get(clientid_enrichment_module, undefined) of
undefined ->
{ok, Channel};
Mod ->
Mod:enrich_clientid_alias(Packet, Channel)
end.
%%--------------------------------------------------------------------
%% Auth Connect

165
src/emqx_crl_cache.erl Normal file
View File

@ -0,0 +1,165 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% @doc EMQX CRL cache.
%%--------------------------------------------------------------------
-module(emqx_crl_cache).
%% API
-export([ start_link/0
, start_link/1
, refresh/1
, evict/1
]).
%% gen_server callbacks
-export([ init/1
, handle_call/3
, handle_cast/2
, handle_info/2
]).
%% internal exports
-export([http_get/2]).
-behaviour(gen_server).
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
-define(LOG(Level, Format, Args),
logger:log(Level, "[~p] " ++ Format, [?MODULE | Args])).
-define(HTTP_TIMEOUT, timer:seconds(10)).
-define(RETRY_TIMEOUT, 5_000).
-record(state,
{ refresh_timers = #{} :: #{binary() => timer:tref()}
, refresh_interval = timer:minutes(15) :: timer:time()
}).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
start_link() ->
URLs = emqx:get_env(crl_cache_urls, []),
RefreshIntervalMS = emqx:get_env(crl_cache_refresh_interval,
timer:minutes(15)),
start_link(#{urls => URLs, refresh_interval => RefreshIntervalMS}).
start_link(Opts = #{urls := _, refresh_interval := _}) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, Opts, []).
refresh(URL) ->
gen_server:call(?MODULE, {refresh, URL}, ?HTTP_TIMEOUT + 2_000).
evict(URL) ->
gen_server:cast(?MODULE, {evict, URL}).
%%--------------------------------------------------------------------
%% gen_server behaviour
%%--------------------------------------------------------------------
init(#{urls := URLs, refresh_interval := RefreshIntervalMS}) ->
State = lists:foldl(fun(URL, Acc) -> ensure_timer(URL, Acc, 0) end,
#state{refresh_interval = RefreshIntervalMS},
URLs),
{ok, State}.
handle_call({refresh, URL}, _From, State0) ->
case do_http_fetch_and_cache(URL) of
{error, Error} ->
?LOG(error, "failed to fetch crl response for ~p; error: ~p",
[URL, Error]),
{reply, error, ensure_timer(URL, State0, ?RETRY_TIMEOUT)};
{ok, CRLs} ->
?LOG(debug, "fetched crl response for ~p", [URL]),
{reply, {ok, CRLs}, ensure_timer(URL, State0)}
end;
handle_call(Call, _From, State) ->
{reply, {error, {bad_call, Call}}, State}.
handle_cast({evict, URL}, State0 = #state{refresh_timers = RefreshTimers0}) ->
ssl_crl_cache:delete(URL),
MTimer = maps:get(URL, RefreshTimers0, undefined),
emqx_misc:cancel_timer(MTimer),
RefreshTimers = maps:without([URL], RefreshTimers0),
State = State0#state{refresh_timers = RefreshTimers},
?tp(crl_cache_evict,
#{ url => URL
}),
{noreply, State};
handle_cast(_Cast, State) ->
{noreply, State}.
handle_info({timeout, TRef, {refresh, URL}},
State = #state{refresh_timers = RefreshTimers}) ->
case maps:get(URL, RefreshTimers, undefined) of
TRef ->
?tp(crl_refresh_timer, #{url => URL}),
?LOG(debug, "refreshing crl response for ~p", [URL]),
case do_http_fetch_and_cache(URL) of
{error, Error} ->
?LOG(error, "failed to fetch crl response for ~p; error: ~p",
[URL, Error]),
{noreply, ensure_timer(URL, State, ?RETRY_TIMEOUT)};
{ok, _CRLs} ->
?LOG(debug, "fetched crl response for ~p", [URL]),
{noreply, ensure_timer(URL, State)}
end;
_ ->
{noreply, State}
end;
handle_info(_Info, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% internal functions
%%--------------------------------------------------------------------
http_get(URL, HTTPTimeout) ->
httpc:request(
get,
{URL,
[{"connection", "close"}]},
[{timeout, HTTPTimeout}],
[{body_format, binary}]
).
do_http_fetch_and_cache(URL) ->
%% FIXME
Resp = ?MODULE:http_get(URL, ?HTTP_TIMEOUT),
case Resp of
{ok, {{_, 200, _}, _, Body}} ->
case parse_crls(Body) of
error ->
{error, invalid_crl};
CRLs ->
ssl_crl_cache:insert(URL, {der, CRLs}),
?tp(crl_cache_insert, #{url => URL}),
{ok, CRLs}
end;
{ok, {{_, Code, _}, _, Body}} ->
{error, {bad_response, #{code => Code, body => Body}}};
{error, Error} ->
{error, {http_error, Error}}
end.
parse_crls(Bin) ->
try
[CRL || {'CertificateList', CRL, not_encrypted} <- public_key:pem_decode(Bin)]
catch
_:_ ->
error
end.
ensure_timer(URL, State = #state{refresh_interval = Timeout}) ->
ensure_timer(URL, State, Timeout).
ensure_timer(URL, State = #state{refresh_timers = RefreshTimers0}, Timeout) ->
MTimer = maps:get(URL, RefreshTimers0, undefined),
emqx_misc:cancel_timer(MTimer),
RefreshTimers = RefreshTimers0#{URL => emqx_misc:start_timer(
Timeout,
{refresh, URL})},
State#state{refresh_timers = RefreshTimers}.

View File

@ -33,7 +33,9 @@ init([]) ->
child_spec(emqx_stats, worker),
child_spec(emqx_metrics, worker),
child_spec(emqx_ctl, worker),
child_spec(emqx_zone, worker)]}}.
child_spec(emqx_zone, worker),
child_spec(emqx_ocsp_cache, worker),
child_spec(emqx_crl_cache, worker)]}}.
child_spec(M, Type) ->
child_spec(M, Type, []).

View File

@ -45,6 +45,7 @@
, proto := esockd:proto()
, listen_on := esockd:listen_on()
, opts := [esockd:option()]
, any() => term()
}).
%% @doc Find listener identifier by listen-on.
@ -104,8 +105,9 @@ ensure_all_started([L | Rest], Results) ->
format_listen_on(ListenOn) -> format(ListenOn).
-spec(start_listener(listener()) -> ok).
start_listener(#{proto := Proto, name := Name, listen_on := ListenOn, opts := Options}) ->
start_listener(Listener = #{proto := Proto, name := Name, listen_on := ListenOn}) ->
ID = identifier(Proto, Name),
Options = emqx_ocsp_cache:inject_sni_fun(Listener),
case start_listener(Proto, ListenOn, Options) of
{ok, _} ->
console_print("Start ~s listener on ~s successfully.~n", [ID, format(ListenOn)]);

313
src/emqx_ocsp_cache.erl Normal file
View File

@ -0,0 +1,313 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% @doc EMQX OCSP cache.
%%--------------------------------------------------------------------
-module(emqx_ocsp_cache).
-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/1
, inject_sni_fun/1
]).
%% 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(LOG(Level, Format, Args),
logger:log(Level, "[~p] " ++ Format, [?MODULE | Args])).
-define(CACHE_TAB, ?MODULE).
-define(CALL_TIMEOUT, 20_000).
-define(RETRY_TIMEOUT, 5_000).
-define(REFRESH_TIMER(LID), {refresh_timer, LID}).
%%--------------------------------------------------------------------
%% 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 ets:lookup(?CACHE_TAB, cache_key(ListenerID)) of
[{_, DERResponse}] ->
?tp(ocsp_cache_hit, #{listener_id => ListenerID}),
?LOG(debug, "using cached ocsp response for ~p", [ListenerID]),
{ok, DERResponse};
[] ->
?tp(ocsp_cache_miss, #{listener_id => ListenerID}),
?LOG(debug, "fetching new ocsp response for ~p", [ListenerID]),
http_fetch(ListenerID)
end.
register_listener(ListenerID) ->
gen_server:call(?MODULE, {register_listener, ListenerID}, ?CALL_TIMEOUT).
-spec inject_sni_fun(emqx_listeners:listener()) -> [esockd:option()].
inject_sni_fun(Listener = #{proto := Proto, name := Name, opts := Options0}) ->
%% We need to patch `sni_fun' here and not in `emqx.schema'
%% because otherwise an anonymous function will end up in
%% `app.*.config'...
ListenerID = emqx_listeners:identifier(Listener),
case proplists:get_value(ocsp_responder_url, Options0, undefined) of
undefined ->
Options0;
_URL ->
SSLOpts0 = proplists:get_value(ssl_options, Options0, []),
SNIFun = fun(SN) -> emqx_ocsp_cache:sni_fun(SN, ListenerID) end,
Options1 = proplists:delete(ssl_options, Options0),
Options = [{ssl_options, [{sni_fun, SNIFun} | SSLOpts0]} | Options1],
%% save to env
{[ThisListener0], Listeners} =
lists:partition(
fun(#{name := N, proto := P}) ->
N =:= Name andalso P =:= Proto
end,
emqx:get_env(listeners)),
ThisListener = ThisListener0#{opts => Options},
application:set_env(emqx, listeners, [ThisListener | Listeners]),
ok = emqx_ocsp_cache:register_listener(ListenerID),
Options
end.
%%--------------------------------------------------------------------
%% 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) ->
%% Respond immediately if a concurrent call already fetched it.
case ets:lookup(?CACHE_TAB, cache_key(ListenerID)) of
[{_, Response}] ->
{reply, {ok, Response}, State};
[] ->
case do_http_fetch_and_cache(ListenerID) of
error -> {reply, error, ensure_timer(ListenerID, State, ?RETRY_TIMEOUT)};
{ok, Response} -> {reply, {ok, Response}, ensure_timer(ListenerID, State)}
end
end;
handle_call({register_listener, ListenerID}, _From, State0) ->
?LOG(debug, "registering ocsp cache for ~p", [ListenerID]),
#{opts := Opts} = emqx_listeners:find_by_id(ListenerID),
RefreshInterval = proplists:get_value(ocsp_refresh_interval, Opts),
State = State0#{{refresh_interval, ListenerID} => RefreshInterval},
{reply, ok, ensure_timer(ListenerID, 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}),
?LOG(debug, "refreshing ocsp response for ~p", [ListenerID]),
case do_http_fetch_and_cache(ListenerID) of
error ->
?LOG(debug, "failed to fetch ocsp response for ~p", [ListenerID]),
{noreply, ensure_timer(ListenerID, State0, ?RETRY_TIMEOUT)};
{ok, _Response} ->
?LOG(debug, "fetched ocsp response for ~p", [ListenerID]),
{noreply, ensure_timer(ListenerID, State0)}
end;
_ ->
{noreply, State0}
end;
handle_info(_Info, State) ->
{noreply, State}.
code_change(_Vsn, State, _Extra) ->
%% we need to re-create the `sni_fun' lambda that the SSL
%% listeners are holding onto to avoid them becoming `badfun''s.
ListenersToPatch =
lists:filter(
fun(#{opts := Opts}) ->
undefined =/= proplists:get_value(ocsp_responder_url, Opts)
end,
emqx:get_env(listeners, [])),
PatchedListeners = [L#{opts => ?MODULE:inject_sni_fun(L)} || L <- ListenersToPatch],
lists:foreach(
fun(L) ->
emqx_listeners:update_listeners_env(update, L)
end,
PatchedListeners),
{ok, State}.
%%--------------------------------------------------------------------
%% internal functions
%%--------------------------------------------------------------------
http_fetch(ListenerID) ->
%% TODO: configurable call timeout?
gen_server:call(?MODULE, {http_fetch, ListenerID}, ?CALL_TIMEOUT).
cache_key(ListenerID) ->
#{opts := Options} = emqx_listeners:find_by_id(ListenerID),
SSLOpts = proplists:get_value(ssl_options, Options, undefined),
ServerCertPemPath = proplists:get_value(certfile, SSLOpts, undefined),
#'Certificate'{
tbsCertificate =
#'TBSCertificate'{
signature = Signature
}} = read_server_cert(ServerCertPemPath),
{ocsp_response, Signature}.
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.
do_http_fetch_and_cache(ListenerID) ->
#{opts := Options} = emqx_listeners:find_by_id(ListenerID),
ResponderURL0 = proplists:get_value(ocsp_responder_url, Options, undefined),
ResponderURL = uri_string:normalize(ResponderURL0),
IssuerPemPath = proplists:get_value(ocsp_issuer_pem, Options, undefined),
SSLOpts = proplists:get_value(ssl_options, Options, undefined),
ServerCertPemPath = proplists:get_value(certfile, SSLOpts, undefined),
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),
HTTPTimeout = proplists:get_value(ocsp_refresh_http_timeout, Options),
?tp(ocsp_http_fetch, #{ listener_id => ListenerID
, responder_url => ResponderURL
, timeout => HTTPTimeout
}),
Resp = ?MODULE:http_get(ResponderURL ++ Request, HTTPTimeout),
case Resp of
{ok, {{_, 200, _}, _, Body}} ->
?LOG(debug, "caching ocsp response for ~p", [ListenerID]),
true = ets:insert(?CACHE_TAB, {cache_key(ListenerID), Body}),
?tp(ocsp_http_fetch_and_cache, #{listener_id => ListenerID}),
{ok, Body};
{ok, {{_, Code, _}, _, Body}} ->
?tp(error, ocsp_http_fetch_bad_code,
#{ listener_id => ListenerID
, body => Body
, code => Code
}),
?LOG(error, "error fetching ocsp response for ~p: code ~b, body: ~p", [ListenerID, Code, Body]),
error;
{error, Error} ->
?tp(error, ocsp_http_fetch_error,
#{ listener_id => ListenerID
, error => Error
}),
?LOG(error, "error fetching ocsp response for ~p: ~p", [ListenerID, 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) ->
emqx_misc:cancel_timer(maps:get(?REFRESH_TIMER(ListenerID), State, undefined)),
State#{?REFRESH_TIMER(ListenerID) => emqx_misc:start_timer(
Timeout,
{refresh, ListenerID})}.
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

@ -31,6 +31,84 @@ init_per_suite(Config) ->
end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([]).
t_compile_clientid_common_name_alias_placeholders(_Config) ->
Rule1 = {allow, all, pubsub, <<"%cida">>},
?assertEqual(
{allow, all, pubsub, [{pattern,[<<"%cida">>]}]},
emqx_access_rule:compile(Rule1)),
Rule2 = {allow, all, pubsub, <<"%cna">>},
?assertEqual(
{allow, all, pubsub, [{pattern,[<<"%cna">>]}]},
emqx_access_rule:compile(Rule2)),
ok.
t_match_clientid_common_name_alias_placeholders(_Config) ->
ClientInfo1 = #{clientid => <<"something-123456789">>,
cn => <<"another-987654321">>,
clientid_alias => <<"123456789">>,
common_name_alias => <<"987654321">>
},
Rule1 = {allow, all, pubsub, <<"t/%cida">>},
Compiled1 = emqx_access_rule:compile(Rule1),
?assertEqual({matched, allow},
emqx_access_rule:match(
ClientInfo1,
<<"t/123456789">>,
Compiled1)),
?assertEqual(nomatch,
emqx_access_rule:match(
ClientInfo1,
<<"t/987654321">>,
Compiled1)),
?assertEqual(nomatch,
emqx_access_rule:match(
ClientInfo1,
<<"123456789">>,
Compiled1)),
?assertEqual(nomatch,
emqx_access_rule:match(
ClientInfo1,
<<"t/something-123456789">>,
Compiled1)),
?assertEqual(nomatch,
emqx_access_rule:match(
ClientInfo1,
<<"t/%cida">>,
Compiled1)),
Rule2 = {allow, all, pubsub, <<"t/%cna">>},
Compiled2 = emqx_access_rule:compile(Rule2),
?assertEqual(nomatch,
emqx_access_rule:match(
ClientInfo1,
<<"t/123456789">>,
Compiled2)),
?assertEqual({matched, allow},
emqx_access_rule:match(
ClientInfo1,
<<"t/987654321">>,
Compiled2)),
?assertEqual(nomatch,
emqx_access_rule:match(
ClientInfo1,
<<"987654321">>,
Compiled2)),
?assertEqual(nomatch,
emqx_access_rule:match(
ClientInfo1,
<<"t/another-987654321">>,
Compiled2)),
?assertEqual(nomatch,
emqx_access_rule:match(
ClientInfo1,
<<"t/%cida">>,
Compiled2)),
ok.
t_compile(_) ->
Rule1 = {allow, all, pubsub, <<"%u">>},
Compile1 = {allow, all, pubsub, [{pattern,[<<"%u">>]}]},

View File

@ -0,0 +1,400 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_crl_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").
%% from ssl_manager.erl
-record(state, {
session_cache_client,
session_cache_client_cb,
session_lifetime,
certificate_db,
session_validation_timer,
session_cache_client_max,
session_client_invalidator,
options,
client_session_order
}).
%%--------------------------------------------------------------------
%% CT boilerplate
%%--------------------------------------------------------------------
all() ->
emqx_ct:all(?MODULE).
init_per_testcase(TestCase, Config)
when TestCase =:= t_empty_cache;
TestCase =:= t_filled_cache;
TestCase =:= t_revoked ->
DataDir = ?config(data_dir, Config),
CRLFile = filename:join([DataDir, "crl.pem"]),
{ok, CRLPem} = file:read_file(CRLFile),
[{'CertificateList', CRLDer, not_encrypted}] = public_key:pem_decode(CRLPem),
ServerPid = start_crl_server(CRLPem),
IsCached = lists:member(TestCase, [t_filled_cache, t_revoked]),
ok = setup_crl_options(Config, #{is_cached => IsCached}),
[ {crl_pem, CRLPem}
, {crl_der, CRLDer}
, {http_server, ServerPid}
| Config];
init_per_testcase(t_not_cached_and_unreachable, Config) ->
DataDir = ?config(data_dir, Config),
CRLFile = filename:join([DataDir, "crl.pem"]),
{ok, CRLPem} = file:read_file(CRLFile),
[{'CertificateList', CRLDer, not_encrypted}] = public_key:pem_decode(CRLPem),
application:stop(cowboy),
ok = setup_crl_options(Config, #{is_cached => false}),
[ {crl_pem, CRLPem}
, {crl_der, CRLDer}
| Config];
init_per_testcase(_TestCase, Config) ->
DataDir = ?config(data_dir, Config),
CRLFile = filename:join([DataDir, "crl.pem"]),
{ok, CRLPem} = file:read_file(CRLFile),
[{'CertificateList', CRLDer, not_encrypted}] = public_key:pem_decode(CRLPem),
TestPid = self(),
ok = meck:new(emqx_crl_cache, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx_crl_cache, http_get,
fun(URL, _HTTPTimeout) ->
TestPid ! {http_get, URL},
{ok, {{"HTTP/1.0", 200, 'OK'}, [], CRLPem}}
end),
[ {crl_pem, CRLPem}
, {crl_der, CRLDer}
| Config].
end_per_testcase(TestCase, Config)
when TestCase =:= t_empty_cache;
TestCase =:= t_filled_cache;
TestCase =:= t_revoked ->
ServerPid = ?config(http_server, Config),
emqx_crl_cache_http_server:stop(ServerPid),
emqx_ct_helpers:stop_apps([]),
application:set_env(emqx, crl_cache_urls, []),
application:stop(cowboy),
clear_crl_cache(),
ok;
end_per_testcase(t_not_cached_and_unreachable, _Config) ->
emqx_ct_helpers:stop_apps([]),
application:set_env(emqx, crl_cache_urls, []),
clear_crl_cache(),
ok;
end_per_testcase(_TestCase, _Config) ->
meck:unload([emqx_crl_cache]),
clear_crl_cache(),
ok.
%%--------------------------------------------------------------------
%% Helper functions
%%--------------------------------------------------------------------
assert_http_get(URL) ->
receive
{http_get, URL} ->
ok
after
1000 ->
error({should_have_requested, URL})
end.
get_crl_cache_table() ->
#state{certificate_db = [_, _, _, {Ref, _}]} = sys:get_state(ssl_manager),
Ref.
start_crl_server(Port, CRLPem) ->
{ok, LSock} = gen_tcp:listen(Port, [binary, {active, true}, reusedaddr]),
spawn_link(fun() -> accept_loop(LSock, CRLPem) end),
ok.
accept_loop(LSock, CRLPem) ->
case gen_tcp:accept(LSock) of
{ok, Sock} ->
Worker = spawn_link(fun() -> crl_loop(Sock, CRLPem) end),
gen_tcp:controlling_process(Sock, Worker),
accept_loop(LSock, CRLPem);
{error, Reason} ->
error({accept_error, Reason})
end.
crl_loop(Sock, CRLPem) ->
receive
{tcp, Sock, _Data} ->
gen_tcp:send(Sock, CRLPem),
crl_loop(Sock, CRLPem);
_Msg ->
ok
end.
drain_msgs() ->
receive
_Msg ->
drain_msgs()
after
0 ->
ok
end.
clear_crl_cache() ->
%% reset the CRL cache
exit(whereis(ssl_manager), kill),
ok.
force_cacertfile(Cacertfile) ->
{SSLListeners0, OtherListeners} = lists:partition(
fun(#{proto := Proto}) -> Proto =:= ssl end,
emqx:get_env(listeners)),
SSLListeners =
lists:map(
fun(Listener = #{opts := Opts0}) ->
SSLOpts0 = proplists:get_value(ssl_options, Opts0),
%% it injects some garbage...
SSLOpts1 = lists:keydelete(cacertfile, 1, lists:keydelete(cacertfile, 1, SSLOpts0)),
SSLOpts2 = [{cacertfile, Cacertfile} | SSLOpts1],
Opts1 = lists:keyreplace(ssl_options, 1, Opts0, {ssl_options, SSLOpts2}),
Listener#{opts => Opts1}
end,
SSLListeners0),
application:set_env(emqx, listeners, SSLListeners ++ OtherListeners),
ok.
setup_crl_options(Config, #{is_cached := IsCached}) ->
DataDir = ?config(data_dir, Config),
Cacertfile = filename:join(DataDir, "ca-chain.cert.pem"),
Certfile = filename:join(DataDir, "server.cert.pem"),
Keyfile = filename:join(DataDir, "server.key.pem"),
URLs = case IsCached of
false -> [];
true -> ["http://localhost:9878/intermediate.crl.pem"]
end,
Handler =
fun(emqx) ->
application:set_env(emqx, crl_cache_urls, URLs),
emqx_ct_helpers:change_emqx_opts(
ssl_twoway, [{ssl_options, [ {certfile, Certfile}
, {keyfile, Keyfile}
, {verify, verify_peer}
%% {crl_check, true} does not work; probably bug in OTP
, {crl_check, peer}
, {crl_cache,
{ssl_crl_cache, {internal, [{http, timer:seconds(15)}]}}}
]}]),
%% emqx_ct_helpers:change_emqx_opts has cacertfile hardcoded....
ok = force_cacertfile(Cacertfile),
ok;
(_) ->
ok
end,
emqx_ct_helpers:start_apps([], Handler),
case IsCached of
true ->
%% wait the cache to be filled
receive
http_get -> ok
after
1_000 -> error(crl_cache_not_filled)
end;
false ->
%% ensure cache is empty
clear_crl_cache(),
ct:sleep(200),
ok
end,
drain_msgs(),
ok.
start_crl_server(CRLPem) ->
application:ensure_all_started(cowboy),
{ok, ServerPid} = emqx_crl_cache_http_server:start_link(self(), 9878, CRLPem, []),
receive
{ServerPid, ready} -> ok
after
1000 -> error(timeout_starting_http_server)
end,
ServerPid.
%%--------------------------------------------------------------------
%% Test cases
%%--------------------------------------------------------------------
t_init_empty_urls(_Config) ->
Ref = get_crl_cache_table(),
?assertEqual([], ets:tab2list(Ref)),
?assertMatch({ok, _}, emqx_crl_cache:start_link()),
receive
{http_get, _} ->
error(should_not_make_http_request)
after
1000 -> ok
end,
?assertEqual([], ets:tab2list(Ref)),
ok.
t_init_refresh(Config) ->
CRLDer = ?config(crl_der, Config),
Ref = get_crl_cache_table(),
?assertEqual([], ets:tab2list(Ref)),
URL1 = "http://localhost/crl1.pem",
URL2 = "http://localhost/crl2.pem",
Opts = #{ urls => [URL1, URL2]
, refresh_interval => timer:minutes(15)
},
ok = snabbkaffe:start_trace(),
{ok, SubRef} = snabbkaffe:subscribe(
fun(#{?snk_kind := Kind}) ->
Kind =:= crl_cache_insert
end,
_NEvents = 2,
_Timeout = 2_000),
?assertMatch({ok, _}, emqx_crl_cache:start_link(Opts)),
lists:foreach(fun assert_http_get/1, [URL1, URL2]),
{ok, _} = snabbkaffe:receive_events(SubRef),
snabbkaffe:stop(),
?assertEqual(
[{"crl1.pem", [CRLDer]}, {"crl2.pem", [CRLDer]}],
lists:sort(ets:tab2list(Ref))),
ok.
t_manual_refresh(Config) ->
CRLDer = ?config(crl_der, Config),
Ref = get_crl_cache_table(),
?assertEqual([], ets:tab2list(Ref)),
{ok, _} = emqx_crl_cache:start_link(),
URL = "http://localhost/crl.pem",
?assertEqual({ok, [CRLDer]}, emqx_crl_cache:refresh(URL)),
?assertEqual(
[{"crl.pem", [CRLDer]}],
ets:tab2list(Ref)),
ok.
t_refresh_request_error(_Config) ->
meck:expect(emqx_crl_cache, http_get,
fun(_URL, _HTTPTimeout) ->
{ok, {{"HTTP/1.0", 404, 'Not Found'}, [], <<"not found">>}}
end),
{ok, _} = emqx_crl_cache:start_link(),
URL = "http://localhost/crl.pem",
?assertEqual(error, emqx_crl_cache:refresh(URL)),
ok.
t_refresh_invalid_response(_Config) ->
meck:expect(emqx_crl_cache, http_get,
fun(_URL, _HTTPTimeout) ->
{ok, {{"HTTP/1.0", 200, 'OK'}, [], <<"not a crl">>}}
end),
{ok, _} = emqx_crl_cache:start_link(),
URL = "http://localhost/crl.pem",
?assertEqual({ok, []}, emqx_crl_cache:refresh(URL)),
ok.
t_refresh_http_error(_Config) ->
meck:expect(emqx_crl_cache, http_get,
fun(_URL, _HTTPTimeout) ->
{error, timeout}
end),
{ok, _} = emqx_crl_cache:start_link(),
URL = "http://localhost/crl.pem",
?assertEqual(error, emqx_crl_cache:refresh(URL)),
ok.
t_unknown_messages(_Config) ->
{ok, Server} = emqx_crl_cache:start_link(),
gen_server:call(Server, foo),
gen_server:cast(Server, foo),
Server ! foo,
ok.
t_evict(_Config) ->
{ok, _} = emqx_crl_cache:start_link(),
URL = "http://localhost/crl.pem",
{ok, [_]} = emqx_crl_cache:refresh(URL),
Ref = get_crl_cache_table(),
?assertMatch([{"crl.pem", _}], ets:tab2list(Ref)),
snabbkaffe:start_trace(),
{ok, {ok, _}} = ?wait_async_action(emqx_crl_cache:evict(URL),
#{?snk_kind := crl_cache_evict}),
snabbkaffe:stop(),
?assertEqual([], ets:tab2list(Ref)),
ok.
%% check that the URL in the certificate is checked on the fly if the
%% cache is empty.
t_empty_cache(Config) ->
DataDir = ?config(data_dir, Config),
ClientCert = filename:join(DataDir, "client.cert.pem"),
ClientKey = filename:join(DataDir, "client.key.pem"),
{ok, C} = emqtt:start_link([ {ssl, true}
, {ssl_opts, [ {certfile, ClientCert}
, {keyfile, ClientKey}
]}
, {port, 8883}
]),
{ok, _} = emqtt:connect(C),
receive
http_get -> ok
after
2_000 -> error(should_have_checked_server)
end,
emqtt:disconnect(C),
ok.
%% check that the URL in the certificate is *not* checked if the cache
%% contains that URL.
t_filled_cache(Config) ->
DataDir = ?config(data_dir, Config),
ClientCert = filename:join(DataDir, "client.cert.pem"),
ClientKey = filename:join(DataDir, "client.key.pem"),
{ok, C} = emqtt:start_link([ {ssl, true}
, {ssl_opts, [ {certfile, ClientCert}
, {keyfile, ClientKey}
]}
, {port, 8883}
]),
{ok, _} = emqtt:connect(C),
receive
http_get -> error(should_have_used_cache)
after
2_000 -> ok
end,
emqtt:disconnect(C),
ok.
%% If the CRL is not cached when the client tries to connect and the
%% CRL server is unreachable, the client will be denied connection.
t_not_cached_and_unreachable(Config) ->
DataDir = ?config(data_dir, Config),
ClientCert = filename:join(DataDir, "client.cert.pem"),
ClientKey = filename:join(DataDir, "client.key.pem"),
{ok, C} = emqtt:start_link([ {ssl, true}
, {ssl_opts, [ {certfile, ClientCert}
, {keyfile, ClientKey}
]}
, {port, 8883}
]),
Ref = get_crl_cache_table(),
?assertEqual([], ets:tab2list(Ref)),
process_flag(trap_exit, true),
?assertMatch({error, {{shutdown, {tls_alert, {bad_certificate, _}}}, _}}, emqtt:connect(C)),
ok.
t_revoked(Config) ->
DataDir = ?config(data_dir, Config),
ClientCert = filename:join(DataDir, "client-revoked.cert.pem"),
ClientKey = filename:join(DataDir, "client-revoked.key.pem"),
{ok, C} = emqtt:start_link([ {ssl, true}
, {ssl_opts, [ {certfile, ClientCert}
, {keyfile, ClientKey}
]}
, {port, 8883}
]),
process_flag(trap_exit, true),
?assertMatch({error, {{shutdown, {tls_alert, {certificate_revoked, _}}}, _}}, emqtt:connect(C)),
ok.

View File

@ -0,0 +1,68 @@
-----BEGIN CERTIFICATE-----
MIIF+zCCA+OgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwbzELMAkGA1UEBhMCU0Ux
EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQK
DAlNeU9yZ05hbWUxETAPBgNVBAsMCE15Um9vdENBMREwDwYDVQQDDAhNeVJvb3RD
QTAeFw0yMjA2MTMxMjQyMDVaFw0zMjA2MTAxMjQyMDVaMGsxCzAJBgNVBAYTAlNF
MRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAoMCU15T3JnTmFtZTEZMBcGA1UE
CwwQTXlJbnRlcm1lZGlhdGVDQTEZMBcGA1UEAwwQTXlJbnRlcm1lZGlhdGVDQTCC
AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAN32wUPOGrLjVHY37ICI4sWH
2GBEYgGtoKyZTfLKqK8W965uxHxMKKeKC7Ro93ScT4giR2GdsCvMyLP4Dlv7FxL7
rWEHc7vbP22uT4NQQ0hgfl4ch8rTiSl/5FfynOuvMHnKh58z+lGyQ/uqKqAzc6OD
6FypzSQO2R6JpA+kxKlbTYmOcsLiLFKjCjxyA1ibeozUaRHjPRM7VLHVxYfmpGm/
86NJxtHrw4hwoKIM9bfURboxfn9R1YM14mZYA6Uw2pScS1+j79tNy74NQNYu1t9E
cyPQk+AJdp2BsbR5KPYr1SMVLHlTwnzjk1IVW4wUUdX2h56ygmRNo9ui74ODfyud
4cclg45SeRdOT0w5e3g20ZvfLZpIkhXk19EIIU/YbG6GpL8gLvBHkz6vvidE7L/2
h2//alJBeWCvyOloIWYNYnwnHGeXR2c5pzNxHipkBSuMeBaLJOO7X9oqKVanu+xq
nVagFhEYnd+T0PsPa5IVA73KiWMWWeFgJI0pRUydyp0/FhXEhkMWZNJHiscbxcdn
hTdNCAbMfV/4fMar+d/QKY/GMWUVQ4OlXUoo3WjjRi4T8NJEjZGfLdKw5x81WM4A
yqDV3OVVCBf1XrHH4IbvbUDgeG1OEGSV9pdvKX4Sm7226vdOc3HPfRnVyf/N1Pv7
lzPbUlCheKbTW6Oeq97VAgMBAAGjgaQwgaEwHQYDVR0OBBYEFCuv1TkzC1fSgTfz
E1m1u5pRCJsVMB8GA1UdIwQYMBaAFErA8sZMX6obhoo3XvUpaTy6Z4uhMBIGA1Ud
EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMDsGA1UdHwQ0MDIwMKAuoCyG
Kmh0dHA6Ly9sb2NhbGhvc3Q6OTg3OC9pbnRlcm1lZGlhdGUuY3JsLnBlbTANBgkq
hkiG9w0BAQsFAAOCAgEAtn/u/ZfmioZyh5DNETRNAftXWvxOyi2MK8soNEsaSbmq
2ajkQwJ1MZ0+C5HuzsoEEoqStYtD3JG34ydPzQbMPwkTDg48+guu8ji30jYXGIfI
RQQfseEj1hN8wTLEDAJGl17kJA+js6dcHkJp93qocOCoOwa5MAYB8ZZq/uRJlzVt
ol6dvhBvhoxkKvJfrhg5dNISVBgIXrs5YOMyXqh6W3YMmepNjs05e5bcLYADyHCd
f4TK9pgyys4OIVHiRZo0+hlaChKo4vDK7acgZOds7qxS/sxrwKe49FTIrAWWP6fG
Ij2RHF91fLhi+10oVVSWtCyWRJOaSM4cenbLN36OUg1JswacsqojTCUylMAa8sAB
RggZ+tt8LlARj3/pdz6IWrccabC+AGZQa1kOKl97hjsE0qy9V5WOiueJ+78u+BY5
NXIoIyuPG0WCItb76jyn7UDjiCsJt7rfJ5t5rRVLpm8YRG43KuMWjXih/bT07YdE
tA3X5Bk/XLQBqQbRKpR5+CKFhvXbNmKnuAdFbiG8UTFQdo/HGyBR7zzkF4vTqu8s
2pMl4xFAMNnGsWQYce6YioQfYL3apgx9PIqgHr+IzXae6/NIpoIs9ShymEZP1jT5
9DqKzmUQz3czc7RNXLUZbWxoWRtivrDJGPgbSHyMqVu4h2yLINftHSGegC+ZEgc=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFzzCCA7egAwIBAgIUVYtdlrTMH1BoMy9JpiiL/FXv5NMwDQYJKoZIhvcNAQEL
BQAwbzELMAkGA1UEBhMCU0UxEjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJ
U3RvY2tob2xtMRIwEAYDVQQKDAlNeU9yZ05hbWUxETAPBgNVBAsMCE15Um9vdENB
MREwDwYDVQQDDAhNeVJvb3RDQTAeFw0yMjA2MTMxMjQyMDVaFw00MjA2MDgxMjQy
MDVaMG8xCzAJBgNVBAYTAlNFMRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAcM
CVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMREwDwYDVQQLDAhNeVJvb3RD
QTERMA8GA1UEAwwITXlSb290Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
AoICAQDRKeWdrXluKLuVxTXxpWbCzBm/6x+kg8EjhIKUm9Mq0t+cOLC1sn99s4E0
/NBA7jc3O+hQ9S2fZuMp9OUXuf8HjjtlKQY2M9T9kCE9Yxc+ygpcPEvDYl0ke/1r
m6eUOGS2LkIDHebuYbY+KGXjpaF15w49q199wQAk0sbdcCiv+OymBUxi3lrHepo3
EBIjHtTwNtehgeiJfxCe8TLlLpbPdCsMNwlPvNa0E+1Ol1P9DfHPfK1Dt0ua8X5q
ZBhnW1cawrwzrnth2ZtrSw7RaWhtNWFt/SdedGPgVUMIgFY2I7f2wT2hoe8R/D+e
XLDzVMAW+yrlu/LlBaHE2Ffkk2La8mSHwJOvCZ/Q0V1mJnj6F4pfNemY4MnFX6Jm
7u2TSWooowMppdNPCCRlGk+XS8lfmb9N/0RsOMSz5+1u8wOfFI6Z1GaH6K2Ne2xf
VIBjhGRAiTCHDIFNYBCeAxR64zYeGFKv8L19oV6cbA6St7GMikfk5U/fyG9SUK88
Fvjm+MrNDu/EEyjkKhK6I9ao3Pb4LcCofTcu44RBYT0HhkjqGWQgapm0i80NcRlL
yK7wm5C8Tntmbehg1KE8iqSzAcBZu6L+j4qQ90T+AIHNTmRUa9dH2uhEej4hIhjd
yJSJcDm4/CYEuK6WgZ6EuJadRy4QgPiH7zWCFXp/OGLvFmUEWQIDAQABo2MwYTAd
BgNVHQ4EFgQUSsDyxkxfqhuGijde9SlpPLpni6EwHwYDVR0jBBgwFoAUSsDyxkxf
qhuGijde9SlpPLpni6EwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYw
DQYJKoZIhvcNAQELBQADggIBAJQSy3WpS/j8emep2gJXB7jYxolhhN78DiJaqUZh
0YXa19YaghAsk5YVqbe2XLAGyFqAmmGQ6ymCbULqkdSNUTLTvwHYGFYH7NizUaCS
r9XaB9lizr7dwpd/8HwpqMq7rxNXLMNfl3iVywiKkah6r3goNwz9xgclOj1Q2Uly
s5S3BaNnf8Q9ypygbUMOalYN6KzHloj52inX3fpGfOov0O/1+bkp/3SvATU0aRpB
4bKLt99b69x1HSIkCNxeJQz2klgEMNQYGsnXOYCvnI0cOpGGDxQuIYiZbYbTJb7X
FI7FfV0ryn55mMLgZboGezMhWGYeHpWfa7H0La1ZjgedBsu3HH5VaPuHOngFNa+X
78vbWFcD09biZJwatwZlRjFGItaPSyhqSWJx1IqBZ98ZG9ziOVQ+kq1+uRecI/3S
jNw8LFDnOH20UxJnhRddA7f7cFWkk3WRIIec+wvE7uOibHDwFmqCo6JeOhkZDF5f
R1dfN4CZznMzbUb5iqUb4JwGdViijCaDHTEe3C3nKS1mPxt2kn3H1C37y71Fh5yu
mStVxjZZTdbaP8WPEAEDhK+7eB6pAKa+ERXOlWY1L5nNkh4ahIw5837yIY60oyBh
goiqX/Vy0wEJMa7HgXT3cDnW4NiXQA8nVKsRUiZco136YAATS89iLk3ibYJxzGP+
G2LZ
-----END CERTIFICATE-----

View File

@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFnDCCA4SgAwIBAgICEAIwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux
EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL
DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X
DTIyMDYxMzEyNDIwNVoXDTIzMDYyMzEyNDIwNVowfTELMAkGA1UEBhMCU0UxEjAQ
BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN
eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExFzAVBgNVBAMMDmNs
aWVudC1yZXZva2VkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA584w
VAczDWrlIJXm6+0oYepacPPrVEMC9WwsQFO5GbBDdxrBvxgQ5u8/DDNEtYk0sJMt
zgSLxsYK5duhrwVyICXpKgxMI2fKdKP0zxB/eB4V0vrc6FqR4L8D8XoNPKSaTnjv
NNh3wjLNvmBRfHRSUCe4zEvPZzMLuBIHXRR20prtA90FFV8fliNvMCBIbFkqthjf
fQ/tSXXxNQNjacuHVfY+LVN3xu9Jjll4AaCKKz19rDexq9HTLLZ8y4jBD1eRobp+
spKKu4HNpon+YMp3vJuNmxsTU+xBkbESWGJTFot7lZL1PVBvxdgbSd3OrPKI+QbK
06FN3iBrcW3Yjp04LQIDAQABo4IBNjCCATIwCQYDVR0TBAIwADARBglghkgBhvhC
AQEEBAMCBaAwMwYJYIZIAYb4QgENBCYWJE9wZW5TU0wgR2VuZXJhdGVkIENsaWVu
dCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUSq+9djRFuryFk0Mdqlsy0chljWswHwYD
VR0jBBgwFoAUK6/VOTMLV9KBN/MTWbW7mlEImxUwDgYDVR0PAQH/BAQDAgXgMB0G
A1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDA7BgNVHR8ENDAyMDCgLqAshipo
dHRwOi8vbG9jYWxob3N0Ojk4NzgvaW50ZXJtZWRpYXRlLmNybC5wZW0wMQYIKwYB
BQUHAQEEJTAjMCEGCCsGAQUFBzABhhVodHRwOi8vbG9jYWxob3N0Ojk4NzcwDQYJ
KoZIhvcNAQELBQADggIBAD4XyEjAfU0VE+YRQXUxlqpeJdMij0Io9ZCf1j6JMNFl
/p8vbOm6orK55bYWXCzRaIEkgOGlpQLDIXpViAjmBbXisA97hS1v6rW10W6LyNkN
i7LC6kTgBi4yIV3FiQk5CIE3Tj22+0GjaepHc2bPGaLSRBaoe8uBvDlDqRjxW64H
KYOIyux3WqauEziNEVklXG3VNX+6WfUw0jP9p4cglLaL43htwdavA1g7RP0wE1/6
hvZ242jK4bvCGn/p7IDa8YtgXTufjQf6hJB9kRAJRqrSJQnihb6P9UZr2saaR7mp
28w7a6L1RLSkCSpcwRoTHgTMkFwCD/h7NxZmpoOCBk4vrHY7SXG05ptb6o7x8TD7
lRV/+ay8EIPWCKGpTrGWHGzjxiuXw4TqnfETlvz0rFUG4TapXwmWnaSwTUk5d10v
olBQJ22CIidkKvoW/6bmD6mtyRX/F3KDTN7vHSsjvaty+TzqjWLLd2rjWpZi5xcc
h/lOyWHXbFkakRT879USUvqU/Y6+2CpbZ2ssks4+bnD8Dsdq1fFLTXtLhBBFcz11
amuwx923tJTY9f6e7y3X/TveCKcibo+aluA4ACkYix8mR/oFxmsulW6MTVcqZZ+i
9+oo9hyOPQsJhWYISAtLBuDCqz9fKM4llmnuQZA55FuZBkSmHBRAXw5xwA+gbZcs
-----END CERTIFICATE-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA584wVAczDWrlIJXm6+0oYepacPPrVEMC9WwsQFO5GbBDdxrB
vxgQ5u8/DDNEtYk0sJMtzgSLxsYK5duhrwVyICXpKgxMI2fKdKP0zxB/eB4V0vrc
6FqR4L8D8XoNPKSaTnjvNNh3wjLNvmBRfHRSUCe4zEvPZzMLuBIHXRR20prtA90F
FV8fliNvMCBIbFkqthjffQ/tSXXxNQNjacuHVfY+LVN3xu9Jjll4AaCKKz19rDex
q9HTLLZ8y4jBD1eRobp+spKKu4HNpon+YMp3vJuNmxsTU+xBkbESWGJTFot7lZL1
PVBvxdgbSd3OrPKI+QbK06FN3iBrcW3Yjp04LQIDAQABAoIBAQDN52MaYMLCel9I
0J6slp62SxtHFgPFdzjbk9jC0xuqa92hoIzVF6V73KxeQ/QWZOf+qN2ZEISwbh4k
CzHVa7ryP3qbtQy0rm8xqKm+fGMd6WttWxR6+Gh4AHSaPNYhNf0zE0033ciTIdmL
77ayHAk51e7a2cRDYR5ZxPnxfkoFy5M2y9U7daAZsEjIyoxjGmkO92YU4byPfUxA
vxC+rcSCUHA1OSoKONuGmwAYrLyHHoIgmVSXe6qSjgSWBPFVLp6OKTiBc7klRnlw
EAcoop7NSit+eMCqQM85Tp8wMmKd+9jfoek9yc5ahWgLDMsd5Dlc/+CXoChmQ5dO
w0h7vqbpAoGBAPk5HEuZ752VJQOtcSfJacOsYlAc8tlOBl33negGIplAKYeWKib7
xtCzdU29oW4tPIJO6v+e7y+GcV7n2+6DpoXTUr6f4fG3vPQ3ZKPctsAHyFpKvzR3
137YllShpgQKIcnwC7y/KG+wwgiVjZ+cDWCUuGA9/3m249DdO2b8F+jXAoGBAO4b
1KC32rjzOk3QOZWIvWADbC8MxY0jeM+phtycoeyCreSOKq6dLpX4JD5NZ1XP4uol
wr0Ta0D8FN7T/JRfT8FnoFXTJxwKTi6oGwXQ3Am/bXsz2A2H8vsXL8tgutYfX4Xi
YNf57zNiCTHLC4K9aiS8iJ2UtJIwGi558ONVOPKbAoGBAJcjS0WN1QJ7sDbKuBSo
0LsZj4WGCMA/0RyrTden4NOPVaAkMOvzRF7MdhbvKTbnuApOUbUzbVok7hvgAEBl
FleSEFwKGbu88Zoo/Z9h+nH6RkZ7jfkDtGv4bTJl1YgdnOAZ8wRD6QHS79jE2V4y
BOrNRgMXlhb6Eq5Xe+64cseBAoGBANuLB6tEukQr6AdVRbMNyGbd4QMkyIXRPhRj
IDkLpvVWrJV/S/WCcjDPAkP7xJrHulbgiEUjwZHCnE+0sD/x/ay7KofX0Ei3a8zz
LS9Ym3nValHdxIj9X9mKUIQ6ZSsG9GGTEG4zQg1jiEzEBZH/qf3DZEe/lBryhUFz
J9vEeWSfAoGAPV6RXEP4uJstCBUsWnWXGNtQ8TwS/NsH5FyPuWEXWxJwrUHEDyiA
FhRW2tGH/k5cCZw7lvzmZui+iWBv90l1/N3J2+SKZMVZNGsXCYx9szSd5uAer/Zs
emH0oOBC6NXXVDTIp+vDzy/lx6Xxcp4n6iiI3I+uCOeI5qRhM0GHcdU=
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFljCCA36gAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux
EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL
DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X
DTIyMDYxMzEyNDIwNVoXDTIzMDYyMzEyNDIwNVowdzELMAkGA1UEBhMCU0UxEjAQ
BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN
eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExETAPBgNVBAMMCE15
Q2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtoHIpsxDnO4y
TLKghUGkAIg5102fcVj2sQ4kJhybnN7tfbQ0gZo0GE8A7SQAYM/lO0VcDknp/nkz
A8Lsc5Lc1SJ7z1Iyd+/ZUg3GJE/ijPoPdMlFU4/HndmoMdLhcflCRZiirnLGwdvT
bBrlGD0alK/kOqbSc9LT3oFVXWo2XvAyNtBxU7dHznxT5JuFnPyCZvoY+FvSpCbP
5rV0NLzdCpk+6C4KTs3YAaTh6kVvSCch55E7hE0I2KKLjz1Ge563vPEWOnB40p6w
ZhzcwFIbiXmq8TpA342HInES1tFRwv7ZGSs+B8t/q9nGWTDf1as4+wBrQ/i35Xav
sMuNp+O+mQIDAQABo4IBNjCCATIwCQYDVR0TBAIwADARBglghkgBhvhCAQEEBAMC
BaAwMwYJYIZIAYb4QgENBCYWJE9wZW5TU0wgR2VuZXJhdGVkIENsaWVudCBDZXJ0
aWZpY2F0ZTAdBgNVHQ4EFgQUwRox6T4QUt1kUQDCuZ6NJNKGHIgwHwYDVR0jBBgw
FoAUK6/VOTMLV9KBN/MTWbW7mlEImxUwDgYDVR0PAQH/BAQDAgXgMB0GA1UdJQQW
MBQGCCsGAQUFBwMCBggrBgEFBQcDBDA7BgNVHR8ENDAyMDCgLqAshipodHRwOi8v
bG9jYWxob3N0Ojk4NzgvaW50ZXJtZWRpYXRlLmNybC5wZW0wMQYIKwYBBQUHAQEE
JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vbG9jYWxob3N0Ojk4NzcwDQYJKoZIhvcN
AQELBQADggIBAEFjWfL32GKfOExZ+4FPUj80V282pJ4iEuRSmbti/EVZse0GsKvZ
xF1swDdoTrcdNTruHxIKfojwKz28XP6JoM6MUruxQglwdwZGJGlC0KpanR7dvQHa
XT/ushugHptobRh3+f47Gbyd0A0MKfGFLGLC1XZokdrvLPoKHCJWq/DRUgHMnn53
FrM7a+JXZUpkSU+uqCPTMpkmuZc0E+SPVtLGiH71q1kbMzJKIKSuzkRCG91PW8fT
B2PUmy4W6YV6MUSi+jHx5JDLvjwycTsNFBiK0zZtcKsaz9QWi/qnrjkCWR0LXaRZ
W99ER7hEMe8J5CYt3lrmtaVL8lZ4TnoQE/Rsr1GBr7+WA9WnB/ijSG/3Q02hyqcK
EaFE+aqOvY03UlIyk/m+PFoRwge9vERf2dGgFN4osbczDMwXc67jx1MKyFQLQvg4
TcN7fXCbqQeWDH0RCAF4goFGqjq/KZQYHN+BOJ1cubUxbY4+9zuQ7pZ1s1tonchA
TQFYbZNXKFUYTrgSSZRoJGGkSQkUKvARsraIdFvOt6qzLq4k86efQAgPDwFd1Kni
+iML7b3fIzHAl00WYH/hvjyjiDbh9YJpGorflgzjyt/dDNcYk5mcMNJJyrY/kPUG
3/eiMMIfW6UFSJq3ePK0NBFCzmdGWGUb+ZQS+JYP51eN2zsbZRm/Pfel
-----END CERTIFICATE-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAtoHIpsxDnO4yTLKghUGkAIg5102fcVj2sQ4kJhybnN7tfbQ0
gZo0GE8A7SQAYM/lO0VcDknp/nkzA8Lsc5Lc1SJ7z1Iyd+/ZUg3GJE/ijPoPdMlF
U4/HndmoMdLhcflCRZiirnLGwdvTbBrlGD0alK/kOqbSc9LT3oFVXWo2XvAyNtBx
U7dHznxT5JuFnPyCZvoY+FvSpCbP5rV0NLzdCpk+6C4KTs3YAaTh6kVvSCch55E7
hE0I2KKLjz1Ge563vPEWOnB40p6wZhzcwFIbiXmq8TpA342HInES1tFRwv7ZGSs+
B8t/q9nGWTDf1as4+wBrQ/i35XavsMuNp+O+mQIDAQABAoIBACZaJ5xFmH/F3nQX
pXvbS2eBOQZxnWvoUg7q9dW8dUcF4cpksBP8H65sC7nJsvqlNXq7HJk0FyQOvBWy
RJYU6qsvT+1FTK2/jV+c3WKMFwOhGNZl5VemA0C8mIe/1PhqdO7DIIygOfxLAaba
EAKD9K4COGfK3rbQOw2rCBFVXI+ed0L2q+IzpyZdvtUFVdDKF9d5i3m2Qr6Ayeuq
dpf2ig69nC+4I505govODX2O9+q7x+zkddooxL72OXdEQFHn8p1K69Zk9iLz2jPX
HPVyKXGYzCOyfZZk/jJjBCv+8bkFQg/Ncm/KSBeBjp2Gxo6EjfERUhkShkxtOB5P
pAETPXECgYEA4SP/hV/MbmXnouKvjoWCZ0dYH92lNVOMMr2Ij/nOpvxTAyc1FXg+
m5NrvMjRnGu32QfLcJmaXnK9YjqAZD9koQ3DisS0FbwTa7V8NSR5ImJfPWpa2VMP
SiyWQCh6Iba2852hl8Ryph7wbbSXAcocthx6dIuP/QsX2Q95jrh+g0cCgYEAz4XO
MSjwsrT/9i3voVKD+7eWBYkFpOJ+piYxrrmIrkMSWrwPIE9KBFfn7TlPvBPCb+W2
uN/eLtO8HDePKfJQdZxphvlmy8eeIfjzBhod2HELJy3ShHS57uaD7iLLhPXrV9Gm
As99lS+kKGeH30j9Si53oQU0eIyH8iZX+beR3x8CgYB/DGhqZHghqIIByjhVjgPb
skgJm3NaV25bR9ejn829L9DMi7iKCBQUiSmYHB8lTSgvYhWs0hFp0QgMQYUojRmF
RRYe3gfd6AdxlbWk65MsEyU5rCXeU9/h9K1JQU5CbjBp439H/MTR982nquw4R0zS
e9mioQs9OaBYjkIDhxtliwKBgCRtaHRYq2ezPfsItTesNF7LKxptov/+ghzIN5Bk
IQn13BLxT/Zr9KIujBeoJ8br8QWTXS+2nFm78RlC526Finoaqqt2vASpVajA+mfn
zbVgooSOFpYJp1m4PRBgKzl7sYQI2QtFQNYfNsGg6sjXFx8eaQFq2HsQsAxhjq/W
+VQhAoGBALWrW12fQ0dJffRIXs/PCfwPy24071Q41YEadiZbV2/ZYlpDG6MQIENe
KaDMpflkuHhR6RQZmfJoImLvpKH0iOoxnTzPJhDQHKvwiqei3v7SuN2vh7VWGCGH
ikLROTT/oVrq4Z5XrZuAR1wtqRgi/KwvztG+QTzxLhygOmmkbSdt
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,20 @@
-----BEGIN X509 CRL-----
MIIDPDCCASQCAQEwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0UxEjAQBgNV
BAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQLDBBNeUlu
dGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBFw0yMjA3MjAy
MDIzNTNaFw0zMjEwMjUyMDIzNTNaMBUwEwICEAIXDTIyMDYxMzEyNDIwNVqgbjBs
MB8GA1UdIwQYMBaAFCuv1TkzC1fSgTfzE1m1u5pRCJsVMDwGA1UdHAQ1MDOgLqAs
hipodHRwOi8vbG9jYWxob3N0Ojk4NzgvaW50ZXJtZWRpYXRlLmNybC5wZW2EAf8w
CwYDVR0UBAQCAhADMA0GCSqGSIb3DQEBCwUAA4ICAQBbWdqRFsIrG6coL6ln1RL+
uhgW9l3XMmjNlyiYHHNzOgnlBok9xu9UdaVCOKC6GEthWSzSlBY1AZugje57DQQd
RkIJol9am94lKMTjF/qhzFLiSjho8fwZGDGyES5YeZXkLqNMOf6m/drKaI3iofWf
l63qU9jY8dnSrVDkwgCguUL2FTx60v5H9NPxSctQ3VDxDvDj0sTAcHFknQcZbfvY
ZWpOYNS0FAJlQPVK9wUoDxI0LhrWDq5h/T1jcGO34fPT8RUA5HRtFVUevqSuOLWx
WTfTx5oDeMZPJTvHWUcg4yMElHty4tEvtkFxLSYbZqj7qTU+mi/LAN3UKBH/gBEN
y2OsJvFhVRgHf+zPYegf3WzBSoeaXNAJZ4UnRo34P9AL3Mrh+4OOUP++oYRKjWno
pYtAmTrIwEYoLyisEhhZ6aD92f/Op3dIYsxwhHt0n0lKrbTmUfiJUAe7kUZ4PMn4
Gg/OHlbEDaDxW1dCymjyRGl+3/8kjy7bkYUXCf7w6JBeL2Hw2dFp1Gh13NRjre93
PYlSOvI6QNisYGscfuYPwefXogVrNjf/ttCksMa51tUk+ylw7ZMZqQjcPPSzmwKc
5CqpnQHfolvRuN0xIVZiAn5V6/MdHm7ocrXxOkzWQyaoNODTq4js8h8eYXgAkt1w
p1PTEFBucGud7uBDE6Ub6A==
-----END X509 CRL-----

View File

@ -0,0 +1,67 @@
-module(emqx_crl_cache_http_server).
-behaviour(gen_server).
-compile([nowarn_export_all, export_all]).
%%--------------------------------------------------------------------
%% APIs
%%--------------------------------------------------------------------
start_link(Parent, BasePort, CRLPem, Opts) ->
process_flag(trap_exit, true),
stop_http(),
timer:sleep(100),
gen_server:start_link(?MODULE, {Parent, BasePort, CRLPem, Opts}, []).
init({Parent, BasePort, CRLPem, Opts}) ->
ok = start_http(Parent, CRLPem, [{port, BasePort} | Opts]),
Parent ! {self(), ready},
{ok, #{parent => Parent, crl_pem => CRLPem}}.
handle_call(_Request, _From, State) ->
{reply, ignored, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
stop_http().
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
stop(Pid) ->
ok = gen_server:stop(Pid).
%%--------------------------------------------------------------------
%% Callbacks
%%--------------------------------------------------------------------
start_http(Parent, CRLPem, Opts) ->
{ok, _Pid1} = cowboy:start_clear(http, Opts, #{
env => #{dispatch => compile_router(Parent, CRLPem)}
}),
ok.
stop_http() ->
cowboy:stop_listener(http),
ok.
compile_router(Parent, CRLPem) ->
{ok, _} = application:ensure_all_started(cowboy),
cowboy_router:compile([
{'_', [{'_', ?MODULE, #{parent => Parent, crl_pem => CRLPem}}]}
]).
init(Req, #{parent := Parent, crl_pem := CRLPem} = State) ->
%% assert
<<"GET">> = cowboy_req:method(Req),
Parent ! http_get,
Reply = reply(Req, CRLPem),
{ok, Reply, State}.
reply(Req, CRLPem) ->
cowboy_req:reply(200, #{<<"content-type">> => <<"text/plain">>}, CRLPem, Req).

View File

@ -0,0 +1,20 @@
-----BEGIN X509 CRL-----
MIIDPDCCASQCAQEwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0UxEjAQBgNV
BAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQLDBBNeUlu
dGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBFw0yMjA3MjAy
MDIzNTNaFw0zMjEwMjUyMDIzNTNaMBUwEwICEAIXDTIyMDYxMzEyNDIwNVqgbjBs
MB8GA1UdIwQYMBaAFCuv1TkzC1fSgTfzE1m1u5pRCJsVMDwGA1UdHAQ1MDOgLqAs
hipodHRwOi8vbG9jYWxob3N0Ojk4NzgvaW50ZXJtZWRpYXRlLmNybC5wZW2EAf8w
CwYDVR0UBAQCAhADMA0GCSqGSIb3DQEBCwUAA4ICAQBbWdqRFsIrG6coL6ln1RL+
uhgW9l3XMmjNlyiYHHNzOgnlBok9xu9UdaVCOKC6GEthWSzSlBY1AZugje57DQQd
RkIJol9am94lKMTjF/qhzFLiSjho8fwZGDGyES5YeZXkLqNMOf6m/drKaI3iofWf
l63qU9jY8dnSrVDkwgCguUL2FTx60v5H9NPxSctQ3VDxDvDj0sTAcHFknQcZbfvY
ZWpOYNS0FAJlQPVK9wUoDxI0LhrWDq5h/T1jcGO34fPT8RUA5HRtFVUevqSuOLWx
WTfTx5oDeMZPJTvHWUcg4yMElHty4tEvtkFxLSYbZqj7qTU+mi/LAN3UKBH/gBEN
y2OsJvFhVRgHf+zPYegf3WzBSoeaXNAJZ4UnRo34P9AL3Mrh+4OOUP++oYRKjWno
pYtAmTrIwEYoLyisEhhZ6aD92f/Op3dIYsxwhHt0n0lKrbTmUfiJUAe7kUZ4PMn4
Gg/OHlbEDaDxW1dCymjyRGl+3/8kjy7bkYUXCf7w6JBeL2Hw2dFp1Gh13NRjre93
PYlSOvI6QNisYGscfuYPwefXogVrNjf/ttCksMa51tUk+ylw7ZMZqQjcPPSzmwKc
5CqpnQHfolvRuN0xIVZiAn5V6/MdHm7ocrXxOkzWQyaoNODTq4js8h8eYXgAkt1w
p1PTEFBucGud7uBDE6Ub6A==
-----END X509 CRL-----

View File

@ -0,0 +1,35 @@
-----BEGIN CERTIFICATE-----
MIIGCTCCA/GgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux
EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL
DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X
DTIyMDYxMzEyNDIwNVoXDTIzMDYyMzEyNDIwNVoweDELMAkGA1UEBhMCU0UxEjAQ
BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN
eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExEjAQBgNVBAMMCWxv
Y2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOpBaUId5ga7
NzIXvSvnzqLAsyejjEZYcSVwWQinAH3il/Y/NbUOakPbDCQbb3U7p3oD6H0R0pVx
x0nQyhh2XlSNRc8ORAcrZlfS6/Su5o07htSKL7FZ0ySYbia2EQ6ZRFpke/+UPwr3
eXSQUhdOX+iA0Uf5gN3fUtgGorCuT2POGemGjBvYiPoA3aD1MxHyV7PnI7CoTw8Y
cvFiXIW0Jqp69ZgSuzrmfIiMyum94pfkB1ljxao9TBSFqJTn2A3ysNZAXpGPyfP+
1zPznxiMSvAFLihUVjKwE+glSNKvdhUgl3yAxaFI9+IeoAytPNmQU+E+o4YCuRHT
TCM1Tch95/ECAwEAAaOCAagwggGkMAkGA1UdEwQCMAAwEQYJYIZIAYb4QgEBBAQD
AgZAMDMGCWCGSAGG+EIBDQQmFiRPcGVuU1NMIEdlbmVyYXRlZCBTZXJ2ZXIgQ2Vy
dGlmaWNhdGUwHQYDVR0OBBYEFAPF2lui5EhHG8lTbpglZ3BZYj2kMIGaBgNVHSME
gZIwgY+AFCuv1TkzC1fSgTfzE1m1u5pRCJsVoXOkcTBvMQswCQYDVQQGEwJTRTES
MBAGA1UECAwJU3RvY2tob2xtMRIwEAYDVQQHDAlTdG9ja2hvbG0xEjAQBgNVBAoM
CU15T3JnTmFtZTERMA8GA1UECwwITXlSb290Q0ExETAPBgNVBAMMCE15Um9vdENB
ggIQADAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwOwYDVR0f
BDQwMjAwoC6gLIYqaHR0cDovL2xvY2FsaG9zdDo5ODc4L2ludGVybWVkaWF0ZS5j
cmwucGVtMDEGCCsGAQUFBwEBBCUwIzAhBggrBgEFBQcwAYYVaHR0cDovL2xvY2Fs
aG9zdDo5ODc3MA0GCSqGSIb3DQEBCwUAA4ICAQCCTuM+KXLH2EuUn+TB78LgYrzl
/dlPj5UWPTDgiF+93fQSNvkg2uWVKPkckKNKttg+VyoRnRX87vI/bZp+dzvHfp5v
hXZuv2TkvA8ZMnJ6QUbAmiQlNTqNh0esftfXbmZAo+VvtwC/NZfevH08SO1+3R4S
PMC/Bnqmv/G3mR2qWqLu4X1ikpkgNGj4DTEkkqZFerlRyaeZHZLSVRNt6x3u4lXa
KQKbTPYP+yYpJfi0eBFy6t0hdN63BZDao3z9fulpxfjWfL32gt+TQLMf/6aJugs+
+2BS0LAWXr9mP89Ljzo4V0G6pMBQ84/oK+mYtkFCNjRaAT+b4xGE1Ttp9H40xRZY
hm2xzVH05PgXw+IJdfnvH+245vtPUTtrnysy9FEfF7px3tAAZCiT4ogrgr52+VK6
vGliEIxZq2ICWCK1Wy0i9zZFk3lQCFF3bWjKQCapCAFPE0naZZv7g10HkXl8+5Gc
1aSyP2LcroUyDfYx8VwKw1sLi1KtO71hs/TOTeoAPzFBmZA8h325mU5krpQtuNPg
TRxAPLztWltHR3T+WKOUMq+YZx0IiUvS6VZaCeoQeXTSZZTX5j6BXz8ffmyAcXMZ
tNATxIgDyGUfw0V6fiKQrnZCqP+fF/tdks5LKeU6tYE4JKWDarIzNNnPmhPQnJOI
YeW1Ts3aExER9EorUw==
-----END CERTIFICATE-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA6kFpQh3mBrs3Mhe9K+fOosCzJ6OMRlhxJXBZCKcAfeKX9j81
tQ5qQ9sMJBtvdTunegPofRHSlXHHSdDKGHZeVI1Fzw5EBytmV9Lr9K7mjTuG1Iov
sVnTJJhuJrYRDplEWmR7/5Q/Cvd5dJBSF05f6IDRR/mA3d9S2AaisK5PY84Z6YaM
G9iI+gDdoPUzEfJXs+cjsKhPDxhy8WJchbQmqnr1mBK7OuZ8iIzK6b3il+QHWWPF
qj1MFIWolOfYDfKw1kBekY/J8/7XM/OfGIxK8AUuKFRWMrAT6CVI0q92FSCXfIDF
oUj34h6gDK082ZBT4T6jhgK5EdNMIzVNyH3n8QIDAQABAoIBABG/NN86bqPR6SOV
YtKBtEjmOmxlWoo1xxSkB0q9hC8FTKfuL/5wgiJz5N6TaYVDKLP4udNH12FVBlkU
RUtHJGxZa5F9LjAw3IcIxrF50qOef994PJa+DF34YlfycSZe/Cuw8yfwrjoBd6Ua
De4QFPoDUFeYkme8tIUDM64Y9pDD9llfS/ZOQiCAgn+8mk99uCnJVIFo19Umof5N
009Cn6PElDKOw40Pz8s4ZJ9bpXH/YPKLeNRn0wlgFzYGhJ1z74YBmXF3CUHU6WEy
pVbJkPDAwnFScuAjpwJmzO4TDBfTBd5L/CJy4nsLpY06UOm1PHuUpAhPAgiwuADR
TM3100ECgYEA/F5Glpb0ROR38UTgCSOT8xvxyzNV84YMaQrWzGMUxqJpWWyVaKEy
k/jzUxUKbkUrJDz/Y+bcGElVCI+g8x0yekpcLn1mtB8XwltpmCSBhNBTMgUHnI9v
M+PadQhyZiJFrE/eoXo0V/K30Xv8jokWhWGTIkCdhiVSks/0eWSzPzkCgYEA7aBo
d/0e0R2Y2wXDsfGqxcKFs4IMHnzNl0isycakLOoZwinKPvb1MSg1zxW0mU+I1c/n
18rftcS8/siai26iXErDeZnvtlj+OKzQboRQf5JvDHp+rXBjGpZSmI2Nb+kAQ71S
JeIkglfBHGCKAMJQjE3N3U9YMBqCB1xdJ/TnNnkCgYA4PJnmPMU6BN9leD+kSbVS
W0vKSCpDFf/1+GBdM0cR7GclcjjpE+K9bqBqRyoH4In4jU8r5+nrz4uPWNI42qzA
64kXIwKb6MHWoaAqMxhZjEK9xrknfh79pSytH7C+aay09SdbPGwlnQSxPbvN12aZ
WmD7JQL1PaPk60pDMtluoQKBgDYJLxhyB+r3twW/VtQFJ5dW975tSUI5kSrgzOIJ
eNX52iesByCwWet2wF26Ctp+Gpi8cXVB3gNgnLW3emVQoD0qhy8E0Vz++bh7m941
2nRYIUaOKHZaQz8NhfTI46vaKUQ+LgsNVM4LFI/WaCtqBJUTMEguPdiafo0b9Ncc
OuPJAoGBAPSYfuXinT9IAVwzIjc1TkfdN3G39ckBFQtNSvIoynBro+dlh3us+QJi
i24MtpUiBbCKmFZrrk5WrddxNLbE/JSa40+O+s1RaP3SfNuDGTWMe3HGMh+PVH8D
yte7L8pWouXSQPLVXUb3SmPboxBvXwPZMB54gkbKcSB2Gg7CjfCS
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,467 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 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_ct: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),
OriginalListeners = application:get_env(emqx, listeners, []),
[ {original_listeners, OriginalListeners}
| Config].
end_per_suite(Config) ->
OriginalListeners = ?config(original_listeners, Config),
application:set_env(emqx, listeners, OriginalListeners),
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(10_000),
OriginalListeners = application:get_env(emqx, listeners),
DataDir = ?config(data_dir, Config),
IssuerPem = filename:join([DataDir, "ocsp-issuer.pem"]),
ServerCert = filename:join([DataDir, "server.pem"]),
ServerKey = filename:join([DataDir, "server.key"]),
CACert = filename:join([DataDir, "ca.pem"]),
Handler =
fun(emqx) ->
Listeners0 = emqx:get_env(listeners, []),
{[SSLListener0 = #{opts := Opts0}], Listeners1} =
lists:partition(
fun(#{proto := P, name := N}) ->
N =:= "external" andalso P =:= ssl
end,
Listeners0),
SSLOpts0 = proplists:get_value(ssl_options, Opts0),
SSLOpts1 = lists:foldl(
fun proplists:delete/2,
SSLOpts0,
[certfile, keyfile]),
SSLOpts2 = lists:foldl(
fun({K, V}, Acc) ->
[{K, V} | Acc]
end,
SSLOpts1,
[ {certfile, ServerCert}
, {keyfile, ServerKey}
, {cacertfile, CACert}
]),
Opts1 = proplists:delete(ssl_options, Opts0),
Opts2 = [ {ocsp_responder_url, "http://127.0.0.1:9877"}
, {ocsp_issuer_pem, IssuerPem}
, {ssl_options, SSLOpts2}
| Opts1],
Listeners = [ SSLListener0#{opts => Opts2}
| Listeners1],
application:set_env(emqx, listeners, Listeners),
ok;
(_) ->
ok
end,
OCSPResponderPort = spawn_openssl_ocsp_responder(Config),
{os_pid, OCSPOSPid} = erlang:port_info(OCSPResponderPort, os_pid),
ensure_port_open(9877),
ct:sleep(1_000),
emqx_ct_helpers:start_apps([], Handler),
ct:sleep(1_000),
[ {original_listeners, OriginalListeners}
, {ocsp_responder_port, OCSPResponderPort}
, {ocsp_responder_os_pid, OCSPOSPid}
| Config];
init_per_testcase(_TestCase, Config) ->
ct:timetrap(10_000),
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),
application:set_env(
emqx, listeners,
[#{ proto => ssl
, name => "test_ocsp"
, opts => [ {ssl_options, [{certfile,
filename:join(DataDir, "server.pem")}]}
, {ocsp_responder_url, "http://localhost:9877"}
, {ocsp_issuer_pem,
filename:join(DataDir, "ocsp-issuer.pem")}
, {ocsp_refresh_http_timeout, 15_000}
, {ocsp_refresh_interval, 1_000}
]
}]),
snabbkaffe:start_trace(),
[ {cache_pid, CachePid}
| Config].
end_per_testcase(t_openssl_client, Config) ->
OriginalListeners = ?config(original_listeners, Config),
OCSPResponderOSPid = ?config(ocsp_responder_os_pid, Config),
case OriginalListeners of
{ok, Listeners} -> application:set_env(emqx, listeners, Listeners);
_ -> ok
end,
catch kill_pid(OCSPResponderOSPid),
emqx_ct_helpers:stop_apps([]),
ok;
end_per_testcase(_TestCase, Config) ->
CachePid = ?config(cache_pid, Config),
catch gen_server:stop(CachePid),
meck:unload([emqx_ocsp_cache]),
ok.
%%--------------------------------------------------------------------
%% Helper functions
%%--------------------------------------------------------------------
assert_no_http_get() ->
receive
{http_get, _URL} ->
error(should_be_cached)
after
0 ->
ok
end.
assert_http_get(0) -> ok;
assert_http_get(N) when N > 0 ->
receive
{http_get, URL} ->
?assertMatch("http://localhost:9877/" ++ _Request64, URL),
ok
after
0 ->
error(no_http_get)
end,
assert_http_get(N - 1).
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).
%%--------------------------------------------------------------------
%% Test cases
%%--------------------------------------------------------------------
t_request_ocsp_response(_Config) ->
?check_trace(
begin
ListenerID = <<"mqtt: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 = <<"mqtt: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 = <<"mqtt: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 = <<"mqtt: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 = <<"mqtt:ssl:test_ocsp">>,
%% should fetch and cache immediately
{ok, {ok, _}} =
?wait_async_action(
emqx_ocsp_cache:register_listener(ListenerID),
#{?snk_kind := ocsp_http_fetch_and_cache, listener_id := ListenerID}),
assert_http_get(1),
?assertMatch([{_, <<"ocsp response">>}], ets:tab2list(?CACHE_TAB)),
ok.
t_refresh_periodically(_Config) ->
ListenerID = <<"mqtt: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 = 5_000),
ok = emqx_ocsp_cache:register_listener(ListenerID),
?assertMatch({ok, [_, _]}, snabbkaffe:receive_events(SubRef)),
assert_http_get(2),
ok.
t_sni_fun_success(_Config) ->
ListenerID = <<"mqtt: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 = <<"mqtt:ssl:test_ocsp">>,
ServerName = "localhost",
?assertEqual(
[],
emqx_ocsp_cache:sni_fun(ServerName, ListenerID)),
ok.
t_code_change(_Config) ->
ListenerID = <<"mqtt:ssl:test_ocsp">>,
SNIFun0 = get_sni_fun(ListenerID),
?assertMatch({ok, _}, emqx_ocsp_cache:code_change(vsn, state, extra)),
SNIFun1 = get_sni_fun(ListenerID),
?assertNotEqual(SNIFun0, SNIFun1).
t_openssl_client(Config) ->
TLSVsn = ?config(tls_vsn, Config),
WithStatusRequest = ?config(status_request, Config),
%% ensure ocsp response is already cached.
ListenerID = <<"mqtt:ssl:external">>,
?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
QTAeFw0yMjA2MDYxNjU1NTlaFw0zMjA2MDMxNjU1NTlaMGsxCzAJBgNVBAYTAlNF
MRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAoMCU15T3JnTmFtZTEZMBcGA1UE
CwwQTXlJbnRlcm1lZGlhdGVDQTEZMBcGA1UEAwwQTXlJbnRlcm1lZGlhdGVDQTCC
AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANbHOqvXq/7kNmXX6DW4O6EW
/2PBdj/1oPwdIOYo6NfmieCa0BoQ/+W1k+Cj6OR75m0c6JmzPLfUckMvduuW/tJ9
aDz/wFFbSYv/bA9f99mNXI0cebAZvYOI+cRBF4nyag3miRbMcCiOPMMDq936F3hd
Mi6CL+pIFHif74Q66Ncx10M/nL+E9sEhVyTLrAF0FsMmjkSPdzChb+HByKWRD4f2
OYiJxhvJNjLf/+qtn7+A5cwqqbW8zYi6jJPWun24zd2HWHLfehVW8CYzGxBJR7kw
KRnPXEuXhnkgEkgfqj2122Jyd35UC/VUpJW1uJJgFNa2w7+bDj7PS6i4kdqjcEHF
GqUELN8BUIwyicvJ25KnGt0J5JnHy0rDfJeVv9/6EB1Ekm/tKwtJsKLqLThH5Kql
nFM0PUx6OV3nYzXvmmU44SvfIDPy3rhhpVCyaBBzdPyt0ugDkKuwwWcuo6KWSxR1
lGTs2YjIYPwnRm8OqvynqCsn0vWUFOluR7suKlUmJFdPpWw04x+v1GZ3YRAig/GL
6qtByXDgZ27F4QwNPR5nGMmiu7M2QOE/nJS65A3mjrKeYSD+reQ/WbIWtLuHIhn0
XDfHIsuzG6yK9Up1Ske+S1Uos8hTGzvF5NhJlqexd5VJ+VKYLtLkEa/tOj4p/Z5S
3nH+tEqg15nE54ovtDMdAgMBAAGjgaQwgaEwHQYDVR0OBBYEFNdekbfOfTuU9Z2G
fCx0uoDMaaT5MB8GA1UdIwQYMBaAFJf5t2YMW5pYapGnvfGXaTDV4tQKMBIGA1Ud
EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMDsGA1UdHwQ0MDIwMKAuoCyG
Kmh0dHA6Ly9sb2NhbGhvc3Q6OTg3OC9pbnRlcm1lZGlhdGUuY3JsLnBlbTANBgkq
hkiG9w0BAQsFAAOCAgEAn6Mw7+BBqzsEEtSDrd97v8niqJdVXPgxyIeHJvdZm1iF
3eRmxAUXy1DvD8tCqI8vJoz6p/4ELjedxc/Pcum0OravAElO6+jBOplwZCyhmpl8
D4wgh/tbKnOWL20sX6sQESq/dORPf/oPDm9CBu55LaYnNTQHCNtYBZ7UrBhlsdYA
CDtsthnYcC4bYTjmm5fYlg/r3LeVN5HINYFUkh3ffKxIpqWdYOhQShOHv2k5hrzM
Lg8qu098q3kqJ+2thauXuEpD2HME0HelXaqWokwv9ETtJpuA6xQshJrT+2DjF3CY
UY3qdwjWwLr9xjf9DyHmC4hdsaO2ZAqKfMoVi6pcJ4F/20vsTM8063SFxKCbb9aw
hpzLWHB2iiKdRs/bczrW7f6XUbCFF2Et6Wl271Q7cyf8GzIdepvGsRGlcl6UaZ40
wGfpQTgrCZ/kWHNaii4BAXGc9oj8f4NeKRVybV+cCwboed0cvNqnnKBPuz6iQz9c
JzVc7SSgOoAPJ5oh8QYi0vhfVYo34C+ZC+NderWGtMrcQrUzYH1vAv1MQ7Sfxnso
VWO7tCIjeBHcZZ+F7t2d4dJEaGYO9eYjIKSq2brFEQ+4Ix124ZWSbluCzQIXuaR3
49qRH9Ont5UW4mGBAAgSAbyQxPkOZ3IH9vIFa61jyUE4RKGxkOAwHgajKPpmG9w=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFzzCCA7egAwIBAgIUFiQDd5Zw7kGG4XxaFayO4MUPSl0wDQYJKoZIhvcNAQEL
BQAwbzELMAkGA1UEBhMCU0UxEjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJ
U3RvY2tob2xtMRIwEAYDVQQKDAlNeU9yZ05hbWUxETAPBgNVBAsMCE15Um9vdENB
MREwDwYDVQQDDAhNeVJvb3RDQTAeFw0yMjA2MDYxNjU1NTlaFw00MjA2MDExNjU1
NTlaMG8xCzAJBgNVBAYTAlNFMRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAcM
CVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMREwDwYDVQQLDAhNeVJvb3RD
QTERMA8GA1UEAwwITXlSb290Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
AoICAQDdpbLFm3BSkpA22VYdjksOBNmVzM3v26I/Vx9H/j0/k7TTPlQ1bmlvFV5m
TiSRgXXuTpw/Sl2x2OpMnIKS11JKf+bsD2jJW+hHDjqykRCjKSec5iAYrBlm28kE
SIxAOuG/uGWD2aGUd0WvHCDJgI/rXmjieKncZlyybqukJSGis7ZJ0pSaVQMfAaaW
p/lHrsjZawoGi/SJrEwRGsXhCh4eGW9ZGrRmuv8zbp0AFy+YImM/6nysonteBsDl
s9Bjiuj3uM+RqKHCMH/sIF92wJ6rnQSIvjJgvNvENHxCdZWy+dzxlP3mYZ62k7ii
7D7uyBk9hClwGDbEKtrEXmXhhmPO+f/8uOKyOGdhanNu+3Wl9XJZnVYSIEOveypW
4ZD02DBd84mkS5Y4Cq+VDYYRgK8SwfsdyUlxRIz63w1jD6seEguizVlf0J9+G5yW
4rBLM8WAAR6n9PGbFPF8j9r6N8ld56gdKzPhXM5/TpZS/egCbtTunIRp+h7MdE7B
Ztifh7H9MBCuWx+axgEjjiukAIMhMd0NiUr5iWFQcQLg/q8bNsnZy/AKHhN5NkKp
45MRLBgeRNMEBCLc1kOJsbDTGJ7KH1s7fIPATHj0AMF6DZRjO30lJBgUbnL5VOzv
uNVdVhrLIV+Az7v3kSffeoHLiIdQEK2U9OYbkWqz+i/a0a3kjwIDAQABo2MwYTAd
BgNVHQ4EFgQUl/m3Zgxbmlhqkae98ZdpMNXi1AowHwYDVR0jBBgwFoAUl/m3Zgxb
mlhqkae98ZdpMNXi1AowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYw
DQYJKoZIhvcNAQELBQADggIBAMB94Md8vcKF9rNrHxE1wcCMPKlPUu13y0MAyc2c
fXEWM2R756xg8zNM+InIrJ4olmwqdS79W33Wuk+jCI2zcyb5/YRmaVB+wpCwQoym
gDKIFypxxM3A010aDI3Z2IgL4iFtF98NC3MB60xeXQIAkk1TtggnpI8HMTLxF4Lz
lZtQXYcBXq7mmLuuQjaP+iWoqzKL6GaL395s3JT8EhgnXB2s7c2/5v3/r+xBbdsX
XphUAgOXB9eXTfHDfxeBZSpwDL3LyuSO3VuD4ByxXhykKR9URWwBNSS9CzwemGn3
n5wal18L3XDF4g7SSGzbU2tjFAbzDdaUXerwhhtes2LePgx1ukuBWO9ctMrql/WN
bV/nL5c/eAfyKQ93oWYa/a09kyIey/YqknKTEcnHVq1rBylwdc6Vp0Wd98y7W2sM
gR6n/d41W81a44/q/g7Pxp05byX0EHfmtFjyIGaO+5Y+l1Y6mnT/+lO3ppxgic1m
NceB1MZ2mPWZKio+/eTcwF88M2CXUgDTwZUf+pLfP2ow5TCQCSt57ftb4D9BZ/Bg
2wDNyEiPLzvpN9eklZiWNp6zPp8WX+RskejMF//8d2lWWnZ4jgppXqWj3DO5fN8v
xXVwi/qOGb5f51MMB+uSlp9LdspJYQPJZ9E7WVIV4BBbYcygM60pT0z6EiPK5x99
8d9i
-----END CERTIFICATE-----

View File

@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
MIIJKQIBAAKCAgEAvte8rjsOQmvDS/5Nu+HVwiphq1TmmMnkEXA2J8xar3WUbyue
hW4JaWg1c2FBTPG9egGmrZ3Y7x62ZfBCUap2IgyVZjC9F74EMOSTU9Xd2QSsU9Ql
zAwq6RvHfUQj2mUgEibyT4MtaqwiIXC7zALXNPr2JYD/08/5ORysiUUt5Yi+vrgf
MYntCJ84ZGkoaP4Y/TUf4uuVKJlrZCcQ48g8h1z9mOlxznhm26esn/S0xyuThFEU
I2xWySannxSCTqVt/+IDEeVTIw4ZVLCTnGsoV1weKRKfrI32krnMYrSxZPoAp9IS
SGio8eWmtn29O8Z/N87DEFKdWVZCTdq1wb0RlfP6SMAYltFw01hj5UKgj9Iqzrnd
iK7s9svs4HmyVS3TbfwJOLy8GTZOsNxFLbO//yVJbrXHh6YblKHa5YN4/DTiN9yi
gllAk/Sh3drrnBr7wn7zRD920GUZPd/MI/e6My4CMcvVBAFFaTH4iu/hpYLMSH4M
O7eGNIAvpLuVMNgrDTZ+JbUnXaVbIU8CtrT+MvBFrj1eexGNWtoOA22NxBvXVrJt
DV5kBO9mwhCKMnth1wxinCrOXE8wS1YslykIWTEiKZFPSeOhivIojZNSKOrMMpKn
h8nglHiuBN2RBottFzuMLAzeLIOmkhCKBVbq4wNKZxzgdfQsk9h8FSZXgOkCAwEA
AQKCAgA8xG0r/Vn5BF9XX05ZOPvbq6sV5x7WH9MjSlu0KHnnzMTK8VS0n+kXSztZ
1en5GjB/HO958P0Whu0FYI99eY3MIb2goHxVhwfGmcHfvAW8CKTmvHKcmnM/br0C
wiO4cuLXZNdbQiXABbeIdmEWAQGNuSuPnZfYVizBZsP5obGPmQ9Do1UrbQOw7yIB
twpLBD3ownW4x9Li7pcneNkD7sp3P8DwY8T9PZFi9+0bscuub821ICwNa33m5wwQ
t3MWn8563/iiRidGzkJ6mQ8ni2d5CbgA1BFL3Jha+BaAh5DE2mZDCb0QmWLSQ5Hm
IfuTgQ0ZNBkgpW28+J58M9wUKKPkUtafV25fqPOVnynKgdPVlZEH0gUpZg9oQ4+c
DTCHkLFIQDCUEIrGk0XyyGLAmqxTqQaD1qNuT76cR+t/SJXCr3CFDp/haJ4Xzl4k
YiOWcVMX1gcLY1lz8hExLtg+mVvsHzIP7mvi5MBNWxAsfupUx+cwOZ4+hFBOtxcb
/QVbVS0BXWkYBQj92JB1yEws2nA+Yh7Ua4KBlq5vRYcJuo4zDBtL4M56V6B2pcvE
4+ZkBSgkqHGukSPg5ThdRpB1spi/oSjfr+dkdYsvQJt/vYejh8JEyfhTCy3m9lbd
0cR6VuoahkZKYYcrPwZjIbwtYhrrHb66M0cCidTgC9I9c9CJQQKCAQEA4QBl5ZI/
Wlpx6Pwq6YzU1Dsm0Fx2NtXA7kpeMp5o04Ea0gEFrsb4/817KPXWRry2dvNrKgyq
+j3ebN9bJ3UZCMhl5CeXS97VHP2rtfK25fHfaH9cz7EpC3Wn5vUTwfkPlQ7PiLS+
3SYXEII6Qt4qjWS2xgiJNalUBmW6Ee8A3DXam4q2xyB6FjqjHhs4PRX2qYCgvt5P
AAnC20byELIV2NKOC6FBvcpsTbrABUpmUBr+oqz2NGMMKJcOn3mGCyRNB0WuyhfA
RoRWbfNYLgd6Jr4pRRt15leVVMi0Yu6nSwrIEebwfYoKD5en4zNh5XcK6C92FDm5
MuEXiAENBRFcNQKCAQEA2SKWeZb81y0qsCLcmo6oq+hT0TPLIV+cF4oTnTz8YkK1
e6zHO7SPYZUCS45vD2yb/GFWqCaPTCrmKq43IpcO7aThJNQHjlhFC0nfaIrfW0tu
SVo5D2/KDHyI98rHLHMAOoM4QIcOa3sB6JIkuFBtJHah2mHpI+hSeToBLzPARN9H
+GZWqbVlk5Bkw2eBIpwE5WY3twr6ireutxtIGw5AqM+Xe1cEuCscaCG+bdchp7eW
Nbch7cC4Nys9ae6ikqXyOyC534SfY7TGD8d7ptt57Km61iU0vB67qmAJkIpgF9iy
xVXtS36SKNzJl8fONFsyJE7PP7GCscu7gikqswmgZQKCAQEAxidole7fln4y9aIn
VLovsZ1KiJP1lENZ/0JFhUTXQvXfnfVCgPNA1V+syJ0BEhkehNHJniW76ljUa4Ol
rrFE0+RRP8bSSwI7I6YQNFRGWWpTe583KL54TFxY+2D4/oqO+5Iomde8g90I0QQv
tTYduZpqESvxUu4GTwAGB8c6NbXECdn3MJVcj/kiMOB9/eY67nqWlqIIZxsxnZX0
l1mPPf3pFTr2P+Rmz2nZtI8aXg/4E2JKDkbwrUqjmTUYJPXaVL48ukSClFf2Gi0G
irsRC8/LXs5ZXfviSuHbY4mUkdUz7/g38gfA5oyEUeatn73gQC2USymu/a0Y4TEz
uzPXxQKCAQEAuwt/glhdj+K5nic5z+5KQGQPJ+ys9B/Pb7ui79VkOCTs/w+0RKti
xBW/d2TIKQpPSNZ09r7YvC1MPsH6ftKPqolY5Qe9RpKlT3cge4b9p0BQTeHpu3F6
JM12k7ZbYt/h94WoBHYTJuU8nKKf/SJTEpEbFes4EZWEzib4dDfpTarl7YYC64h2
aup80pMr+6tY0GAAaK6NaseWOufGcoARlRnWjQpF53xDqTRAPZCPzlFolPcfxBY6
2lNmQQWviBJpmyOy2mf9gb9sypT14KO54PPJHcXJKrByyu6V7qw04PXr8e/2TQ1I
TOj8w8H70MAqbnpxL5XzVsOA0Dw0KyyEvQKCAQAdWRD7ZYswuaS5dqZi/vJQqFP5
/L1U6KCfrzE5UKtXkx5KfcfBhUPpxxRZSuFsknYCcPVGKFJb8dlrp0fec7Sd5sFA
vOI2rcgqGUie1oIIAJvUTQg+udvdagSSnUz35wdL3aE1iDqKf92caWLvThTgXC3f
4HyxzWBY13vHXqCNAL2YomuusmhVhB7SyHtkwalZjpx+Ao8Ijx7O+2gMoAL7YHOA
fhiqNcf4CsKFzyJKPBge87uy3GP2T/k3x0j0OGKkYZIDL3kzeUL9pCSYvg3c4Cqq
afpg9Z/FtQaoHrRfOYNYVIhsxgn6ePzGbw3rLiQ9ERAtv424t4lg4r0iQjBb
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
MIIF1jCCA76gAwIBAgICEAMwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux
EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL
DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X
DTIyMDYwNjE2NTYwMVoXDTIzMDYxNjE2NTYwMVowejELMAkGA1UEBhMCU0UxEjAQ
BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN
eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExFDASBgNVBAMMC29j
c3AuY2xpZW50MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvte8rjsO
QmvDS/5Nu+HVwiphq1TmmMnkEXA2J8xar3WUbyuehW4JaWg1c2FBTPG9egGmrZ3Y
7x62ZfBCUap2IgyVZjC9F74EMOSTU9Xd2QSsU9QlzAwq6RvHfUQj2mUgEibyT4Mt
aqwiIXC7zALXNPr2JYD/08/5ORysiUUt5Yi+vrgfMYntCJ84ZGkoaP4Y/TUf4uuV
KJlrZCcQ48g8h1z9mOlxznhm26esn/S0xyuThFEUI2xWySannxSCTqVt/+IDEeVT
Iw4ZVLCTnGsoV1weKRKfrI32krnMYrSxZPoAp9ISSGio8eWmtn29O8Z/N87DEFKd
WVZCTdq1wb0RlfP6SMAYltFw01hj5UKgj9IqzrndiK7s9svs4HmyVS3TbfwJOLy8
GTZOsNxFLbO//yVJbrXHh6YblKHa5YN4/DTiN9yigllAk/Sh3drrnBr7wn7zRD92
0GUZPd/MI/e6My4CMcvVBAFFaTH4iu/hpYLMSH4MO7eGNIAvpLuVMNgrDTZ+JbUn
XaVbIU8CtrT+MvBFrj1eexGNWtoOA22NxBvXVrJtDV5kBO9mwhCKMnth1wxinCrO
XE8wS1YslykIWTEiKZFPSeOhivIojZNSKOrMMpKnh8nglHiuBN2RBottFzuMLAze
LIOmkhCKBVbq4wNKZxzgdfQsk9h8FSZXgOkCAwEAAaN1MHMwCQYDVR0TBAIwADAd
BgNVHQ4EFgQUMKfAZbTdlLNdtBwAz5GR633YxkwwHwYDVR0jBBgwFoAU116Rt859
O5T1nYZ8LHS6gMxppPkwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsG
AQUFBwMJMA0GCSqGSIb3DQEBCwUAA4ICAQA/w+fCPxmZTIH6DUmFJeNR1sFGE0yi
oaSjOxHsKhOdOnDj9UQekXEYX9qDbooWgElyMS9V65FKg2CkM3c/DD4vqK5yBiy/
8iK21XKBV2ZeOQJdfFsZlNOgugc9FNsQhYZJfSsEdZhHyhANLj7hEeNyc1CqosFm
inmTPCwyGNYYN3DsGcSb8D4pM3CmMPxnJnZ1EI5jY11Qe4rURSax8KO3cyz5fSTq
LCNqpby1PAK5q5GRKGPN3mqKkPym0BXX7ASB8eNCYrkgqGxkHc1riE1rJ9G9xAau
6kFgS7lOuEsb12aZ95NT4tdPEXU5+3ute8Zq0PoLn2ejOCPiE0whqzF53QcdRKdO
rZ7pPzFpF6Djo+8Q8Q2kYr16KEJ2VQYLKAl5zWW5J8qBUJdG53svp/gt8SqLcvJB
HVNBIcVc/PkRF1yUgJSukVxA30bxv3AqSlATUu0f2cQAQaOftn7dL0g+20pnbJ3f
i5D9xyNozSuuCIBeutaG2vTh8LXJbauH4xC5eF606oNITuKjituHnUP2h32Q3lwN
LiOozmVfHmCz4IdvcW9sbsJu/anwMrnl4JYkBgMfTxAO9juNJOR93SrR4CUoDMaU
Xgv43+dH5Xjap/Rw14t9WfHNVI5yCwAc9f95bo98XKng0W71sKLLg2ZTPgNv38p3
mGlYiXqsw2ImTw==
-----END CERTIFICATE-----

View File

@ -0,0 +1,4 @@
V 230616165600Z 1000 unknown /C=SE/ST=Stockholm/L=Stockholm/O=MyOrgName/OU=MyIntermediateCA/CN=localhost
V 230616165600Z 1001 unknown /C=SE/ST=Stockholm/L=Stockholm/O=MyOrgName/OU=MyIntermediateCA/CN=MyClient
V 230616165601Z 1002 unknown /C=SE/ST=Stockholm/L=Stockholm/O=MyOrgName/OU=MyIntermediateCA/CN=ocsp.server
V 230616165601Z 1003 unknown /C=SE/ST=Stockholm/L=Stockholm/O=MyOrgName/OU=MyIntermediateCA/CN=ocsp.client

View File

@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
MIIF+zCCA+OgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwbzELMAkGA1UEBhMCU0Ux
EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQK
DAlNeU9yZ05hbWUxETAPBgNVBAsMCE15Um9vdENBMREwDwYDVQQDDAhNeVJvb3RD
QTAeFw0yMjA2MDYxNjU1NTlaFw0zMjA2MDMxNjU1NTlaMGsxCzAJBgNVBAYTAlNF
MRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAoMCU15T3JnTmFtZTEZMBcGA1UE
CwwQTXlJbnRlcm1lZGlhdGVDQTEZMBcGA1UEAwwQTXlJbnRlcm1lZGlhdGVDQTCC
AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANbHOqvXq/7kNmXX6DW4O6EW
/2PBdj/1oPwdIOYo6NfmieCa0BoQ/+W1k+Cj6OR75m0c6JmzPLfUckMvduuW/tJ9
aDz/wFFbSYv/bA9f99mNXI0cebAZvYOI+cRBF4nyag3miRbMcCiOPMMDq936F3hd
Mi6CL+pIFHif74Q66Ncx10M/nL+E9sEhVyTLrAF0FsMmjkSPdzChb+HByKWRD4f2
OYiJxhvJNjLf/+qtn7+A5cwqqbW8zYi6jJPWun24zd2HWHLfehVW8CYzGxBJR7kw
KRnPXEuXhnkgEkgfqj2122Jyd35UC/VUpJW1uJJgFNa2w7+bDj7PS6i4kdqjcEHF
GqUELN8BUIwyicvJ25KnGt0J5JnHy0rDfJeVv9/6EB1Ekm/tKwtJsKLqLThH5Kql
nFM0PUx6OV3nYzXvmmU44SvfIDPy3rhhpVCyaBBzdPyt0ugDkKuwwWcuo6KWSxR1
lGTs2YjIYPwnRm8OqvynqCsn0vWUFOluR7suKlUmJFdPpWw04x+v1GZ3YRAig/GL
6qtByXDgZ27F4QwNPR5nGMmiu7M2QOE/nJS65A3mjrKeYSD+reQ/WbIWtLuHIhn0
XDfHIsuzG6yK9Up1Ske+S1Uos8hTGzvF5NhJlqexd5VJ+VKYLtLkEa/tOj4p/Z5S
3nH+tEqg15nE54ovtDMdAgMBAAGjgaQwgaEwHQYDVR0OBBYEFNdekbfOfTuU9Z2G
fCx0uoDMaaT5MB8GA1UdIwQYMBaAFJf5t2YMW5pYapGnvfGXaTDV4tQKMBIGA1Ud
EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMDsGA1UdHwQ0MDIwMKAuoCyG
Kmh0dHA6Ly9sb2NhbGhvc3Q6OTg3OC9pbnRlcm1lZGlhdGUuY3JsLnBlbTANBgkq
hkiG9w0BAQsFAAOCAgEAn6Mw7+BBqzsEEtSDrd97v8niqJdVXPgxyIeHJvdZm1iF
3eRmxAUXy1DvD8tCqI8vJoz6p/4ELjedxc/Pcum0OravAElO6+jBOplwZCyhmpl8
D4wgh/tbKnOWL20sX6sQESq/dORPf/oPDm9CBu55LaYnNTQHCNtYBZ7UrBhlsdYA
CDtsthnYcC4bYTjmm5fYlg/r3LeVN5HINYFUkh3ffKxIpqWdYOhQShOHv2k5hrzM
Lg8qu098q3kqJ+2thauXuEpD2HME0HelXaqWokwv9ETtJpuA6xQshJrT+2DjF3CY
UY3qdwjWwLr9xjf9DyHmC4hdsaO2ZAqKfMoVi6pcJ4F/20vsTM8063SFxKCbb9aw
hpzLWHB2iiKdRs/bczrW7f6XUbCFF2Et6Wl271Q7cyf8GzIdepvGsRGlcl6UaZ40
wGfpQTgrCZ/kWHNaii4BAXGc9oj8f4NeKRVybV+cCwboed0cvNqnnKBPuz6iQz9c
JzVc7SSgOoAPJ5oh8QYi0vhfVYo34C+ZC+NderWGtMrcQrUzYH1vAv1MQ7Sfxnso
VWO7tCIjeBHcZZ+F7t2d4dJEaGYO9eYjIKSq2brFEQ+4Ix124ZWSbluCzQIXuaR3
49qRH9Ont5UW4mGBAAgSAbyQxPkOZ3IH9vIFa61jyUE4RKGxkOAwHgajKPpmG9w=
-----END CERTIFICATE-----

View File

@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
MIIF1jCCA76gAwIBAgICEAIwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux
EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL
DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X
DTIyMDYwNjE2NTYwMVoXDTIzMDYxNjE2NTYwMVowejELMAkGA1UEBhMCU0UxEjAQ
BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN
eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExFDASBgNVBAMMC29j
c3Auc2VydmVyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvte8rjsO
QmvDS/5Nu+HVwiphq1TmmMnkEXA2J8xar3WUbyuehW4JaWg1c2FBTPG9egGmrZ3Y
7x62ZfBCUap2IgyVZjC9F74EMOSTU9Xd2QSsU9QlzAwq6RvHfUQj2mUgEibyT4Mt
aqwiIXC7zALXNPr2JYD/08/5ORysiUUt5Yi+vrgfMYntCJ84ZGkoaP4Y/TUf4uuV
KJlrZCcQ48g8h1z9mOlxznhm26esn/S0xyuThFEUI2xWySannxSCTqVt/+IDEeVT
Iw4ZVLCTnGsoV1weKRKfrI32krnMYrSxZPoAp9ISSGio8eWmtn29O8Z/N87DEFKd
WVZCTdq1wb0RlfP6SMAYltFw01hj5UKgj9IqzrndiK7s9svs4HmyVS3TbfwJOLy8
GTZOsNxFLbO//yVJbrXHh6YblKHa5YN4/DTiN9yigllAk/Sh3drrnBr7wn7zRD92
0GUZPd/MI/e6My4CMcvVBAFFaTH4iu/hpYLMSH4MO7eGNIAvpLuVMNgrDTZ+JbUn
XaVbIU8CtrT+MvBFrj1eexGNWtoOA22NxBvXVrJtDV5kBO9mwhCKMnth1wxinCrO
XE8wS1YslykIWTEiKZFPSeOhivIojZNSKOrMMpKnh8nglHiuBN2RBottFzuMLAze
LIOmkhCKBVbq4wNKZxzgdfQsk9h8FSZXgOkCAwEAAaN1MHMwCQYDVR0TBAIwADAd
BgNVHQ4EFgQUMKfAZbTdlLNdtBwAz5GR633YxkwwHwYDVR0jBBgwFoAU116Rt859
O5T1nYZ8LHS6gMxppPkwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsG
AQUFBwMJMA0GCSqGSIb3DQEBCwUAA4ICAQCT7/RmzBl1TAQIvSFlnmqVd9Ygh1MD
kKcLA9akepntTvZ4nlmHZergx8D2CDovT3wKonvmp8pBt83sXDBjZxejET7TXX8j
NlvlwZk99/ikWBI0ZA8udl+V15HOVQKF571eeSgRgrsy19YiqLwoK5hZdiYpBLl7
zy9EO9X+zggzerAGBDuf87BxAbYChSjVC4PBgsWfGgD0SMehGvVDq90u5FYr8Fd7
karVIQYZM3+z/BPqjyX9Ac6bfNFHrxj3cXifu4cLS4Ly0wWNIuNo9t733wfcP6ff
PYaibEWsQ7iLH8E5Q/cJ4Pwrwqpxh7PuJlHWe8ObmR2I5YiHIi6dWHwKxveAw3ks
Ep56kdEk69jBgT51VzvLhIunSMpf8glPWIL1GGyRyFvDGWeHFPq2zLuEivKlYQpY
Nm3xn6yTiO2d6qTgDMTB+nO0mM2dpk8yFepsC4Jsgw6iHh5ofuyXS2GleucxPiii
nPvNKFOFPXYXqqScwisnQ2GJjzmLap3SWMRxHt72IyecAQ+e6Bb5WuGM2DOu16in
Ds2oXiE3zOPCOyKwA/IOawNBPQvqmNPdDBYVEdYcQelw9KfiRtyyRppl+EIYM4ay
ekIKHwN3efb45+TnVAY2ea936vfjyb5C/frpnP8voa6aEkgzOSIxAuLMWQg20Y94
jl8CEkTUF4vfqg==
-----END CERTIFICATE-----