emqx_ocsp_cache_SUITE.erl 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
  3. %%--------------------------------------------------------------------
  4. -module(emqx_ocsp_cache_SUITE).
  5. -compile(export_all).
  6. -compile(nowarn_export_all).
  7. -include_lib("eunit/include/eunit.hrl").
  8. -include_lib("common_test/include/ct.hrl").
  9. -include_lib("snabbkaffe/include/snabbkaffe.hrl").
  10. -include_lib("ssl/src/ssl_handshake.hrl").
  11. -define(CACHE_TAB, emqx_ocsp_cache).
  12. all() ->
  13. [{group, openssl}] ++ tests().
  14. tests() ->
  15. emqx_common_test_helpers:all(?MODULE) -- openssl_tests().
  16. openssl_tests() ->
  17. [t_openssl_client].
  18. groups() ->
  19. OpensslTests = openssl_tests(),
  20. [
  21. {openssl, [
  22. {group, tls12},
  23. {group, tls13}
  24. ]},
  25. {tls12, [
  26. {group, with_status_request},
  27. {group, without_status_request}
  28. ]},
  29. {tls13, [
  30. {group, with_status_request},
  31. {group, without_status_request}
  32. ]},
  33. {with_status_request, [], OpensslTests},
  34. {without_status_request, [], OpensslTests}
  35. ].
  36. init_per_suite(Config) ->
  37. application:load(emqx),
  38. emqx_config:save_schema_mod_and_names(emqx_schema),
  39. emqx_common_test_helpers:boot_modules(all),
  40. Config.
  41. end_per_suite(_Config) ->
  42. ok.
  43. init_per_group(tls12, Config) ->
  44. [{tls_vsn, "-tls1_2"} | Config];
  45. init_per_group(tls13, Config) ->
  46. [{tls_vsn, "-tls1_3"} | Config];
  47. init_per_group(with_status_request, Config) ->
  48. [{status_request, true} | Config];
  49. init_per_group(without_status_request, Config) ->
  50. [{status_request, false} | Config];
  51. init_per_group(_Group, Config) ->
  52. Config.
  53. end_per_group(_Group, _Config) ->
  54. ok.
  55. init_per_testcase(t_openssl_client, Config) ->
  56. ct:timetrap({seconds, 30}),
  57. DataDir = ?config(data_dir, Config),
  58. Handler = fun(_) -> ok end,
  59. {OCSPResponderPort, OCSPOSPid} = setup_openssl_ocsp(Config),
  60. ConfFilePath = filename:join([DataDir, "openssl_listeners.conf"]),
  61. emqx_common_test_helpers:start_apps(
  62. [],
  63. Handler,
  64. #{
  65. extra_mustache_vars => #{test_data_dir => DataDir},
  66. conf_file_path => ConfFilePath
  67. }
  68. ),
  69. ct:sleep(1_000),
  70. [
  71. {ocsp_responder_port, OCSPResponderPort},
  72. {ocsp_responder_os_pid, OCSPOSPid}
  73. | Config
  74. ];
  75. init_per_testcase(TestCase, Config) when
  76. TestCase =:= t_update_listener;
  77. TestCase =:= t_validations
  78. ->
  79. %% when running emqx standalone tests, we can't use those
  80. %% features.
  81. case does_module_exist(emqx_mgmt_api_test_util) of
  82. true ->
  83. ct:timetrap({seconds, 30}),
  84. %% start the listener with the default (non-ocsp) config
  85. TestPid = self(),
  86. ok = meck:new(emqx_ocsp_cache, [non_strict, passthrough, no_history, no_link]),
  87. meck:expect(
  88. emqx_ocsp_cache,
  89. http_get,
  90. fun(URL, _HTTPTimeout) ->
  91. ct:pal("ocsp http request ~p", [URL]),
  92. TestPid ! {http_get, URL},
  93. {ok, {{"HTTP/1.0", 200, 'OK'}, [], <<"ocsp response">>}}
  94. end
  95. ),
  96. emqx_mgmt_api_test_util:init_suite([emqx_conf]),
  97. snabbkaffe:start_trace(),
  98. Config;
  99. false ->
  100. [{skip_does_not_apply, true} | Config]
  101. end;
  102. init_per_testcase(t_ocsp_responder_error_responses, Config) ->
  103. ct:timetrap({seconds, 30}),
  104. TestPid = self(),
  105. ok = meck:new(emqx_ocsp_cache, [non_strict, passthrough, no_history, no_link]),
  106. meck:expect(
  107. emqx_ocsp_cache,
  108. http_get,
  109. fun(URL, _HTTPTimeout) ->
  110. ct:pal("ocsp http request ~p", [URL]),
  111. TestPid ! {http_get, URL},
  112. persistent_term:get({?MODULE, http_response})
  113. end
  114. ),
  115. DataDir = ?config(data_dir, Config),
  116. Type = ssl,
  117. Name = test_ocsp,
  118. ListenerOpts = #{
  119. ssl_options =>
  120. #{
  121. certfile => filename:join(DataDir, "server.pem"),
  122. ocsp => #{
  123. enable_ocsp_stapling => true,
  124. responder_url => <<"http://localhost:9877/">>,
  125. issuer_pem => filename:join(DataDir, "ocsp-issuer.pem"),
  126. refresh_http_timeout => <<"15s">>,
  127. refresh_interval => <<"1s">>
  128. }
  129. }
  130. },
  131. Conf = #{listeners => #{Type => #{Name => ListenerOpts}}},
  132. ConfBin = emqx_utils_maps:binary_key_map(Conf),
  133. CheckedConf = hocon_tconf:check_plain(emqx_schema, ConfBin, #{
  134. required => false, atom_keys => false
  135. }),
  136. Conf2 = emqx_utils_maps:unsafe_atom_key_map(CheckedConf),
  137. ListenerOpts2 = emqx_utils_maps:deep_get([listeners, Type, Name], Conf2),
  138. emqx_config:put_listener_conf(Type, Name, [], ListenerOpts2),
  139. snabbkaffe:start_trace(),
  140. _Heir = spawn_dummy_heir(),
  141. {ok, CachePid} = emqx_ocsp_cache:start_link(),
  142. [
  143. {cache_pid, CachePid}
  144. | Config
  145. ];
  146. init_per_testcase(_TestCase, Config) ->
  147. ct:timetrap({seconds, 10}),
  148. TestPid = self(),
  149. ok = meck:new(emqx_ocsp_cache, [non_strict, passthrough, no_history, no_link]),
  150. meck:expect(
  151. emqx_ocsp_cache,
  152. http_get,
  153. fun(URL, _HTTPTimeout) ->
  154. TestPid ! {http_get, URL},
  155. {ok, {{"HTTP/1.0", 200, 'OK'}, [], <<"ocsp response">>}}
  156. end
  157. ),
  158. snabbkaffe:start_trace(),
  159. _Heir = spawn_dummy_heir(),
  160. {ok, CachePid} = emqx_ocsp_cache:start_link(),
  161. DataDir = ?config(data_dir, Config),
  162. Type = ssl,
  163. Name = test_ocsp,
  164. ListenerOpts = #{
  165. ssl_options =>
  166. #{
  167. certfile => filename:join(DataDir, "server.pem"),
  168. ocsp => #{
  169. enable_ocsp_stapling => true,
  170. responder_url => <<"http://localhost:9877/">>,
  171. issuer_pem => filename:join(DataDir, "ocsp-issuer.pem"),
  172. refresh_http_timeout => <<"15s">>,
  173. refresh_interval => <<"1s">>
  174. }
  175. }
  176. },
  177. Conf = #{listeners => #{Type => #{Name => ListenerOpts}}},
  178. ConfBin = emqx_utils_maps:binary_key_map(Conf),
  179. CheckedConf = hocon_tconf:check_plain(emqx_schema, ConfBin, #{
  180. required => false, atom_keys => false
  181. }),
  182. Conf2 = emqx_utils_maps:unsafe_atom_key_map(CheckedConf),
  183. ListenerOpts2 = emqx_utils_maps:deep_get([listeners, Type, Name], Conf2),
  184. emqx_config:put_listener_conf(Type, Name, [], ListenerOpts2),
  185. [
  186. {cache_pid, CachePid}
  187. | Config
  188. ].
  189. end_per_testcase(t_openssl_client, Config) ->
  190. OCSPResponderOSPid = ?config(ocsp_responder_os_pid, Config),
  191. catch kill_pid(OCSPResponderOSPid),
  192. emqx_common_test_helpers:stop_apps([]),
  193. ok;
  194. end_per_testcase(TestCase, Config) when
  195. TestCase =:= t_update_listener;
  196. TestCase =:= t_validations
  197. ->
  198. Skip = proplists:get_bool(skip_does_not_apply, Config),
  199. case Skip of
  200. true ->
  201. ok;
  202. false ->
  203. emqx_mgmt_api_test_util:end_suite([emqx_conf]),
  204. meck:unload([emqx_ocsp_cache]),
  205. ok
  206. end;
  207. end_per_testcase(t_ocsp_responder_error_responses, Config) ->
  208. CachePid = ?config(cache_pid, Config),
  209. catch gen_server:stop(CachePid),
  210. meck:unload([emqx_ocsp_cache]),
  211. persistent_term:erase({?MODULE, http_response}),
  212. ok;
  213. end_per_testcase(_TestCase, Config) ->
  214. CachePid = ?config(cache_pid, Config),
  215. catch gen_server:stop(CachePid),
  216. meck:unload([emqx_ocsp_cache]),
  217. ok.
  218. %%--------------------------------------------------------------------
  219. %% Helper functions
  220. %%--------------------------------------------------------------------
  221. %% The real cache makes `emqx_kernel_sup' the heir to its ETS table.
  222. %% In some tests, we don't start the full supervision tree, so we need
  223. %% this dummy process.
  224. spawn_dummy_heir() ->
  225. {_, {ok, _}} =
  226. ?wait_async_action(
  227. spawn_link(fun() ->
  228. true = register(emqx_kernel_sup, self()),
  229. ?tp(heir_name_registered, #{}),
  230. receive
  231. stop -> ok
  232. end
  233. end),
  234. #{?snk_kind := heir_name_registered},
  235. 1_000
  236. ),
  237. ok.
  238. does_module_exist(Mod) ->
  239. case erlang:module_loaded(Mod) of
  240. true ->
  241. true;
  242. false ->
  243. case code:ensure_loaded(Mod) of
  244. ok ->
  245. true;
  246. {module, Mod} ->
  247. true;
  248. _ ->
  249. false
  250. end
  251. end.
  252. assert_no_http_get() ->
  253. Timeout = 0,
  254. Error = should_be_cached,
  255. assert_no_http_get(Timeout, Error).
  256. assert_no_http_get(Timeout, Error) ->
  257. receive
  258. {http_get, _URL} ->
  259. error(Error)
  260. after Timeout ->
  261. ok
  262. end.
  263. assert_http_get(N) ->
  264. assert_http_get(N, 0).
  265. assert_http_get(0, _Timeout) ->
  266. ok;
  267. assert_http_get(N, Timeout) when N > 0 ->
  268. receive
  269. {http_get, URL} ->
  270. ?assertMatch(<<"http://localhost:9877/", _Request64/binary>>, URL),
  271. ok
  272. after Timeout ->
  273. error({no_http_get, #{mailbox => process_info(self(), messages)}})
  274. end,
  275. assert_http_get(N - 1, Timeout).
  276. openssl_client_command(TLSVsn, RequestStatus, Config) ->
  277. DataDir = ?config(data_dir, Config),
  278. ClientCert = filename:join([DataDir, "client.pem"]),
  279. ClientKey = filename:join([DataDir, "client.key"]),
  280. Cacert = filename:join([DataDir, "ca.pem"]),
  281. Openssl = os:find_executable("openssl"),
  282. StatusOpt =
  283. case RequestStatus of
  284. true -> ["-status"];
  285. false -> []
  286. end,
  287. [
  288. Openssl,
  289. "s_client",
  290. "-connect",
  291. "localhost:8883",
  292. %% needed to trigger `sni_fun'
  293. "-servername",
  294. "localhost",
  295. TLSVsn,
  296. "-CAfile",
  297. Cacert,
  298. "-cert",
  299. ClientCert,
  300. "-key",
  301. ClientKey
  302. ] ++ StatusOpt.
  303. run_openssl_client(TLSVsn, RequestStatus, Config) ->
  304. Command0 = openssl_client_command(TLSVsn, RequestStatus, Config),
  305. Command = lists:flatten(lists:join(" ", Command0)),
  306. os:cmd(Command).
  307. %% fixme: for some reason, the port program doesn't return any output
  308. %% when running in OTP 25 using `open_port`, but the `os:cmd` version
  309. %% works fine.
  310. %% the `open_port' version works fine in OTP 24 for some reason.
  311. spawn_openssl_client(TLSVsn, RequestStatus, Config) ->
  312. [Openssl | Args] = openssl_client_command(TLSVsn, RequestStatus, Config),
  313. open_port(
  314. {spawn_executable, Openssl},
  315. [
  316. {args, Args},
  317. binary,
  318. stderr_to_stdout
  319. ]
  320. ).
  321. spawn_openssl_ocsp_responder(Config) ->
  322. DataDir = ?config(data_dir, Config),
  323. IssuerCert = filename:join([DataDir, "ocsp-issuer.pem"]),
  324. IssuerKey = filename:join([DataDir, "ocsp-issuer.key"]),
  325. Cacert = filename:join([DataDir, "ca.pem"]),
  326. Index = filename:join([DataDir, "index.txt"]),
  327. Openssl = os:find_executable("openssl"),
  328. open_port(
  329. {spawn_executable, Openssl},
  330. [
  331. {args, [
  332. "ocsp",
  333. "-ignore_err",
  334. "-port",
  335. "9877",
  336. "-CA",
  337. Cacert,
  338. "-rkey",
  339. IssuerKey,
  340. "-rsigner",
  341. IssuerCert,
  342. "-index",
  343. Index
  344. ]},
  345. binary,
  346. stderr_to_stdout
  347. ]
  348. ).
  349. kill_pid(OSPid) ->
  350. os:cmd("kill -9 " ++ integer_to_list(OSPid)).
  351. test_ocsp_connection(TLSVsn, WithRequestStatus = true, Config) ->
  352. OCSPOutput = run_openssl_client(TLSVsn, WithRequestStatus, Config),
  353. ?assertMatch(
  354. {match, _},
  355. re:run(OCSPOutput, "OCSP Response Status: successful"),
  356. #{mailbox => process_info(self(), messages)}
  357. ),
  358. ?assertMatch(
  359. {match, _},
  360. re:run(OCSPOutput, "Cert Status: good"),
  361. #{mailbox => process_info(self(), messages)}
  362. ),
  363. ok;
  364. test_ocsp_connection(TLSVsn, WithRequestStatus = false, Config) ->
  365. OCSPOutput = run_openssl_client(TLSVsn, WithRequestStatus, Config),
  366. ?assertMatch(
  367. nomatch,
  368. re:run(OCSPOutput, "Cert Status: good", [{capture, none}]),
  369. #{mailbox => process_info(self(), messages)}
  370. ),
  371. ok.
  372. ensure_port_open(Port) ->
  373. do_ensure_port_open(Port, 10).
  374. do_ensure_port_open(Port, 0) ->
  375. error({port_not_open, Port});
  376. do_ensure_port_open(Port, N) when N > 0 ->
  377. Timeout = 1_000,
  378. case gen_tcp:connect("localhost", Port, [], Timeout) of
  379. {ok, Sock} ->
  380. gen_tcp:close(Sock),
  381. ok;
  382. {error, _} ->
  383. ct:sleep(500),
  384. do_ensure_port_open(Port, N - 1)
  385. end.
  386. get_sni_fun(ListenerID) ->
  387. #{opts := Opts} = emqx_listeners:find_by_id(ListenerID),
  388. SSLOpts = proplists:get_value(ssl_options, Opts),
  389. proplists:get_value(sni_fun, SSLOpts).
  390. openssl_version() ->
  391. Res0 = string:trim(os:cmd("openssl version"), trailing),
  392. [_, Res] = string:split(Res0, " "),
  393. {match, [Version]} = re:run(Res, "^([^ ]+)", [{capture, first, list}]),
  394. Version.
  395. setup_openssl_ocsp(Config) ->
  396. OCSPResponderPort = spawn_openssl_ocsp_responder(Config),
  397. {os_pid, OCSPOSPid} = erlang:port_info(OCSPResponderPort, os_pid),
  398. %%%%%%%% Warning!!!
  399. %% Apparently, openssl 3.0.7 introduced a bug in the responder
  400. %% that makes it hang forever if one probes the port with
  401. %% `gen_tcp:open' / `gen_tcp:close'... Comment this out if
  402. %% openssl gets updated in CI or in your local machine.
  403. OpenSSLVersion = openssl_version(),
  404. ct:pal("openssl version: ~p", [OpenSSLVersion]),
  405. case OpenSSLVersion of
  406. "3." ++ _ ->
  407. %% hope that the responder has started...
  408. ok;
  409. _ ->
  410. ensure_port_open(9877)
  411. end,
  412. ct:sleep(1_000),
  413. {OCSPResponderPort, OCSPOSPid}.
  414. request(Method, Url, QueryParams, Body) ->
  415. AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
  416. Opts = #{return_all => true},
  417. case emqx_mgmt_api_test_util:request_api(Method, Url, QueryParams, AuthHeader, Body, Opts) of
  418. {ok, {Reason, Headers, BodyR}} ->
  419. {ok, {Reason, Headers, emqx_utils_json:decode(BodyR, [return_maps])}};
  420. Error ->
  421. Error
  422. end.
  423. get_listener_via_api(ListenerId) ->
  424. Path = emqx_mgmt_api_test_util:api_path(["listeners", ListenerId]),
  425. request(get, Path, [], []).
  426. update_listener_via_api(ListenerId, NewConfig) ->
  427. Path = emqx_mgmt_api_test_util:api_path(["listeners", ListenerId]),
  428. request(put, Path, [], NewConfig).
  429. put_http_response(Response) ->
  430. persistent_term:put({?MODULE, http_response}, Response).
  431. %%--------------------------------------------------------------------
  432. %% Test cases
  433. %%--------------------------------------------------------------------
  434. t_request_ocsp_response(_Config) ->
  435. ?check_trace(
  436. begin
  437. ListenerID = <<"ssl:test_ocsp">>,
  438. %% not yet cached.
  439. ?assertEqual([], ets:tab2list(?CACHE_TAB)),
  440. ?assertEqual(
  441. {ok, <<"ocsp response">>},
  442. emqx_ocsp_cache:fetch_response(ListenerID)
  443. ),
  444. assert_http_get(1),
  445. ?assertMatch([{_, <<"ocsp response">>}], ets:tab2list(?CACHE_TAB)),
  446. %% already cached; should not perform request again.
  447. ?assertEqual(
  448. {ok, <<"ocsp response">>},
  449. emqx_ocsp_cache:fetch_response(ListenerID)
  450. ),
  451. assert_no_http_get(),
  452. ok
  453. end,
  454. fun(Trace) ->
  455. ?assert(
  456. ?strict_causality(
  457. #{?snk_kind := ocsp_cache_miss, listener_id := _ListenerID},
  458. #{?snk_kind := ocsp_http_fetch_and_cache, listener_id := _ListenerID},
  459. Trace
  460. )
  461. ),
  462. ?assertMatch(
  463. [_],
  464. ?of_kind(ocsp_cache_miss, Trace)
  465. ),
  466. ?assertMatch(
  467. [_],
  468. ?of_kind(ocsp_http_fetch_and_cache, Trace)
  469. ),
  470. ?assertMatch(
  471. [_],
  472. ?of_kind(ocsp_cache_hit, Trace)
  473. ),
  474. ok
  475. end
  476. ).
  477. t_request_ocsp_response_restart_cache(Config) ->
  478. process_flag(trap_exit, true),
  479. CachePid = ?config(cache_pid, Config),
  480. ListenerID = <<"ssl:test_ocsp">>,
  481. ?check_trace(
  482. begin
  483. [] = ets:tab2list(?CACHE_TAB),
  484. {ok, _} = emqx_ocsp_cache:fetch_response(ListenerID),
  485. ?wait_async_action(
  486. begin
  487. Ref = monitor(process, CachePid),
  488. exit(CachePid, kill),
  489. receive
  490. {'DOWN', Ref, process, CachePid, killed} ->
  491. ok
  492. after 1_000 ->
  493. error(cache_not_killed)
  494. end,
  495. {ok, _} = emqx_ocsp_cache:start_link(),
  496. ok
  497. end,
  498. #{?snk_kind := ocsp_cache_init}
  499. ),
  500. {ok, _} = emqx_ocsp_cache:fetch_response(ListenerID),
  501. ok
  502. end,
  503. fun(Trace) ->
  504. %% Only one fetch because the cache table was preserved by
  505. %% its heir ("emqx_kernel_sup").
  506. ?assertMatch(
  507. [_],
  508. ?of_kind(ocsp_http_fetch_and_cache, Trace)
  509. ),
  510. assert_http_get(1),
  511. ok
  512. end
  513. ).
  514. t_request_ocsp_response_bad_http_status(_Config) ->
  515. TestPid = self(),
  516. meck:expect(
  517. emqx_ocsp_cache,
  518. http_get,
  519. fun(URL, _HTTPTimeout) ->
  520. TestPid ! {http_get, URL},
  521. {ok, {{"HTTP/1.0", 404, 'Not Found'}, [], <<"not found">>}}
  522. end
  523. ),
  524. ListenerID = <<"ssl:test_ocsp">>,
  525. %% not yet cached.
  526. ?assertEqual([], ets:tab2list(?CACHE_TAB)),
  527. ?assertEqual(
  528. error,
  529. emqx_ocsp_cache:fetch_response(ListenerID)
  530. ),
  531. assert_http_get(1),
  532. ?assertEqual([], ets:tab2list(?CACHE_TAB)),
  533. ok.
  534. t_request_ocsp_response_timeout(_Config) ->
  535. TestPid = self(),
  536. meck:expect(
  537. emqx_ocsp_cache,
  538. http_get,
  539. fun(URL, _HTTPTimeout) ->
  540. TestPid ! {http_get, URL},
  541. {error, timeout}
  542. end
  543. ),
  544. ListenerID = <<"ssl:test_ocsp">>,
  545. %% not yet cached.
  546. ?assertEqual([], ets:tab2list(?CACHE_TAB)),
  547. ?assertEqual(
  548. error,
  549. emqx_ocsp_cache:fetch_response(ListenerID)
  550. ),
  551. assert_http_get(1),
  552. ?assertEqual([], ets:tab2list(?CACHE_TAB)),
  553. ok.
  554. t_register_listener(_Config) ->
  555. ListenerID = <<"ssl:test_ocsp">>,
  556. Conf = emqx_config:get_listener_conf(ssl, test_ocsp, []),
  557. %% should fetch and cache immediately
  558. {ok, {ok, _}} =
  559. ?wait_async_action(
  560. emqx_ocsp_cache:register_listener(ListenerID, Conf),
  561. #{?snk_kind := ocsp_http_fetch_and_cache, listener_id := ListenerID}
  562. ),
  563. assert_http_get(1),
  564. ?assertMatch([{_, <<"ocsp response">>}], ets:tab2list(?CACHE_TAB)),
  565. ok.
  566. t_register_twice(_Config) ->
  567. ListenerID = <<"ssl:test_ocsp">>,
  568. Conf = emqx_config:get_listener_conf(ssl, test_ocsp, []),
  569. {ok, {ok, _}} =
  570. ?wait_async_action(
  571. emqx_ocsp_cache:register_listener(ListenerID, Conf),
  572. #{?snk_kind := ocsp_http_fetch_and_cache, listener_id := ListenerID}
  573. ),
  574. assert_http_get(1),
  575. ?assertMatch([{_, <<"ocsp response">>}], ets:tab2list(?CACHE_TAB)),
  576. %% should have no problem in registering the same listener again.
  577. %% this prompts an immediate refresh.
  578. {ok, {ok, _}} =
  579. ?wait_async_action(
  580. emqx_ocsp_cache:register_listener(ListenerID, Conf),
  581. #{?snk_kind := ocsp_http_fetch_and_cache, listener_id := ListenerID}
  582. ),
  583. ok.
  584. t_refresh_periodically(_Config) ->
  585. ListenerID = <<"ssl:test_ocsp">>,
  586. Conf = emqx_config:get_listener_conf(ssl, test_ocsp, []),
  587. %% should refresh periodically
  588. {ok, SubRef} =
  589. snabbkaffe:subscribe(
  590. fun
  591. (#{?snk_kind := ocsp_http_fetch_and_cache, listener_id := ListenerID0}) ->
  592. ListenerID0 =:= ListenerID;
  593. (_) ->
  594. false
  595. end,
  596. _NEvents = 2,
  597. _Timeout = 10_000
  598. ),
  599. ok = emqx_ocsp_cache:register_listener(ListenerID, Conf),
  600. ?assertMatch({ok, [_, _]}, snabbkaffe:receive_events(SubRef)),
  601. assert_http_get(2),
  602. ok.
  603. t_sni_fun_success(_Config) ->
  604. ListenerID = <<"ssl:test_ocsp">>,
  605. ServerName = "localhost",
  606. ?assertEqual(
  607. [
  608. {certificate_status, #certificate_status{
  609. status_type = ?CERTIFICATE_STATUS_TYPE_OCSP,
  610. response = <<"ocsp response">>
  611. }}
  612. ],
  613. emqx_ocsp_cache:sni_fun(ServerName, ListenerID)
  614. ),
  615. ok.
  616. t_sni_fun_http_error(_Config) ->
  617. meck:expect(
  618. emqx_ocsp_cache,
  619. http_get,
  620. fun(_URL, _HTTPTimeout) ->
  621. {error, timeout}
  622. end
  623. ),
  624. ListenerID = <<"ssl:test_ocsp">>,
  625. ServerName = "localhost",
  626. ?assertEqual(
  627. [],
  628. emqx_ocsp_cache:sni_fun(ServerName, ListenerID)
  629. ),
  630. ok.
  631. %% check that we can start with a non-ocsp stapling listener and
  632. %% restart it with the new ocsp config.
  633. t_update_listener(Config) ->
  634. case proplists:get_bool(skip_does_not_apply, Config) of
  635. true ->
  636. ok;
  637. false ->
  638. do_t_update_listener(Config)
  639. end.
  640. do_t_update_listener(Config) ->
  641. DataDir = ?config(data_dir, Config),
  642. Keyfile = filename:join([DataDir, "server.key"]),
  643. Certfile = filename:join([DataDir, "server.pem"]),
  644. Cacertfile = filename:join([DataDir, "ca.pem"]),
  645. IssuerPemPath = filename:join([DataDir, "ocsp-issuer.pem"]),
  646. {ok, IssuerPem} = file:read_file(IssuerPemPath),
  647. %% no ocsp at first
  648. ListenerId = "ssl:default",
  649. {ok, {{_, 200, _}, _, ListenerData0}} = get_listener_via_api(ListenerId),
  650. ?assertMatch(
  651. #{
  652. <<"enable_ocsp_stapling">> := false,
  653. <<"refresh_http_timeout">> := _,
  654. <<"refresh_interval">> := _
  655. },
  656. emqx_utils_maps:deep_get([<<"ssl_options">>, <<"ocsp">>], ListenerData0, undefined)
  657. ),
  658. assert_no_http_get(),
  659. %% configure ocsp
  660. OCSPConfig =
  661. #{
  662. <<"ssl_options">> =>
  663. #{
  664. <<"keyfile">> => Keyfile,
  665. <<"certfile">> => Certfile,
  666. <<"cacertfile">> => Cacertfile,
  667. <<"ocsp">> =>
  668. #{
  669. <<"enable_ocsp_stapling">> => true,
  670. %% we use the file contents to check that
  671. %% the API converts that to an internally
  672. %% managed file
  673. <<"issuer_pem">> => IssuerPem,
  674. <<"responder_url">> => <<"http://localhost:9877">>,
  675. %% for quicker testing; min refresh in tests is 5 s.
  676. <<"refresh_interval">> => <<"5s">>
  677. }
  678. }
  679. },
  680. ListenerData1 = emqx_utils_maps:deep_merge(ListenerData0, OCSPConfig),
  681. {ok, {_, _, ListenerData2}} = update_listener_via_api(ListenerId, ListenerData1),
  682. ?assertMatch(
  683. #{
  684. <<"ssl_options">> :=
  685. #{
  686. <<"ocsp">> :=
  687. #{
  688. <<"enable_ocsp_stapling">> := true,
  689. <<"issuer_pem">> := _,
  690. <<"responder_url">> := _
  691. }
  692. }
  693. },
  694. ListenerData2
  695. ),
  696. %% issuer pem should have been uploaded and saved to a new
  697. %% location
  698. ?assertNotEqual(
  699. IssuerPemPath,
  700. emqx_utils_maps:deep_get(
  701. [<<"ssl_options">>, <<"ocsp">>, <<"issuer_pem">>],
  702. ListenerData2
  703. )
  704. ),
  705. ?assertNotEqual(
  706. IssuerPem,
  707. emqx_utils_maps:deep_get(
  708. [<<"ssl_options">>, <<"ocsp">>, <<"issuer_pem">>],
  709. ListenerData2
  710. )
  711. ),
  712. assert_http_get(1, 5_000),
  713. %% Disable OCSP Stapling; the periodic refreshes should stop
  714. RefreshInterval = emqx_config:get([listeners, ssl, default, ssl_options, ocsp, refresh_interval]),
  715. OCSPConfig1 =
  716. #{
  717. <<"ssl_options">> =>
  718. #{
  719. <<"ocsp">> =>
  720. #{
  721. <<"enable_ocsp_stapling">> => false
  722. }
  723. }
  724. },
  725. ListenerData3 = emqx_utils_maps:deep_merge(ListenerData2, OCSPConfig1),
  726. {ok, {_, _, ListenerData4}} = update_listener_via_api(ListenerId, ListenerData3),
  727. ?assertMatch(
  728. #{
  729. <<"ssl_options">> :=
  730. #{
  731. <<"ocsp">> :=
  732. #{
  733. <<"enable_ocsp_stapling">> := false
  734. }
  735. }
  736. },
  737. ListenerData4
  738. ),
  739. assert_no_http_get(2 * RefreshInterval, should_stop_refreshing),
  740. ok.
  741. t_double_unregister(_Config) ->
  742. ListenerID = <<"ssl:test_ocsp">>,
  743. Conf = emqx_config:get_listener_conf(ssl, test_ocsp, []),
  744. ?check_trace(
  745. begin
  746. {ok, {ok, _}} =
  747. ?wait_async_action(
  748. emqx_ocsp_cache:register_listener(ListenerID, Conf),
  749. #{?snk_kind := ocsp_http_fetch_and_cache, listener_id := ListenerID},
  750. 5_000
  751. ),
  752. assert_http_get(1),
  753. {ok, {ok, _}} =
  754. ?wait_async_action(
  755. emqx_ocsp_cache:unregister_listener(ListenerID),
  756. #{?snk_kind := ocsp_cache_listener_unregistered, listener_id := ListenerID},
  757. 5_000
  758. ),
  759. %% Should be idempotent and not crash
  760. {ok, {ok, _}} =
  761. ?wait_async_action(
  762. emqx_ocsp_cache:unregister_listener(ListenerID),
  763. #{?snk_kind := ocsp_cache_listener_unregistered, listener_id := ListenerID},
  764. 5_000
  765. ),
  766. ok
  767. end,
  768. []
  769. ),
  770. ok.
  771. t_ocsp_responder_error_responses(_Config) ->
  772. ListenerId = <<"ssl:test_ocsp">>,
  773. Conf = emqx_config:get_listener_conf(ssl, test_ocsp, []),
  774. ?check_trace(
  775. begin
  776. %% successful response without headers
  777. put_http_response({ok, {200, <<"ocsp_response">>}}),
  778. {ok, {ok, _}} =
  779. ?wait_async_action(
  780. emqx_ocsp_cache:register_listener(ListenerId, Conf),
  781. #{?snk_kind := ocsp_http_fetch_and_cache, headers := false},
  782. 1_000
  783. ),
  784. %% error response with headers
  785. put_http_response({ok, {{"HTTP/1.0", 500, "Internal Server Error"}, [], <<"error">>}}),
  786. {ok, {ok, _}} =
  787. ?wait_async_action(
  788. emqx_ocsp_cache:register_listener(ListenerId, Conf),
  789. #{?snk_kind := ocsp_http_fetch_bad_code, code := 500, headers := true},
  790. 1_000
  791. ),
  792. %% error response without headers
  793. put_http_response({ok, {500, <<"error">>}}),
  794. {ok, {ok, _}} =
  795. ?wait_async_action(
  796. emqx_ocsp_cache:register_listener(ListenerId, Conf),
  797. #{?snk_kind := ocsp_http_fetch_bad_code, code := 500, headers := false},
  798. 1_000
  799. ),
  800. %% econnrefused
  801. put_http_response(
  802. {error,
  803. {failed_connect, [
  804. {to_address, {"localhost", 9877}},
  805. {inet, [inet], econnrefused}
  806. ]}}
  807. ),
  808. {ok, {ok, _}} =
  809. ?wait_async_action(
  810. emqx_ocsp_cache:register_listener(ListenerId, Conf),
  811. #{?snk_kind := ocsp_http_fetch_error, error := {failed_connect, _}},
  812. 1_000
  813. ),
  814. %% timeout
  815. put_http_response({error, timeout}),
  816. {ok, {ok, _}} =
  817. ?wait_async_action(
  818. emqx_ocsp_cache:register_listener(ListenerId, Conf),
  819. #{?snk_kind := ocsp_http_fetch_error, error := timeout},
  820. 1_000
  821. ),
  822. ok
  823. end,
  824. []
  825. ),
  826. ok.
  827. t_unknown_requests(_Config) ->
  828. emqx_ocsp_cache ! unknown,
  829. ?assertEqual(ok, gen_server:cast(emqx_ocsp_cache, unknown)),
  830. ?assertEqual({error, {unknown_call, unknown}}, gen_server:call(emqx_ocsp_cache, unknown)),
  831. ok.
  832. t_validations(Config) ->
  833. case proplists:get_bool(skip_does_not_apply, Config) of
  834. true ->
  835. ok;
  836. false ->
  837. do_t_validations(Config)
  838. end.
  839. do_t_validations(_Config) ->
  840. ListenerId = <<"ssl:default">>,
  841. {ok, {{_, 200, _}, _, ListenerData0}} = get_listener_via_api(ListenerId),
  842. ListenerData1 =
  843. emqx_utils_maps:deep_merge(
  844. ListenerData0,
  845. #{
  846. <<"ssl_options">> =>
  847. #{<<"ocsp">> => #{<<"enable_ocsp_stapling">> => true}}
  848. }
  849. ),
  850. {error, {_, _, ResRaw1}} = update_listener_via_api(ListenerId, ListenerData1),
  851. #{<<"code">> := <<"BAD_REQUEST">>, <<"message">> := MsgRaw1} =
  852. emqx_utils_json:decode(ResRaw1, [return_maps]),
  853. ?assertMatch(
  854. #{
  855. <<"kind">> := <<"validation_error">>,
  856. <<"reason">> :=
  857. <<"The responder URL is required for OCSP stapling">>
  858. },
  859. emqx_utils_json:decode(MsgRaw1, [return_maps])
  860. ),
  861. ListenerData2 =
  862. emqx_utils_maps:deep_merge(
  863. ListenerData0,
  864. #{
  865. <<"ssl_options">> =>
  866. #{
  867. <<"ocsp">> => #{
  868. <<"enable_ocsp_stapling">> => true,
  869. <<"responder_url">> => <<"http://localhost:9877">>
  870. }
  871. }
  872. }
  873. ),
  874. {error, {_, _, ResRaw2}} = update_listener_via_api(ListenerId, ListenerData2),
  875. #{<<"code">> := <<"BAD_REQUEST">>, <<"message">> := MsgRaw2} =
  876. emqx_utils_json:decode(ResRaw2, [return_maps]),
  877. ?assertMatch(
  878. #{
  879. <<"kind">> := <<"validation_error">>,
  880. <<"reason">> :=
  881. <<"The issuer PEM path is required for OCSP stapling">>
  882. },
  883. emqx_utils_json:decode(MsgRaw2, [return_maps])
  884. ),
  885. ListenerData3a =
  886. emqx_utils_maps:deep_merge(
  887. ListenerData0,
  888. #{
  889. <<"ssl_options">> =>
  890. #{
  891. <<"ocsp">> => #{
  892. <<"enable_ocsp_stapling">> => true,
  893. <<"responder_url">> => <<"http://localhost:9877">>,
  894. <<"issuer_pem">> => <<"some_file">>
  895. }
  896. }
  897. }
  898. ),
  899. ListenerData3 = emqx_utils_maps:deep_remove(
  900. [<<"ssl_options">>, <<"certfile">>], ListenerData3a
  901. ),
  902. {error, {_, _, ResRaw3}} = update_listener_via_api(ListenerId, ListenerData3),
  903. #{<<"code">> := <<"BAD_REQUEST">>, <<"message">> := MsgRaw3} =
  904. emqx_utils_json:decode(ResRaw3, [return_maps]),
  905. %% we can't remove certfile now, because it has default value.
  906. ?assertMatch(
  907. <<"{bad_ssl_config,#{file_read => enoent,pem_check => invalid_pem", _/binary>>,
  908. MsgRaw3
  909. ),
  910. ok.
  911. t_unknown_error_fetching_ocsp_response(_Config) ->
  912. ListenerID = <<"ssl:test_ocsp">>,
  913. TestPid = self(),
  914. ok = meck:expect(
  915. emqx_ocsp_cache,
  916. http_get,
  917. fun(_RequestURI, _HTTPTimeout) ->
  918. TestPid ! error_raised,
  919. meck:exception(error, something_went_wrong)
  920. end
  921. ),
  922. ?assertEqual(error, emqx_ocsp_cache:fetch_response(ListenerID)),
  923. receive
  924. error_raised -> ok
  925. after 200 -> ct:fail("should have tried to fetch ocsp response")
  926. end,
  927. ok.
  928. t_openssl_client(Config) ->
  929. TLSVsn = ?config(tls_vsn, Config),
  930. WithStatusRequest = ?config(status_request, Config),
  931. %% ensure ocsp response is already cached.
  932. ListenerID = <<"ssl:default">>,
  933. ?assertMatch(
  934. {ok, _},
  935. emqx_ocsp_cache:fetch_response(ListenerID),
  936. #{msgs => process_info(self(), messages)}
  937. ),
  938. timer:sleep(500),
  939. test_ocsp_connection(TLSVsn, WithStatusRequest, Config).