1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2008-2018. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20%%
21
22-module(ssl_crl_SUITE).
23
24%% Note: This directive should only be used in test suites.
25-compile(export_all).
26
27-include_lib("common_test/include/ct.hrl").
28-include_lib("public_key/include/public_key.hrl").
29
30%%--------------------------------------------------------------------
31%% Common Test interface functions -----------------------------------
32%%--------------------------------------------------------------------
33all() ->
34    [
35     {group, check_true},
36     {group, check_peer},
37     {group, check_best_effort}
38    ].
39
40groups() ->
41    [
42     {check_true, [],  [{group, v2_crl},
43			{group, v1_crl},
44			{group, idp_crl},
45                        {group, crl_hash_dir}]},
46     {check_peer, [],   [{group, v2_crl},
47			 {group, v1_crl},
48			 {group, idp_crl},
49                         {group, crl_hash_dir}]},
50     {check_best_effort, [], [{group, v2_crl},
51			       {group, v1_crl},
52			      {group, idp_crl},
53			      {group, crl_hash_dir}]},
54     {v2_crl,  [], basic_tests()},
55     {v1_crl,  [], basic_tests()},
56     {idp_crl, [], basic_tests()},
57     {crl_hash_dir, [], basic_tests() ++ crl_hash_dir_tests()}].
58
59basic_tests() ->
60    [crl_verify_valid, crl_verify_revoked, crl_verify_no_crl].
61
62crl_hash_dir_tests() ->
63    [crl_hash_dir_collision, crl_hash_dir_expired].
64
65init_per_suite(Config) ->
66    case os:find_executable("openssl") of
67	false ->
68	    {skip, "Openssl not found"};
69	_ ->
70	    OpenSSL_version = (catch os:cmd("openssl version")),
71	    case ssl_test_lib:enough_openssl_crl_support(OpenSSL_version) of
72		false ->
73		    {skip, io_lib:format("Bad openssl version: ~p",[OpenSSL_version])};
74		_ ->
75		    end_per_suite(Config),
76		    try crypto:start() of
77			ok ->
78			    {ok, Hostname0} = inet:gethostname(),
79			    IPfamily =
80				case lists:member(list_to_atom(Hostname0), ct:get_config(ipv6_hosts,[])) of
81				    true -> inet6;
82				    false -> inet
83				end,
84			    [{ipfamily,IPfamily}, {openssl_version,OpenSSL_version} | Config]
85		    catch _:_ ->
86			    {skip, "Crypto did not start"}
87		    end
88	    end
89    end.
90
91end_per_suite(_Config) ->
92    ssl:stop(),
93    application:stop(crypto).
94
95init_per_group(check_true, Config) ->
96    [{crl_check, true} | Config];
97init_per_group(check_peer, Config) ->
98    [{crl_check, peer} | Config];
99init_per_group(check_best_effort, Config) ->
100    [{crl_check, best_effort} | Config];
101init_per_group(Group, Config0) ->
102    try
103	case is_idp(Group) of
104	    true ->
105		[{idp_crl, true} | Config0];
106	    false ->
107		DataDir = proplists:get_value(data_dir, Config0),
108		CertDir = filename:join(proplists:get_value(priv_dir, Config0), Group),
109		{CertOpts, Config} = init_certs(CertDir, Group, Config0),
110		{ok, _} =  make_certs:all(DataDir, CertDir, CertOpts),
111		CrlCacheOpts = case Group of
112				   crl_hash_dir ->
113				       CrlDir = filename:join(CertDir, "crls"),
114				       %% Copy CRLs to their hashed filenames.
115				       %% Find the hashes with 'openssl crl -noout -hash -in crl.pem'.
116				       populate_crl_hash_dir(CertDir, CrlDir,
117							     [{"erlangCA", "d6134ed3"},
118							      {"otpCA", "d4c8d7e5"}],
119							     replace),
120				       [{crl_cache,
121					 {ssl_crl_hash_dir,
122					  {internal, [{dir, CrlDir}]}}}];
123				   _ ->
124				       []
125			       end,
126		[{crl_cache_opts, CrlCacheOpts},
127		 {cert_dir, CertDir},
128		 {idp_crl, false} | Config]
129	end
130    catch
131	_:_ ->
132	    {skip, "Unable to create crls"}
133    end.
134
135end_per_group(_GroupName, Config) ->
136
137    Config.
138
139init_per_testcase(Case, Config0) ->
140    case proplists:get_value(idp_crl, Config0) of
141	true ->
142	    end_per_testcase(Case, Config0),
143	    inets:start(),
144	    ssl_test_lib:clean_start(),
145	    ServerRoot = make_dir_path([proplists:get_value(priv_dir, Config0), idp_crl, tmp]),
146	    %% start a HTTP server to serve the CRLs
147	    {ok, Httpd} = inets:start(httpd, [{ipfamily, proplists:get_value(ipfamily, Config0)},
148					      {server_name, "localhost"}, {port, 0},
149					      {server_root, ServerRoot},
150					      {document_root,
151					       filename:join(proplists:get_value(priv_dir, Config0), idp_crl)}
152					     ]),
153	    [{port,Port}] = httpd:info(Httpd, [port]),
154	    Config = [{httpd_port, Port} | Config0],
155	    DataDir = proplists:get_value(data_dir, Config),
156	    CertDir = filename:join(proplists:get_value(priv_dir, Config0), idp_crl),
157	    {CertOpts, Config} = init_certs(CertDir, idp_crl, Config),
158	    case make_certs:all(DataDir, CertDir, CertOpts) of
159                {ok, _} ->
160                    ct:timetrap({seconds, 6}),
161                    [{cert_dir, CertDir} | Config];
162                _ ->
163                    end_per_testcase(Case, Config0),
164                    ssl_test_lib:clean_start(),
165                    {skip, "Unable to create IDP crls"}
166            end;
167	false ->
168	    end_per_testcase(Case, Config0),
169	    ssl_test_lib:clean_start(),
170	    Config0
171    end.
172
173end_per_testcase(_, Config) ->
174    case proplists:get_value(idp_crl, Config) of
175	true ->
176	    ssl:stop(),
177	    inets:stop();
178	false ->
179	    ssl:stop()
180    end.
181
182%%%================================================================
183%%% Test cases
184%%%================================================================
185
186crl_verify_valid() ->
187    [{doc,"Verify a simple valid CRL chain"}].
188crl_verify_valid(Config) when is_list(Config) ->
189    PrivDir = proplists:get_value(cert_dir, Config),
190    Check = proplists:get_value(crl_check, Config),
191    ServerOpts =  [{keyfile, filename:join([PrivDir, "server", "key.pem"])},
192      		  {certfile, filename:join([PrivDir, "server", "cert.pem"])},
193		   {cacertfile, filename:join([PrivDir, "server", "cacerts.pem"])}],
194    ClientOpts =  case proplists:get_value(idp_crl, Config) of
195		      true ->
196			  [{cacertfile, filename:join([PrivDir, "server", "cacerts.pem"])},
197			   {crl_check, Check},
198			   {crl_cache, {ssl_crl_cache, {internal, [{http, 5000}]}}},
199			   {verify, verify_peer}];
200		      false ->
201			  proplists:get_value(crl_cache_opts, Config) ++
202			      [{cacertfile, filename:join([PrivDir, "server", "cacerts.pem"])},
203			       {crl_check, Check},
204			       {verify, verify_peer}]
205		  end,
206    {ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config),
207
208    ssl_crl_cache:insert({file, filename:join([PrivDir, "erlangCA", "crl.pem"])}),
209    ssl_crl_cache:insert({file, filename:join([PrivDir, "otpCA", "crl.pem"])}),
210
211    crl_verify_valid(Hostname, ServerNode, ServerOpts, ClientNode, ClientOpts).
212
213crl_verify_revoked() ->
214    [{doc,"Verify a simple CRL chain when peer cert is reveoked"}].
215crl_verify_revoked(Config)  when is_list(Config) ->
216    PrivDir = proplists:get_value(cert_dir, Config),
217    Check = proplists:get_value(crl_check, Config),
218    ServerOpts = [{keyfile, filename:join([PrivDir, "revoked", "key.pem"])},
219      		  {certfile, filename:join([PrivDir, "revoked", "cert.pem"])},
220      		  {cacertfile, filename:join([PrivDir, "revoked", "cacerts.pem"])}],
221
222    {ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config),
223
224    ssl_crl_cache:insert({file, filename:join([PrivDir, "erlangCA", "crl.pem"])}),
225    ssl_crl_cache:insert({file, filename:join([PrivDir, "otpCA", "crl.pem"])}),
226
227    ClientOpts =  case proplists:get_value(idp_crl, Config) of
228		      true ->
229			  [{cacertfile, filename:join([PrivDir, "revoked", "cacerts.pem"])},
230			   {crl_cache, {ssl_crl_cache, {internal, [{http, 5000}]}}},
231			   {crl_check, Check},
232			   {verify, verify_peer}];
233		      false ->
234			  proplists:get_value(crl_cache_opts, Config) ++
235			      [{cacertfile, filename:join([PrivDir, "revoked", "cacerts.pem"])},
236			       {crl_check, Check},
237			       {verify, verify_peer}]
238		  end,
239
240    crl_verify_error(Hostname, ServerNode, ServerOpts, ClientNode, ClientOpts,
241                     certificate_revoked).
242
243crl_verify_no_crl() ->
244    [{doc,"Verify a simple CRL chain when the CRL is missing"}].
245crl_verify_no_crl(Config) when is_list(Config) ->
246    PrivDir = proplists:get_value(cert_dir, Config),
247    Check = proplists:get_value(crl_check, Config),
248    ServerOpts =  [{keyfile, filename:join([PrivDir, "server", "key.pem"])},
249      		  {certfile, filename:join([PrivDir, "server", "cert.pem"])},
250		   {cacertfile, filename:join([PrivDir, "server", "cacerts.pem"])}],
251    ClientOpts =  case proplists:get_value(idp_crl, Config) of
252		      true ->
253			  [{cacertfile, filename:join([PrivDir, "server", "cacerts.pem"])},
254			   {crl_check, Check},
255			   {crl_cache, {ssl_crl_cache, {internal, [{http, 5000}]}}},
256			   {verify, verify_peer}];
257		      false ->
258			  [{cacertfile, filename:join([PrivDir, "server", "cacerts.pem"])},
259			   {crl_check, Check},
260			   {verify, verify_peer}]
261		  end,
262    {ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config),
263
264    %% In case we're running an HTTP server that serves CRLs, let's
265    %% rename those files, so the CRL is absent when we try to verify
266    %% it.
267    %%
268    %% If we're not using an HTTP server, we just need to refrain from
269    %% adding the CRLs to the cache manually.
270    rename_crl(filename:join([PrivDir, "erlangCA", "crl.pem"])),
271    rename_crl(filename:join([PrivDir, "otpCA", "crl.pem"])),
272
273    %% The expected outcome when the CRL is missing depends on the
274    %% crl_check setting.
275    case Check of
276        true ->
277            %% The error "revocation status undetermined" gets turned
278            %% into "bad certificate".
279            crl_verify_error(Hostname, ServerNode, ServerOpts, ClientNode, ClientOpts,
280                             bad_certificate);
281        peer ->
282            crl_verify_error(Hostname, ServerNode, ServerOpts, ClientNode, ClientOpts,
283                             bad_certificate);
284        best_effort ->
285            %% In "best effort" mode, we consider the certificate not
286            %% to be revoked if we can't find the appropriate CRL.
287            crl_verify_valid(Hostname, ServerNode, ServerOpts, ClientNode, ClientOpts)
288    end.
289
290crl_hash_dir_collision() ->
291    [{doc,"Verify ssl_crl_hash_dir behaviour with hash collisions"}].
292crl_hash_dir_collision(Config) when is_list(Config) ->
293    PrivDir = proplists:get_value(cert_dir, Config),
294    Check = proplists:get_value(crl_check, Config),
295
296    %% Create two CAs whose names hash to the same value
297    CA1 = "hash-collision-0000000000",
298    CA2 = "hash-collision-0258497583",
299    CertsConfig = make_certs:make_config([]),
300    make_certs:intermediateCA(PrivDir, CA1, "erlangCA", CertsConfig),
301    make_certs:intermediateCA(PrivDir, CA2, "erlangCA", CertsConfig),
302
303    make_certs:enduser(PrivDir, CA1, "collision-client-1", CertsConfig),
304    make_certs:enduser(PrivDir, CA2, "collision-client-2", CertsConfig),
305
306    [ServerOpts1, ServerOpts2] =
307	[
308	 [{keyfile, filename:join([PrivDir, EndUser, "key.pem"])},
309	  {certfile, filename:join([PrivDir, EndUser, "cert.pem"])},
310	  {cacertfile, filename:join([PrivDir, EndUser, "cacerts.pem"])}]
311	 || EndUser <- ["collision-client-1", "collision-client-2"]],
312
313    %% Add CRLs for our new CAs into the CRL hash directory.
314    %% Find the hashes with 'openssl crl -noout -hash -in crl.pem'.
315    CrlDir = filename:join(PrivDir, "crls"),
316    populate_crl_hash_dir(PrivDir, CrlDir,
317			  [{CA1, "b68fc624"},
318			   {CA2, "b68fc624"}],
319			 replace),
320
321    NewCA = new_ca(filename:join([PrivDir, "new_ca"]),
322		   filename:join([PrivDir, "erlangCA", "cacerts.pem"]),
323		   filename:join([PrivDir, "server", "cacerts.pem"])),
324
325    ClientOpts = proplists:get_value(crl_cache_opts, Config) ++
326	[{cacertfile, NewCA},
327	 {crl_check, Check},
328	 {verify, verify_peer}],
329
330    {ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config),
331
332    %% Neither certificate revoked; both succeed.
333    crl_verify_valid(Hostname, ServerNode, ServerOpts1, ClientNode, ClientOpts),
334    crl_verify_valid(Hostname, ServerNode, ServerOpts2, ClientNode, ClientOpts),
335
336    make_certs:revoke(PrivDir, CA1, "collision-client-1", CertsConfig),
337    populate_crl_hash_dir(PrivDir, CrlDir,
338			  [{CA1, "b68fc624"},
339			   {CA2, "b68fc624"}],
340			 replace),
341
342    %% First certificate revoked; first fails, second succeeds.
343    crl_verify_error(Hostname, ServerNode, ServerOpts1, ClientNode, ClientOpts,
344                     certificate_revoked),
345    crl_verify_valid(Hostname, ServerNode, ServerOpts2, ClientNode, ClientOpts),
346
347    make_certs:revoke(PrivDir, CA2, "collision-client-2", CertsConfig),
348    populate_crl_hash_dir(PrivDir, CrlDir,
349			  [{CA1, "b68fc624"},
350			   {CA2, "b68fc624"}],
351			 replace),
352
353    %% Second certificate revoked; both fail.
354    crl_verify_error(Hostname, ServerNode, ServerOpts1, ClientNode, ClientOpts,
355                     certificate_revoked),
356    crl_verify_error(Hostname, ServerNode, ServerOpts2, ClientNode, ClientOpts,
357                     certificate_revoked),
358
359    ok.
360
361crl_hash_dir_expired() ->
362    [{doc,"Verify ssl_crl_hash_dir behaviour with expired CRLs"}].
363crl_hash_dir_expired(Config) when is_list(Config) ->
364    PrivDir = proplists:get_value(cert_dir, Config),
365    Check = proplists:get_value(crl_check, Config),
366
367    CA = "CRL-maybe-expired-CA",
368    %% Add "issuing distribution point", to ensure that verification
369    %% fails if there is no valid CRL.
370    CertsConfig = make_certs:make_config([{issuing_distribution_point, true}]),
371    make_certs:can_generate_expired_crls(CertsConfig)
372    	orelse throw({skip, "cannot generate CRLs with expiry date in the past"}),
373    make_certs:intermediateCA(PrivDir, CA, "erlangCA", CertsConfig),
374    EndUser = "CRL-maybe-expired",
375    make_certs:enduser(PrivDir, CA, EndUser, CertsConfig),
376
377    ServerOpts =  [{keyfile, filename:join([PrivDir, EndUser, "key.pem"])},
378		   {certfile, filename:join([PrivDir, EndUser, "cert.pem"])},
379		   {cacertfile, filename:join([PrivDir, EndUser, "cacerts.pem"])}],
380    ClientOpts = proplists:get_value(crl_cache_opts, Config) ++
381	[{cacertfile, filename:join([PrivDir, CA, "cacerts.pem"])},
382	 {crl_check, Check},
383	 {verify, verify_peer}],
384    {ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config),
385
386    %% First make a CRL that will expire in one second.
387    make_certs:gencrl_sec(PrivDir, CA, CertsConfig, 1),
388    %% Sleep until the next CRL is due
389    ct:sleep({seconds, 1}),
390
391    CrlDir = filename:join(PrivDir, "crls"),
392    populate_crl_hash_dir(PrivDir, CrlDir,
393			  [{CA, "1627b4b0"}],
394			  replace),
395
396    %% Since the CRL has expired, it's treated as missing, and the
397    %% outcome depends on the crl_check setting.
398    case Check of
399        true ->
400            %% The error "revocation status undetermined" gets turned
401            %% into "bad certificate".
402            crl_verify_error(Hostname, ServerNode, ServerOpts, ClientNode, ClientOpts,
403                             bad_certificate);
404        peer ->
405            crl_verify_error(Hostname, ServerNode, ServerOpts, ClientNode, ClientOpts,
406                             bad_certificate);
407        best_effort ->
408            %% In "best effort" mode, we consider the certificate not
409            %% to be revoked if we can't find the appropriate CRL.
410            crl_verify_valid(Hostname, ServerNode, ServerOpts, ClientNode, ClientOpts)
411    end,
412
413    %% Now make a CRL that expires tomorrow.
414    make_certs:gencrl(PrivDir, CA, CertsConfig, 24),
415    CrlDir = filename:join(PrivDir, "crls"),
416    populate_crl_hash_dir(PrivDir, CrlDir,
417			  [{CA, "1627b4b0"}],
418			  add),
419
420    %% With a valid CRL, verification should always pass.
421    crl_verify_valid(Hostname, ServerNode, ServerOpts, ClientNode, ClientOpts),
422
423    ok.
424
425crl_verify_valid(Hostname, ServerNode, ServerOpts, ClientNode, ClientOpts) ->
426    Server = ssl_test_lib:start_server([{node, ServerNode}, {port, 0},
427					{from, self()},
428					{mfa, {ssl_test_lib,
429					       send_recv_result_active, []}},
430					{options, ServerOpts}]),
431    Port = ssl_test_lib:inet_port(Server),
432    Client = ssl_test_lib:start_client([{node, ClientNode}, {port, Port},
433					{host, Hostname},
434					{from, self()},
435					{mfa, {ssl_test_lib,
436					       send_recv_result_active, []}},
437					{options, ClientOpts}]),
438
439    ssl_test_lib:check_result(Client, ok,  Server, ok),
440
441    ssl_test_lib:close(Server),
442    ssl_test_lib:close(Client).
443
444crl_verify_error(Hostname, ServerNode, ServerOpts, ClientNode, ClientOpts, ExpectedAlert) ->
445    Server = ssl_test_lib:start_server_error([{node, ServerNode}, {port, 0},
446					      {from, self()},
447					      {options, ServerOpts}]),
448    Port = ssl_test_lib:inet_port(Server),
449
450    Client = ssl_test_lib:start_client_error([{node, ClientNode}, {port, Port},
451					      {host, Hostname},
452					      {from, self()},
453					      {options, ClientOpts}]),
454
455    ssl_test_lib:check_client_alert(Server, Client, ExpectedAlert).
456
457%%--------------------------------------------------------------------
458%% Internal functions ------------------------------------------------
459%%--------------------------------------------------------------------
460is_idp(idp_crl) ->
461    true;
462is_idp(_) ->
463    false.
464
465init_certs(_,v1_crl, Config)  ->
466    {[{v2_crls, false}], Config};
467init_certs(_, idp_crl, Config) ->
468    Port = proplists:get_value(httpd_port, Config),
469    {[{crl_port,Port},
470      {issuing_distribution_point, true}], Config
471    };
472init_certs(_,_,Config) ->
473    {[], Config}.
474
475make_dir_path(PathComponents) ->
476    lists:foldl(fun(F,P0) -> file:make_dir(P=filename:join(P0,F)), P end,
477		"",
478		PathComponents).
479
480rename_crl(Filename) ->
481    file:rename(Filename, Filename ++ ".notfound").
482
483populate_crl_hash_dir(CertDir, CrlDir, CAsHashes, AddOrReplace) ->
484    ok = filelib:ensure_dir(filename:join(CrlDir, "crls")),
485    case AddOrReplace of
486	replace ->
487	    %% Delete existing files, so we can override them.
488	    [ok = file:delete(FileToDelete) ||
489		{_CA, Hash} <- CAsHashes,
490		FileToDelete <- filelib:wildcard(
491				  filename:join(CrlDir, Hash ++ ".r*"))];
492	add ->
493	    ok
494    end,
495    %% Create new files, incrementing suffix if needed to find unique names.
496    [{ok, _} =
497	 file:copy(filename:join([CertDir, CA, "crl.pem"]),
498		   find_free_name(CrlDir, Hash, 0))
499     || {CA, Hash} <- CAsHashes],
500    ok.
501
502find_free_name(CrlDir, Hash, N) ->
503    Name = filename:join(CrlDir, Hash ++ ".r" ++ integer_to_list(N)),
504    case filelib:is_file(Name) of
505	true ->
506	    find_free_name(CrlDir, Hash, N + 1);
507	false ->
508	    Name
509    end.
510
511new_ca(FileName, CA1, CA2) ->
512    {ok, P1} = file:read_file(CA1),
513    E1 = public_key:pem_decode(P1),
514    {ok, P2} = file:read_file(CA2),
515    E2 = public_key:pem_decode(P2),
516    Pem = public_key:pem_encode(E1 ++E2),
517    file:write_file(FileName,  Pem),
518    FileName.
519