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