emqx_crl_cache_SUITE.erl 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2022-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
  3. %%--------------------------------------------------------------------
  4. -module(emqx_crl_cache_SUITE).
  5. -compile(export_all).
  6. -compile(nowarn_export_all).
  7. -include_lib("stdlib/include/assert.hrl").
  8. -include_lib("emqx/include/asserts.hrl").
  9. -include_lib("common_test/include/ct.hrl").
  10. -include_lib("snabbkaffe/include/snabbkaffe.hrl").
  11. %% from ssl_manager.erl
  12. -record(state, {
  13. session_cache_client,
  14. session_cache_client_cb,
  15. session_lifetime,
  16. certificate_db,
  17. session_validation_timer,
  18. session_cache_client_max,
  19. session_client_invalidator,
  20. options,
  21. client_session_order
  22. }).
  23. -define(DEFAULT_URL, "http://localhost:9878/intermediate.crl.pem").
  24. %%--------------------------------------------------------------------
  25. %% CT boilerplate
  26. %%--------------------------------------------------------------------
  27. all() ->
  28. emqx_common_test_helpers:all(?MODULE).
  29. init_per_suite(Config) ->
  30. Config.
  31. end_per_suite(_Config) ->
  32. ok.
  33. init_per_testcase(TestCase, Config) when
  34. TestCase =:= t_cache;
  35. TestCase =:= t_filled_cache;
  36. TestCase =:= t_revoked
  37. ->
  38. ct:timetrap({seconds, 30}),
  39. ok = snabbkaffe:start_trace(),
  40. DataDir = ?config(data_dir, Config),
  41. {CRLPem, CRLDer} = read_crl(filename:join(DataDir, "intermediate-revoked.crl.pem")),
  42. ServerPid = start_crl_server(CRLPem),
  43. IsCached = lists:member(TestCase, [t_filled_cache, t_revoked]),
  44. Apps = start_emqx_with_crl_cache(#{is_cached => IsCached}, TestCase, Config),
  45. [
  46. {crl_pem, CRLPem},
  47. {crl_der, CRLDer},
  48. {http_server, ServerPid},
  49. {tc_apps, Apps}
  50. | Config
  51. ];
  52. init_per_testcase(t_revoke_then_refresh = TestCase, Config) ->
  53. ct:timetrap({seconds, 120}),
  54. ok = snabbkaffe:start_trace(),
  55. DataDir = ?config(data_dir, Config),
  56. {CRLPemNotRevoked, CRLDerNotRevoked} =
  57. read_crl(filename:join(DataDir, "intermediate-not-revoked.crl.pem")),
  58. {CRLPemRevoked, CRLDerRevoked} =
  59. read_crl(filename:join(DataDir, "intermediate-revoked.crl.pem")),
  60. ServerPid = start_crl_server(CRLPemNotRevoked),
  61. Apps = start_emqx_with_crl_cache(
  62. #{is_cached => true, overrides => #{crl_cache => #{refresh_interval => <<"10s">>}}},
  63. TestCase,
  64. Config
  65. ),
  66. [
  67. {crl_pem_not_revoked, CRLPemNotRevoked},
  68. {crl_der_not_revoked, CRLDerNotRevoked},
  69. {crl_pem_revoked, CRLPemRevoked},
  70. {crl_der_revoked, CRLDerRevoked},
  71. {http_server, ServerPid},
  72. {tc_apps, Apps}
  73. | Config
  74. ];
  75. init_per_testcase(t_cache_overflow = TestCase, Config) ->
  76. ct:timetrap({seconds, 120}),
  77. ok = snabbkaffe:start_trace(),
  78. DataDir = ?config(data_dir, Config),
  79. {CRLPemRevoked, _} = read_crl(filename:join(DataDir, "intermediate-revoked.crl.pem")),
  80. ServerPid = start_crl_server(CRLPemRevoked),
  81. Apps = start_emqx_with_crl_cache(
  82. #{is_cached => false, overrides => #{crl_cache => #{capacity => 2}}},
  83. TestCase,
  84. Config
  85. ),
  86. [
  87. {http_server, ServerPid},
  88. {tc_apps, Apps}
  89. | Config
  90. ];
  91. init_per_testcase(TestCase, Config) when
  92. TestCase =:= t_not_cached_and_unreachable;
  93. TestCase =:= t_update_config
  94. ->
  95. ct:timetrap({seconds, 30}),
  96. ok = snabbkaffe:start_trace(),
  97. DataDir = ?config(data_dir, Config),
  98. {CRLPem, CRLDer} = read_crl(filename:join(DataDir, "intermediate-revoked.crl.pem")),
  99. Apps = start_emqx_with_crl_cache(#{is_cached => false}, TestCase, Config),
  100. [
  101. {crl_pem, CRLPem},
  102. {crl_der, CRLDer},
  103. {tc_apps, Apps}
  104. | Config
  105. ];
  106. init_per_testcase(t_refresh_config = TestCase, Config) ->
  107. ct:timetrap({seconds, 30}),
  108. ok = snabbkaffe:start_trace(),
  109. DataDir = ?config(data_dir, Config),
  110. {CRLPem, CRLDer} = read_crl(filename:join(DataDir, "intermediate-revoked.crl.pem")),
  111. TestPid = self(),
  112. ok = meck:new(emqx_crl_cache, [non_strict, passthrough, no_history, no_link]),
  113. meck:expect(
  114. emqx_crl_cache,
  115. http_get,
  116. fun(URL, _HTTPTimeout) ->
  117. ct:pal("http get crl ~p", [URL]),
  118. TestPid ! {http_get, URL},
  119. {ok, {{"HTTP/1.0", 200, "OK"}, [], CRLPem}}
  120. end
  121. ),
  122. Apps = start_emqx_with_crl_cache(#{is_cached => false}, TestCase, Config),
  123. [
  124. {crl_pem, CRLPem},
  125. {crl_der, CRLDer},
  126. {tc_apps, Apps}
  127. | Config
  128. ];
  129. init_per_testcase(TestCase, Config) when
  130. TestCase =:= t_update_listener;
  131. TestCase =:= t_validations
  132. ->
  133. ct:timetrap({seconds, 30}),
  134. ok = snabbkaffe:start_trace(),
  135. %% when running emqx standalone tests, we can't use those
  136. %% features.
  137. case does_module_exist(emqx_management) of
  138. true ->
  139. DataDir = ?config(data_dir, Config),
  140. CRLFile = filename:join([DataDir, "intermediate-revoked.crl.pem"]),
  141. {ok, CRLPem} = file:read_file(CRLFile),
  142. ServerPid = start_crl_server(CRLPem),
  143. ListenerConf = #{
  144. enable => true,
  145. ssl_options => #{
  146. keyfile => filename:join(DataDir, "server.key.pem"),
  147. certfile => filename:join(DataDir, "server.cert.pem"),
  148. cacertfile => filename:join(DataDir, "ca-chain.cert.pem"),
  149. verify => verify_peer,
  150. enable_crl_check => false
  151. }
  152. },
  153. Apps = emqx_cth_suite:start(
  154. [
  155. {emqx_conf, #{config => #{listeners => #{ssl => #{default => ListenerConf}}}}},
  156. emqx,
  157. emqx_management,
  158. {emqx_dashboard, "dashboard.listeners.http { enable = true, bind = 18083 }"}
  159. ],
  160. #{work_dir => emqx_cth_suite:work_dir(TestCase, Config)}
  161. ),
  162. [
  163. {http_server, ServerPid},
  164. {tc_apps, Apps}
  165. | Config
  166. ];
  167. false ->
  168. [{skip_does_not_apply, true} | Config]
  169. end;
  170. init_per_testcase(_TestCase, Config) ->
  171. ct:timetrap({seconds, 30}),
  172. ok = snabbkaffe:start_trace(),
  173. DataDir = ?config(data_dir, Config),
  174. {CRLPem, CRLDer} = read_crl(filename:join(DataDir, "intermediate-revoked.crl.pem")),
  175. TestPid = self(),
  176. ok = meck:new(emqx_crl_cache, [non_strict, passthrough, no_history, no_link]),
  177. meck:expect(
  178. emqx_crl_cache,
  179. http_get,
  180. fun(URL, _HTTPTimeout) ->
  181. ct:pal("http get crl ~p", [URL]),
  182. TestPid ! {http_get, URL},
  183. {ok, {{"HTTP/1.0", 200, 'OK'}, [], CRLPem}}
  184. end
  185. ),
  186. [
  187. {crl_pem, CRLPem},
  188. {crl_der, CRLDer}
  189. | Config
  190. ].
  191. read_crl(Filename) ->
  192. {ok, PEM} = file:read_file(Filename),
  193. [{'CertificateList', DER, not_encrypted}] = public_key:pem_decode(PEM),
  194. {PEM, DER}.
  195. end_per_testcase(TestCase, Config) when
  196. TestCase =:= t_update_listener;
  197. TestCase =:= t_validations
  198. ->
  199. Skip = proplists:get_bool(skip_does_not_apply, Config),
  200. case Skip of
  201. true ->
  202. ok;
  203. false ->
  204. end_per_testcase(common, Config)
  205. end;
  206. end_per_testcase(_TestCase, Config) ->
  207. ok = snabbkaffe:stop(),
  208. clear_crl_cache(),
  209. _ = emqx_maybe:apply(
  210. fun emqx_crl_cache_http_server:stop/1,
  211. proplists:get_value(http_server, Config)
  212. ),
  213. _ = emqx_maybe:apply(
  214. fun emqx_cth_suite:stop/1,
  215. proplists:get_value(tc_apps, Config)
  216. ),
  217. catch meck:unload([emqx_crl_cache]),
  218. ok.
  219. %%--------------------------------------------------------------------
  220. %% Helper functions
  221. %%--------------------------------------------------------------------
  222. does_module_exist(Mod) ->
  223. case erlang:module_loaded(Mod) of
  224. true ->
  225. true;
  226. false ->
  227. case code:ensure_loaded(Mod) of
  228. ok ->
  229. true;
  230. {module, Mod} ->
  231. true;
  232. _ ->
  233. false
  234. end
  235. end.
  236. assert_http_get(URL) ->
  237. receive
  238. {http_get, URL} ->
  239. ok
  240. after 1000 ->
  241. ct:pal("mailbox: ~p", [process_info(self(), messages)]),
  242. error({should_have_requested, URL})
  243. end.
  244. get_crl_cache_table() ->
  245. #state{certificate_db = [_, _, _, {Ref, _}]} = sys:get_state(ssl_manager),
  246. Ref.
  247. start_crl_server(Port, CRLPem) ->
  248. {ok, LSock} = gen_tcp:listen(Port, [binary, {active, true}, reusedaddr]),
  249. spawn_link(fun() -> accept_loop(LSock, CRLPem) end),
  250. ok.
  251. accept_loop(LSock, CRLPem) ->
  252. case gen_tcp:accept(LSock) of
  253. {ok, Sock} ->
  254. Worker = spawn_link(fun() -> crl_loop(Sock, CRLPem) end),
  255. gen_tcp:controlling_process(Sock, Worker),
  256. accept_loop(LSock, CRLPem);
  257. {error, Reason} ->
  258. error({accept_error, Reason})
  259. end.
  260. crl_loop(Sock, CRLPem) ->
  261. receive
  262. {tcp, Sock, _Data} ->
  263. gen_tcp:send(Sock, CRLPem),
  264. crl_loop(Sock, CRLPem);
  265. _Msg ->
  266. ok
  267. end.
  268. drain_msgs() ->
  269. receive
  270. _Msg ->
  271. drain_msgs()
  272. after 0 ->
  273. ok
  274. end.
  275. clear_crl_cache() ->
  276. %% reset the CRL cache
  277. Ref = monitor(process, whereis(ssl_manager)),
  278. exit(whereis(ssl_manager), kill),
  279. receive
  280. {'DOWN', Ref, process, _, _} ->
  281. ok
  282. after 1_000 ->
  283. ct:fail("ssl_manager didn't die")
  284. end,
  285. ensure_ssl_manager_alive(),
  286. ok.
  287. start_emqx_with_crl_cache(#{is_cached := IsCached} = Opts, TC, Config) ->
  288. DataDir = ?config(data_dir, Config),
  289. Overrides = maps:get(overrides, Opts, #{}),
  290. ListenerConf = #{
  291. enable => true,
  292. ssl_options => #{
  293. keyfile => filename:join(DataDir, "server.key.pem"),
  294. certfile => filename:join(DataDir, "server.cert.pem"),
  295. cacertfile => filename:join(DataDir, "ca-chain.cert.pem"),
  296. verify => verify_peer,
  297. enable_crl_check => true
  298. }
  299. },
  300. Conf = #{
  301. listeners => #{ssl => #{default => ListenerConf}},
  302. crl_cache => #{
  303. refresh_interval => <<"11m">>,
  304. http_timeout => <<"17s">>,
  305. capacity => 100
  306. }
  307. },
  308. Apps = emqx_cth_suite:start(
  309. [{emqx, #{config => emqx_utils_maps:deep_merge(Conf, Overrides)}}],
  310. #{work_dir => emqx_cth_suite:work_dir(TC, Config)}
  311. ),
  312. case IsCached of
  313. true ->
  314. %% wait the cache to be filled
  315. emqx_crl_cache:refresh(?DEFAULT_URL),
  316. ?assertReceive({http_get, <<?DEFAULT_URL>>});
  317. false ->
  318. %% ensure cache is empty
  319. ok
  320. end,
  321. Apps.
  322. start_crl_server(CRLPem) ->
  323. application:ensure_all_started(cowboy),
  324. {ok, ServerPid} = emqx_crl_cache_http_server:start_link(self(), 9878, CRLPem, []),
  325. receive
  326. {ServerPid, ready} -> ok
  327. after 1000 -> error(timeout_starting_http_server)
  328. end,
  329. ServerPid.
  330. request(Method, Url, QueryParams, Body) ->
  331. AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
  332. Opts = #{return_all => true},
  333. case emqx_mgmt_api_test_util:request_api(Method, Url, QueryParams, AuthHeader, Body, Opts) of
  334. {ok, {Reason, Headers, BodyR}} ->
  335. {ok, {Reason, Headers, emqx_utils_json:decode(BodyR, [return_maps])}};
  336. Error ->
  337. Error
  338. end.
  339. get_listener_via_api(ListenerId) ->
  340. Path = emqx_mgmt_api_test_util:api_path(["listeners", ListenerId]),
  341. request(get, Path, [], []).
  342. update_listener_via_api(ListenerId, NewConfig) ->
  343. Path = emqx_mgmt_api_test_util:api_path(["listeners", ListenerId]),
  344. request(put, Path, [], NewConfig).
  345. assert_successful_connection(Config) ->
  346. assert_successful_connection(Config, default).
  347. assert_successful_connection(Config, ClientNum) ->
  348. DataDir = ?config(data_dir, Config),
  349. Num =
  350. case ClientNum of
  351. default -> "";
  352. _ -> integer_to_list(ClientNum)
  353. end,
  354. ClientCert = filename:join(DataDir, "client" ++ Num ++ ".cert.pem"),
  355. ClientKey = filename:join(DataDir, "client" ++ Num ++ ".key.pem"),
  356. %% 1) At first, the cache is empty, and the CRL is fetched and
  357. %% cached on the fly.
  358. {ok, C0} = emqtt:start_link([
  359. {ssl, true},
  360. {ssl_opts, [
  361. {certfile, ClientCert},
  362. {keyfile, ClientKey},
  363. {verify, verify_none}
  364. ]},
  365. {port, 8883}
  366. ]),
  367. ?tp_span(
  368. mqtt_client_connection,
  369. #{client_num => ClientNum},
  370. begin
  371. {ok, _} = emqtt:connect(C0),
  372. emqtt:stop(C0),
  373. ok
  374. end
  375. ).
  376. trace_between(Trace0, Marker1, Marker2) ->
  377. {Trace1, [_ | _]} = ?split_trace_at(#{?snk_kind := Marker2}, Trace0),
  378. {[_ | _], [_ | Trace2]} = ?split_trace_at(#{?snk_kind := Marker1}, Trace1),
  379. Trace2.
  380. of_kinds(Trace0, Kinds0) ->
  381. Kinds = sets:from_list(Kinds0, [{version, 2}]),
  382. lists:filter(
  383. fun(#{?snk_kind := K}) -> sets:is_element(K, Kinds) end,
  384. Trace0
  385. ).
  386. ensure_ssl_manager_alive() ->
  387. ?retry(
  388. _Sleep0 = 200,
  389. _Attempts0 = 50,
  390. true = is_pid(whereis(ssl_manager))
  391. ).
  392. %%--------------------------------------------------------------------
  393. %% Test cases
  394. %%--------------------------------------------------------------------
  395. t_init_empty_urls(_Config) ->
  396. Ref = get_crl_cache_table(),
  397. ?assertEqual([], ets:tab2list(Ref)),
  398. emqx_config_handler:start_link(),
  399. ?assertMatch({ok, _}, emqx_crl_cache:start_link()),
  400. receive
  401. {http_get, _} ->
  402. error(should_not_make_http_request)
  403. after 1000 -> ok
  404. end,
  405. ?assertEqual([], ets:tab2list(Ref)),
  406. emqx_config_handler:stop(),
  407. ok.
  408. t_update_config(_Config) ->
  409. Conf = #{
  410. <<"refresh_interval">> => <<"5m">>,
  411. <<"http_timeout">> => <<"10m">>,
  412. <<"capacity">> => 123
  413. },
  414. ?assertMatch({ok, _}, emqx:update_config([<<"crl_cache">>], Conf)),
  415. State = emqx_crl_cache:info(),
  416. ?assertEqual(
  417. #{
  418. refresh_interval => timer:minutes(5),
  419. http_timeout => timer:minutes(10),
  420. cache_capacity => 123
  421. },
  422. maps:with([refresh_interval, http_timeout, cache_capacity], State)
  423. ).
  424. t_manual_refresh(Config) ->
  425. CRLDer = ?config(crl_der, Config),
  426. Ref = get_crl_cache_table(),
  427. ?assertEqual([], ets:tab2list(Ref)),
  428. emqx_config_handler:start_link(),
  429. {ok, _} = emqx_crl_cache:start_link(),
  430. URL = "http://localhost/crl.pem",
  431. ok = snabbkaffe:start_trace(),
  432. ?wait_async_action(
  433. ?assertEqual(ok, emqx_crl_cache:refresh(URL)),
  434. #{?snk_kind := crl_cache_insert},
  435. 5_000
  436. ),
  437. ok = snabbkaffe:stop(),
  438. ?assertEqual(
  439. [{"crl.pem", [CRLDer]}],
  440. ets:tab2list(Ref)
  441. ),
  442. emqx_config_handler:stop(),
  443. ok.
  444. t_refresh_request_error(_Config) ->
  445. meck:expect(
  446. emqx_crl_cache,
  447. http_get,
  448. fun(_URL, _HTTPTimeout) ->
  449. {ok, {{"HTTP/1.0", 404, 'Not Found'}, [], <<"not found">>}}
  450. end
  451. ),
  452. emqx_config_handler:start_link(),
  453. {ok, _} = emqx_crl_cache:start_link(),
  454. URL = "http://localhost/crl.pem",
  455. ?check_trace(
  456. ?wait_async_action(
  457. ?assertEqual(ok, emqx_crl_cache:refresh(URL)),
  458. #{?snk_kind := crl_cache_insert},
  459. 5_000
  460. ),
  461. fun(Trace) ->
  462. ?assertMatch(
  463. [#{error := {bad_response, #{code := 404}}}],
  464. ?of_kind(crl_refresh_failure, Trace)
  465. ),
  466. ok
  467. end
  468. ),
  469. ok = snabbkaffe:stop(),
  470. emqx_config_handler:stop(),
  471. ok.
  472. t_refresh_invalid_response(_Config) ->
  473. meck:expect(
  474. emqx_crl_cache,
  475. http_get,
  476. fun(_URL, _HTTPTimeout) ->
  477. {ok, {{"HTTP/1.0", 200, 'OK'}, [], <<"not a crl">>}}
  478. end
  479. ),
  480. emqx_config_handler:start_link(),
  481. {ok, _} = emqx_crl_cache:start_link(),
  482. URL = "http://localhost/crl.pem",
  483. ?check_trace(
  484. ?wait_async_action(
  485. ?assertEqual(ok, emqx_crl_cache:refresh(URL)),
  486. #{?snk_kind := crl_cache_insert},
  487. 5_000
  488. ),
  489. fun(Trace) ->
  490. ?assertMatch(
  491. [#{crls := []}],
  492. ?of_kind(crl_cache_insert, Trace)
  493. ),
  494. ok
  495. end
  496. ),
  497. ok = snabbkaffe:stop(),
  498. emqx_config_handler:stop(),
  499. ok.
  500. t_refresh_http_error(_Config) ->
  501. meck:expect(
  502. emqx_crl_cache,
  503. http_get,
  504. fun(_URL, _HTTPTimeout) ->
  505. {error, timeout}
  506. end
  507. ),
  508. emqx_config_handler:start_link(),
  509. {ok, _} = emqx_crl_cache:start_link(),
  510. URL = "http://localhost/crl.pem",
  511. ?check_trace(
  512. ?wait_async_action(
  513. ?assertEqual(ok, emqx_crl_cache:refresh(URL)),
  514. #{?snk_kind := crl_cache_insert},
  515. 5_000
  516. ),
  517. fun(Trace) ->
  518. ?assertMatch(
  519. [#{error := {http_error, timeout}}],
  520. ?of_kind(crl_refresh_failure, Trace)
  521. ),
  522. ok
  523. end
  524. ),
  525. ok = snabbkaffe:stop(),
  526. emqx_config_handler:stop(),
  527. ok.
  528. t_unknown_messages(_Config) ->
  529. emqx_config_handler:start_link(),
  530. {ok, Server} = emqx_crl_cache:start_link(),
  531. gen_server:call(Server, foo),
  532. gen_server:cast(Server, foo),
  533. Server ! foo,
  534. emqx_config_handler:stop(),
  535. ok.
  536. t_evict(_Config) ->
  537. emqx_config_handler:start_link(),
  538. {ok, _} = emqx_crl_cache:start_link(),
  539. URL = "http://localhost/crl.pem",
  540. ?wait_async_action(
  541. ?assertEqual(ok, emqx_crl_cache:refresh(URL)),
  542. #{?snk_kind := crl_cache_insert},
  543. 5_000
  544. ),
  545. Ref = get_crl_cache_table(),
  546. ?assertMatch([{"crl.pem", _}], ets:tab2list(Ref)),
  547. {ok, {ok, _}} = ?wait_async_action(
  548. emqx_crl_cache:evict(URL),
  549. #{?snk_kind := crl_cache_evict}
  550. ),
  551. ?assertEqual([], ets:tab2list(Ref)),
  552. emqx_config_handler:stop(),
  553. ok.
  554. t_cache(Config) ->
  555. DataDir = ?config(data_dir, Config),
  556. ClientCert = filename:join(DataDir, "client.cert.pem"),
  557. ClientKey = filename:join(DataDir, "client.key.pem"),
  558. %% 1) At first, the cache is empty, and the CRL is fetched and
  559. %% cached on the fly.
  560. {ok, C0} = emqtt:start_link([
  561. {ssl, true},
  562. {ssl_opts, [
  563. {certfile, ClientCert},
  564. {keyfile, ClientKey},
  565. {verify, verify_none}
  566. ]},
  567. {port, 8883}
  568. ]),
  569. {ok, _} = emqtt:connect(C0),
  570. receive
  571. {http_get, _} -> ok
  572. after 500 ->
  573. emqtt:stop(C0),
  574. error(should_have_checked_server)
  575. end,
  576. emqtt:stop(C0),
  577. %% 2) When another client using the cached CRL URL connects later,
  578. %% it uses the cache.
  579. {ok, C1} = emqtt:start_link([
  580. {ssl, true},
  581. {ssl_opts, [
  582. {certfile, ClientCert},
  583. {keyfile, ClientKey},
  584. {verify, verify_none}
  585. ]},
  586. {port, 8883}
  587. ]),
  588. {ok, _} = emqtt:connect(C1),
  589. receive
  590. {http_get, _} ->
  591. emqtt:stop(C1),
  592. error(should_not_have_checked_server)
  593. after 500 -> ok
  594. end,
  595. emqtt:stop(C1),
  596. ok.
  597. t_cache_overflow(Config) ->
  598. %% we have capacity = 2 here.
  599. ?check_trace(
  600. begin
  601. %% First and second connections goes into the cache
  602. ?tp(first_connections, #{}),
  603. assert_successful_connection(Config, 1),
  604. assert_successful_connection(Config, 2),
  605. %% These should be cached
  606. ?tp(first_reconnections, #{}),
  607. assert_successful_connection(Config, 1),
  608. assert_successful_connection(Config, 2),
  609. %% A third client connects and evicts the oldest URL (1)
  610. ?tp(first_eviction, #{}),
  611. assert_successful_connection(Config, 3),
  612. assert_successful_connection(Config, 3),
  613. %% URL (1) connects again and needs to be re-cached; this
  614. %% time, (2) gets evicted
  615. ?tp(second_eviction, #{}),
  616. assert_successful_connection(Config, 1),
  617. %% TODO: force race condition where the same URL is fetched
  618. %% at the same time and tries to be registered
  619. ?tp(test_end, #{}),
  620. ok
  621. end,
  622. fun(Trace) ->
  623. URL1 = "http://localhost:9878/intermediate1.crl.pem",
  624. URL2 = "http://localhost:9878/intermediate2.crl.pem",
  625. URL3 = "http://localhost:9878/intermediate3.crl.pem",
  626. Kinds = [
  627. mqtt_client_connection,
  628. new_crl_url_inserted,
  629. crl_cache_ensure_timer,
  630. crl_cache_overflow
  631. ],
  632. Trace1 = of_kinds(
  633. trace_between(Trace, first_connections, first_reconnections),
  634. Kinds
  635. ),
  636. ?assertMatch(
  637. [
  638. #{
  639. ?snk_kind := mqtt_client_connection,
  640. ?snk_span := start,
  641. client_num := 1
  642. },
  643. #{
  644. ?snk_kind := new_crl_url_inserted,
  645. url := URL1
  646. },
  647. #{
  648. ?snk_kind := crl_cache_ensure_timer,
  649. url := URL1
  650. },
  651. #{
  652. ?snk_kind := mqtt_client_connection,
  653. ?snk_span := {complete, ok},
  654. client_num := 1
  655. },
  656. #{
  657. ?snk_kind := mqtt_client_connection,
  658. ?snk_span := start,
  659. client_num := 2
  660. },
  661. #{
  662. ?snk_kind := new_crl_url_inserted,
  663. url := URL2
  664. },
  665. #{
  666. ?snk_kind := crl_cache_ensure_timer,
  667. url := URL2
  668. },
  669. #{
  670. ?snk_kind := mqtt_client_connection,
  671. ?snk_span := {complete, ok},
  672. client_num := 2
  673. }
  674. ],
  675. Trace1
  676. ),
  677. Trace2 = of_kinds(
  678. trace_between(Trace, first_reconnections, first_eviction),
  679. Kinds
  680. ),
  681. ?assertMatch(
  682. [
  683. #{
  684. ?snk_kind := mqtt_client_connection,
  685. ?snk_span := start,
  686. client_num := 1
  687. },
  688. #{
  689. ?snk_kind := mqtt_client_connection,
  690. ?snk_span := {complete, ok},
  691. client_num := 1
  692. },
  693. #{
  694. ?snk_kind := mqtt_client_connection,
  695. ?snk_span := start,
  696. client_num := 2
  697. },
  698. #{
  699. ?snk_kind := mqtt_client_connection,
  700. ?snk_span := {complete, ok},
  701. client_num := 2
  702. }
  703. ],
  704. Trace2
  705. ),
  706. Trace3 = of_kinds(
  707. trace_between(Trace, first_eviction, second_eviction),
  708. Kinds
  709. ),
  710. ?assertMatch(
  711. [
  712. #{
  713. ?snk_kind := mqtt_client_connection,
  714. ?snk_span := start,
  715. client_num := 3
  716. },
  717. #{
  718. ?snk_kind := new_crl_url_inserted,
  719. url := URL3
  720. },
  721. #{
  722. ?snk_kind := crl_cache_overflow,
  723. oldest_url := URL1
  724. },
  725. #{
  726. ?snk_kind := crl_cache_ensure_timer,
  727. url := URL3
  728. },
  729. #{
  730. ?snk_kind := mqtt_client_connection,
  731. ?snk_span := {complete, ok},
  732. client_num := 3
  733. },
  734. #{
  735. ?snk_kind := mqtt_client_connection,
  736. ?snk_span := start,
  737. client_num := 3
  738. },
  739. #{
  740. ?snk_kind := mqtt_client_connection,
  741. ?snk_span := {complete, ok},
  742. client_num := 3
  743. }
  744. ],
  745. Trace3
  746. ),
  747. Trace4 = of_kinds(
  748. trace_between(Trace, second_eviction, test_end),
  749. Kinds
  750. ),
  751. ?assertMatch(
  752. [
  753. #{
  754. ?snk_kind := mqtt_client_connection,
  755. ?snk_span := start,
  756. client_num := 1
  757. },
  758. #{
  759. ?snk_kind := new_crl_url_inserted,
  760. url := URL1
  761. },
  762. #{
  763. ?snk_kind := crl_cache_overflow,
  764. oldest_url := URL2
  765. },
  766. #{
  767. ?snk_kind := crl_cache_ensure_timer,
  768. url := URL1
  769. },
  770. #{
  771. ?snk_kind := mqtt_client_connection,
  772. ?snk_span := {complete, ok},
  773. client_num := 1
  774. }
  775. ],
  776. Trace4
  777. ),
  778. ok
  779. end
  780. ).
  781. %% check that the URL in the certificate is *not* checked if the cache
  782. %% contains that URL.
  783. t_filled_cache(Config) ->
  784. DataDir = ?config(data_dir, Config),
  785. ClientCert = filename:join(DataDir, "client.cert.pem"),
  786. ClientKey = filename:join(DataDir, "client.key.pem"),
  787. {ok, C} = emqtt:start_link([
  788. {ssl, true},
  789. {ssl_opts, [
  790. {certfile, ClientCert},
  791. {keyfile, ClientKey},
  792. {verify, verify_none}
  793. ]},
  794. {port, 8883}
  795. ]),
  796. {ok, _} = emqtt:connect(C),
  797. receive
  798. http_get ->
  799. emqtt:stop(C),
  800. error(should_have_used_cache)
  801. after 500 -> ok
  802. end,
  803. emqtt:stop(C),
  804. ok.
  805. %% If the CRL is not cached when the client tries to connect and the
  806. %% CRL server is unreachable, the client will be denied connection.
  807. t_not_cached_and_unreachable(Config) ->
  808. DataDir = ?config(data_dir, Config),
  809. ClientCert = filename:join(DataDir, "client.cert.pem"),
  810. ClientKey = filename:join(DataDir, "client.key.pem"),
  811. {ok, C} = emqtt:start_link([
  812. {ssl, true},
  813. {ssl_opts, [
  814. {certfile, ClientCert},
  815. {keyfile, ClientKey},
  816. {verify, verify_none}
  817. ]},
  818. {port, 8883}
  819. ]),
  820. Ref = get_crl_cache_table(),
  821. ?assertEqual([], ets:tab2list(Ref)),
  822. unlink(C),
  823. ?assertMatch({error, {ssl_error, _Sock, {tls_alert, {bad_certificate, _}}}}, emqtt:connect(C)),
  824. ok.
  825. t_revoked(Config) ->
  826. DataDir = ?config(data_dir, Config),
  827. ClientCert = filename:join(DataDir, "client-revoked.cert.pem"),
  828. ClientKey = filename:join(DataDir, "client-revoked.key.pem"),
  829. {ok, C} = emqtt:start_link([
  830. {ssl, true},
  831. {ssl_opts, [
  832. {certfile, ClientCert},
  833. {keyfile, ClientKey},
  834. {verify, verify_none}
  835. ]},
  836. {port, 8883}
  837. ]),
  838. unlink(C),
  839. case emqtt:connect(C) of
  840. {error, {ssl_error, _Sock, {tls_alert, {certificate_revoked, _}}}} ->
  841. ok;
  842. {error, closed} ->
  843. %% this happens due to an unidentified race-condition
  844. ok
  845. end.
  846. t_revoke_then_refresh(Config) ->
  847. DataDir = ?config(data_dir, Config),
  848. CRLPemRevoked = ?config(crl_pem_revoked, Config),
  849. ClientCert = filename:join(DataDir, "client-revoked.cert.pem"),
  850. ClientKey = filename:join(DataDir, "client-revoked.key.pem"),
  851. {ok, C0} = emqtt:start_link([
  852. {ssl, true},
  853. {ssl_opts, [
  854. {certfile, ClientCert},
  855. {keyfile, ClientKey},
  856. {verify, verify_none}
  857. ]},
  858. {port, 8883}
  859. ]),
  860. %% At first, the CRL contains no revoked entries, so the client
  861. %% should be allowed connection.
  862. ?assertMatch({ok, _}, emqtt:connect(C0)),
  863. emqtt:stop(C0),
  864. %% Now we update the CRL on the server and wait for the cache to
  865. %% be refreshed.
  866. {true, {ok, _}} =
  867. ?wait_async_action(
  868. emqx_crl_cache_http_server:set_crl(CRLPemRevoked),
  869. #{?snk_kind := crl_refresh_timer_done},
  870. 70_000
  871. ),
  872. %% The *same client* should now be denied connection.
  873. {ok, C1} = emqtt:start_link([
  874. {ssl, true},
  875. {ssl_opts, [
  876. {certfile, ClientCert},
  877. {keyfile, ClientKey},
  878. {verify, verify_none}
  879. ]},
  880. {port, 8883}
  881. ]),
  882. unlink(C1),
  883. ?assertMatch(
  884. {error, {ssl_error, _Sock, {tls_alert, {certificate_revoked, _}}}}, emqtt:connect(C1)
  885. ),
  886. ok.
  887. %% check that we can start with a non-crl listener and restart it with
  888. %% the new crl config.
  889. t_update_listener(Config) ->
  890. case proplists:get_bool(skip_does_not_apply, Config) of
  891. true ->
  892. ok;
  893. false ->
  894. do_t_update_listener(Config)
  895. end.
  896. do_t_update_listener(Config) ->
  897. DataDir = ?config(data_dir, Config),
  898. Keyfile = filename:join([DataDir, "server.key.pem"]),
  899. Certfile = filename:join([DataDir, "server.cert.pem"]),
  900. Cacertfile = filename:join([DataDir, "ca-chain.cert.pem"]),
  901. ClientCert = filename:join(DataDir, "client-revoked.cert.pem"),
  902. ClientKey = filename:join(DataDir, "client-revoked.key.pem"),
  903. %% no crl at first
  904. ListenerId = "ssl:default",
  905. {ok, {{_, 200, _}, _, ListenerData0}} = get_listener_via_api(ListenerId),
  906. ?assertMatch(
  907. #{
  908. <<"ssl_options">> :=
  909. #{
  910. <<"enable_crl_check">> := false,
  911. <<"verify">> := <<"verify_peer">>
  912. }
  913. },
  914. ListenerData0
  915. ),
  916. {ok, C0} = emqtt:start_link([
  917. {ssl, true},
  918. {ssl_opts, [
  919. {certfile, ClientCert},
  920. {keyfile, ClientKey},
  921. {verify, verify_none}
  922. ]},
  923. {port, 8883}
  924. ]),
  925. %% At first, the CRL contains no revoked entries, so the client
  926. %% should be allowed connection.
  927. ?assertMatch({ok, _}, emqtt:connect(C0)),
  928. emqtt:stop(C0),
  929. %% configure crl
  930. CRLConfig =
  931. #{
  932. <<"ssl_options">> =>
  933. #{
  934. <<"keyfile">> => Keyfile,
  935. <<"certfile">> => Certfile,
  936. <<"cacertfile">> => Cacertfile,
  937. <<"enable_crl_check">> => true
  938. }
  939. },
  940. ListenerData1 = emqx_utils_maps:deep_merge(ListenerData0, CRLConfig),
  941. {ok, {_, _, ListenerData2}} = update_listener_via_api(ListenerId, ListenerData1),
  942. ?assertMatch(
  943. #{
  944. <<"ssl_options">> :=
  945. #{
  946. <<"enable_crl_check">> := true,
  947. <<"verify">> := <<"verify_peer">>
  948. }
  949. },
  950. ListenerData2
  951. ),
  952. %% Now should use CRL information to block connection
  953. {ok, C1} = emqtt:start_link([
  954. {ssl, true},
  955. {ssl_opts, [
  956. {certfile, ClientCert},
  957. {keyfile, ClientKey},
  958. {verify, verify_none}
  959. ]},
  960. {port, 8883}
  961. ]),
  962. unlink(C1),
  963. ?assertMatch(
  964. {error, {ssl_error, _Sock, {tls_alert, {certificate_revoked, _}}}}, emqtt:connect(C1)
  965. ),
  966. assert_http_get(<<?DEFAULT_URL>>),
  967. ok.
  968. t_validations(Config) ->
  969. case proplists:get_bool(skip_does_not_apply, Config) of
  970. true ->
  971. ok;
  972. false ->
  973. do_t_validations(Config)
  974. end.
  975. do_t_validations(_Config) ->
  976. ListenerId = <<"ssl:default">>,
  977. {ok, {{_, 200, _}, _, ListenerData0}} = get_listener_via_api(ListenerId),
  978. ListenerData1 =
  979. emqx_utils_maps:deep_merge(
  980. ListenerData0,
  981. #{
  982. <<"ssl_options">> =>
  983. #{
  984. <<"enable_crl_check">> => true,
  985. <<"verify">> => <<"verify_none">>
  986. }
  987. }
  988. ),
  989. {error, {_, _, ResRaw1}} = update_listener_via_api(ListenerId, ListenerData1),
  990. #{<<"code">> := <<"BAD_REQUEST">>, <<"message">> := MsgRaw1} =
  991. emqx_utils_json:decode(ResRaw1, [return_maps]),
  992. ?assertMatch(
  993. #{
  994. <<"kind">> := <<"validation_error">>,
  995. <<"reason">> :=
  996. <<"verify must be verify_peer when CRL check is enabled">>
  997. },
  998. emqx_utils_json:decode(MsgRaw1, [return_maps])
  999. ),
  1000. ok.