emqx_ocsp_cache_SUITE.erl 31 KB

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