From 8991663e33313233fed9cb7b0149baec1aa32b3f Mon Sep 17 00:00:00 2001 From: Andrew Thompson Date: Thu, 10 Oct 2013 02:23:02 -0400 Subject: Various improvements to CRL handling * Handle v1 CRLs, with no extensions. * Compare the IDP on a CRL correctly, if present * Don't try to double-decode altnames Tests are also included, and the make_certs testing tool in the SSL application has been greatly extended. --- lib/public_key/src/pubkey_cert.erl | 6 +- lib/public_key/src/pubkey_crl.erl | 13 +- lib/ssl/test/Makefile | 1 + lib/ssl/test/make_certs.erl | 310 +++++++++++++++------- lib/ssl/test/ssl_crl_SUITE.erl | 520 +++++++++++++++++++++++++++++++++++++ lib/ssl/test/ssl_test_lib.erl | 8 +- 6 files changed, 764 insertions(+), 94 deletions(-) create mode 100644 lib/ssl/test/ssl_crl_SUITE.erl diff --git a/lib/public_key/src/pubkey_cert.erl b/lib/public_key/src/pubkey_cert.erl index d9288e99bb..07469e342f 100644 --- a/lib/public_key/src/pubkey_cert.erl +++ b/lib/public_key/src/pubkey_cert.erl @@ -319,6 +319,8 @@ verify_fun(Otpcert, Result, UserState0, VerifyFun) -> %% %% Description: Extracts a specific extension from a list of extensions. %%-------------------------------------------------------------------- +select_extension(_, asn1_NOVALUE) -> + undefined; select_extension(_, []) -> undefined; select_extension(Id, [#'Extension'{extnID = Id} = Extension | _]) -> @@ -341,8 +343,8 @@ match_name(uniformResourceIdentifier, URI, [PermittedName | Rest]) -> case split_uri(URI) of incomplete -> false; - {_, _, Host, _, _} -> - match_name(fun is_valid_host_or_domain/2, Host, + {_, _, _Host, _, _} -> + match_name(fun is_valid_host_or_domain/2, URI, PermittedName, Rest) end; diff --git a/lib/public_key/src/pubkey_crl.erl b/lib/public_key/src/pubkey_crl.erl index eaba5bfa1b..503d0205a6 100644 --- a/lib/public_key/src/pubkey_crl.erl +++ b/lib/public_key/src/pubkey_crl.erl @@ -39,7 +39,13 @@ validate(OtpCert, OtherDPCRLs, DP, {DerCRL, CRL}, {DerDeltaCRL, DeltaCRL}, CertIssuer = TBSCert#'OTPTBSCertificate'.issuer, TBSCRL = CRL#'CertificateList'.tbsCertList, CRLIssuer = TBSCRL#'TBSCertList'.issuer, - AltNames = subject_alt_names(TBSCert#'OTPTBSCertificate'.extensions), + AltNames = case pubkey_cert:select_extension(?'id-ce-subjectAltName', + TBSCert#'OTPTBSCertificate'.extensions) of + undefined -> + []; + Ext -> + Ext#'Extension'.extnValue + end, revoked_status(DP, IDP, {directoryName, CRLIssuer}, [ {directoryName, CertIssuer} | AltNames], SerialNumber, Revoked, DeltaRevoked, RevokedState1); @@ -401,7 +407,8 @@ match_one([{Type, Name} | Names], CandidateNames) -> case Candidates of [] -> false; - [_|_] -> case pubkey_cert:match_name(Type, Name, Candidates) of + [_|_] -> + case pubkey_cert:match_name(Type, Name, Candidates) of true -> true; false -> @@ -664,6 +671,8 @@ verify_extensions([#'TBSCertList_revokedCertificates_SEQOF'{crlEntryExtensions = verify_extensions(pubkey_cert:extensions_list(Ext)) and verify_extensions(Rest); verify_extensions([]) -> true; +verify_extensions(asn1_NOVALUE) -> + true; verify_extensions([#'Extension'{critical = true, extnID = Id} | Rest]) -> case lists:member(Id, [?'id-ce-authorityKeyIdentifier', ?'id-ce-issuerAltName', diff --git a/lib/ssl/test/Makefile b/lib/ssl/test/Makefile index 244eb5ce0a..2f8ff6f04e 100644 --- a/lib/ssl/test/Makefile +++ b/lib/ssl/test/Makefile @@ -39,6 +39,7 @@ MODULES = \ ssl_basic_SUITE \ ssl_cipher_SUITE \ ssl_certificate_verify_SUITE\ + ssl_crl_SUITE\ ssl_dist_SUITE \ ssl_handshake_SUITE \ ssl_npn_hello_SUITE \ diff --git a/lib/ssl/test/make_certs.erl b/lib/ssl/test/make_certs.erl index 4603a9f846..2b4eaaef53 100644 --- a/lib/ssl/test/make_certs.erl +++ b/lib/ssl/test/make_certs.erl @@ -18,23 +18,68 @@ %% -module(make_certs). +-compile([export_all]). --export([all/2]). +%-export([all/1, all/2, rootCA/2, intermediateCA/3, endusers/3, enduser/3, revoke/3, gencrl/2, verify/3]). --record(dn, {commonName, +-record(config, {commonName, organizationalUnitName = "Erlang OTP", organizationName = "Ericsson AB", localityName = "Stockholm", countryName = "SE", - emailAddress = "peter@erix.ericsson.se"}). + emailAddress = "peter@erix.ericsson.se", + default_bits = 2048, + v2_crls = true, + ecc_certs = false, + issuing_distribution_point = false, + openssl_cmd = "openssl"}). + + +default_config() -> + #config{}. + +make_config(Args) -> + make_config(Args, #config{}). + +make_config([], C) -> + C; +make_config([{organizationalUnitName, Name}|T], C) when is_list(Name) -> + make_config(T, C#config{organizationalUnitName = Name}); +make_config([{organizationName, Name}|T], C) when is_list(Name) -> + make_config(T, C#config{organizationName = Name}); +make_config([{localityName, Name}|T], C) when is_list(Name) -> + make_config(T, C#config{localityName = Name}); +make_config([{countryName, Name}|T], C) when is_list(Name) -> + make_config(T, C#config{countryName = Name}); +make_config([{emailAddress, Name}|T], C) when is_list(Name) -> + make_config(T, C#config{emailAddress = Name}); +make_config([{default_bits, Bits}|T], C) when is_integer(Bits) -> + make_config(T, C#config{default_bits = Bits}); +make_config([{v2_crls, Bool}|T], C) when is_boolean(Bool) -> + make_config(T, C#config{v2_crls = Bool}); +make_config([{ecc_certs, Bool}|T], C) when is_boolean(Bool) -> + make_config(T, C#config{ecc_certs = Bool}); +make_config([{issuing_distribution_point, Bool}|T], C) when is_boolean(Bool) -> + make_config(T, C#config{issuing_distribution_point = Bool}); +make_config([{openssl_cmd, Cmd}|T], C) when is_list(Cmd) -> + make_config(T, C#config{openssl_cmd = Cmd}). + + +all([DataDir, PrivDir]) -> + all(DataDir, PrivDir). all(DataDir, PrivDir) -> - OpenSSLCmd = "openssl", + all(DataDir, PrivDir, #config{}). + +all(DataDir, PrivDir, C) when is_list(C) -> + all(DataDir, PrivDir, make_config(C)); +all(DataDir, PrivDir, C = #config{}) -> + ok = filelib:ensure_dir(filename:join(PrivDir, "erlangCA")), create_rnd(DataDir, PrivDir), % For all requests - rootCA(PrivDir, OpenSSLCmd, "erlangCA"), - intermediateCA(PrivDir, OpenSSLCmd, "otpCA", "erlangCA"), - endusers(PrivDir, OpenSSLCmd, "otpCA", ["client", "server"]), - collect_certs(PrivDir, ["erlangCA", "otpCA"], ["client", "server"]), + rootCA(PrivDir, "erlangCA", C), + intermediateCA(PrivDir, "otpCA", "erlangCA", C), + endusers(PrivDir, "otpCA", ["client", "server", "revoked"], C), + endusers(PrivDir, "erlangCA", ["localhost"], C), %% Create keycert files SDir = filename:join([PrivDir, "server"]), SC = filename:join([SDir, "cert.pem"]), @@ -46,7 +91,14 @@ all(DataDir, PrivDir) -> CK = filename:join([CDir, "key.pem"]), CKC = filename:join([CDir, "keycert.pem"]), append_files([CK, CC], CKC), - remove_rnd(PrivDir). + RDir = filename:join([PrivDir, "revoked"]), + RC = filename:join([RDir, "cert.pem"]), + RK = filename:join([RDir, "key.pem"]), + RKC = filename:join([RDir, "keycert.pem"]), + revoke(PrivDir, "otpCA", "revoked", C), + append_files([RK, RC], RKC), + remove_rnd(PrivDir), + {ok, C}. append_files(FileNames, ResultFileName) -> {ok, ResultFile} = file:open(ResultFileName, [write]), @@ -59,111 +111,176 @@ do_append_files([F|Fs], RF) -> ok = file:write(RF, Data), do_append_files(Fs, RF). -rootCA(Root, OpenSSLCmd, Name) -> - create_ca_dir(Root, Name, ca_cnf(Name)), - DN = #dn{commonName = Name}, - create_self_signed_cert(Root, OpenSSLCmd, Name, req_cnf(DN)), - ok. +rootCA(Root, Name, C) -> + create_ca_dir(Root, Name, ca_cnf(C#config{commonName = Name})), + create_self_signed_cert(Root, Name, req_cnf(C#config{commonName = Name}), C), + file:copy(filename:join([Root, Name, "cert.pem"]), filename:join([Root, Name, "cacerts.pem"])), + gencrl(Root, Name, C). -intermediateCA(Root, OpenSSLCmd, CA, ParentCA) -> - CA = "otpCA", - create_ca_dir(Root, CA, ca_cnf(CA)), +intermediateCA(Root, CA, ParentCA, C) -> + create_ca_dir(Root, CA, ca_cnf(C#config{commonName = CA})), CARoot = filename:join([Root, CA]), - DN = #dn{commonName = CA}, CnfFile = filename:join([CARoot, "req.cnf"]), - file:write_file(CnfFile, req_cnf(DN)), + file:write_file(CnfFile, req_cnf(C#config{commonName = CA})), KeyFile = filename:join([CARoot, "private", "key.pem"]), ReqFile = filename:join([CARoot, "req.pem"]), - create_req(Root, OpenSSLCmd, CnfFile, KeyFile, ReqFile), + create_req(Root, CnfFile, KeyFile, ReqFile, C), CertFile = filename:join([CARoot, "cert.pem"]), - sign_req(Root, OpenSSLCmd, ParentCA, "ca_cert", ReqFile, CertFile). - -endusers(Root, OpenSSLCmd, CA, Users) -> - lists:foreach(fun(User) -> enduser(Root, OpenSSLCmd, CA, User) end, Users). - -enduser(Root, OpenSSLCmd, CA, User) -> + sign_req(Root, ParentCA, "ca_cert", ReqFile, CertFile, C), + CACertsFile = filename:join(CARoot, "cacerts.pem"), + file:copy(filename:join([Root, ParentCA, "cacerts.pem"]), CACertsFile), + %% append this CA's cert to the cacerts file + {ok, Bin} = file:read_file(CertFile), + {ok, FD} = file:open(CACertsFile, [append]), + file:write(FD, ["\n", Bin]), + file:close(FD), + gencrl(Root, CA, C). + +endusers(Root, CA, Users, C) -> + [enduser(Root, CA, User, C) || User <- Users]. + +enduser(Root, CA, User, C) -> UsrRoot = filename:join([Root, User]), file:make_dir(UsrRoot), CnfFile = filename:join([UsrRoot, "req.cnf"]), - DN = #dn{commonName = User}, - file:write_file(CnfFile, req_cnf(DN)), + file:write_file(CnfFile, req_cnf(C#config{commonName = User})), KeyFile = filename:join([UsrRoot, "key.pem"]), ReqFile = filename:join([UsrRoot, "req.pem"]), - create_req(Root, OpenSSLCmd, CnfFile, KeyFile, ReqFile), + create_req(Root, CnfFile, KeyFile, ReqFile, C), + %create_req(Root, CnfFile, KeyFile, ReqFile), CertFileAllUsage = filename:join([UsrRoot, "cert.pem"]), - sign_req(Root, OpenSSLCmd, CA, "user_cert", ReqFile, CertFileAllUsage), + sign_req(Root, CA, "user_cert", ReqFile, CertFileAllUsage, C), CertFileDigitalSigOnly = filename:join([UsrRoot, "digital_signature_only_cert.pem"]), - sign_req(Root, OpenSSLCmd, CA, "user_cert_digital_signature_only", ReqFile, CertFileDigitalSigOnly). - -collect_certs(Root, CAs, Users) -> - Bins = lists:foldr( - fun(CA, Acc) -> - File = filename:join([Root, CA, "cert.pem"]), - {ok, Bin} = file:read_file(File), - [Bin, "\n" | Acc] - end, [], CAs), - lists:foreach( - fun(User) -> - File = filename:join([Root, User, "cacerts.pem"]), - file:write_file(File, Bins) - end, Users). + sign_req(Root, CA, "user_cert_digital_signature_only", ReqFile, CertFileDigitalSigOnly, C), + CACertsFile = filename:join(UsrRoot, "cacerts.pem"), + file:copy(filename:join([Root, CA, "cacerts.pem"]), CACertsFile), + ok. + +revoke(Root, CA, User, C) -> + UsrCert = filename:join([Root, User, "cert.pem"]), + CACnfFile = filename:join([Root, CA, "ca.cnf"]), + Cmd = [C#config.openssl_cmd, " ca" + " -revoke ", UsrCert, + [" -crl_reason keyCompromise" || C#config.v2_crls ], + " -config ", CACnfFile], + Env = [{"ROOTDIR", filename:absname(Root)}], + cmd(Cmd, Env), + gencrl(Root, CA, C). + +gencrl(Root, CA, C) -> + CACnfFile = filename:join([Root, CA, "ca.cnf"]), + CACRLFile = filename:join([Root, CA, "crl.pem"]), + Cmd = [C#config.openssl_cmd, " ca" + " -gencrl ", + " -crlhours 24", + " -out ", CACRLFile, + " -config ", CACnfFile], + Env = [{"ROOTDIR", filename:absname(Root)}], + cmd(Cmd, Env). -create_self_signed_cert(Root, OpenSSLCmd, CAName, Cnf) -> +verify(Root, CA, User, C) -> + CAFile = filename:join([Root, User, "cacerts.pem"]), + CACRLFile = filename:join([Root, CA, "crl.pem"]), + CertFile = filename:join([Root, User, "cert.pem"]), + Cmd = [C#config.openssl_cmd, " verify" + " -CAfile ", CAFile, + " -CRLfile ", CACRLFile, %% this is undocumented, but seems to work + " -crl_check ", + CertFile], + Env = [{"ROOTDIR", filename:absname(Root)}], + try cmd(Cmd, Env) catch + exit:{eval_cmd, _, _} -> + invalid + end. + +create_self_signed_cert(Root, CAName, Cnf, C = #config{ecc_certs = true}) -> CARoot = filename:join([Root, CAName]), CnfFile = filename:join([CARoot, "req.cnf"]), file:write_file(CnfFile, Cnf), KeyFile = filename:join([CARoot, "private", "key.pem"]), CertFile = filename:join([CARoot, "cert.pem"]), - Cmd = [OpenSSLCmd, " req" + Cmd = [C#config.openssl_cmd, " ecparam" + " -out ", KeyFile, + " -name secp521r1 ", + %" -name sect283k1 ", + " -genkey "], + Env = [{"ROOTDIR", filename:absname(Root)}], + cmd(Cmd, Env), + + Cmd2 = [C#config.openssl_cmd, " req" " -new" " -x509" " -config ", CnfFile, - " -keyout ", KeyFile, + " -key ", KeyFile, + " -outform PEM ", " -out ", CertFile], - Env = [{"ROOTDIR", Root}], - cmd(Cmd, Env), - fix_key_file(OpenSSLCmd, KeyFile). - -% openssl 1.0 generates key files in pkcs8 format by default and we don't handle this format -fix_key_file(OpenSSLCmd, KeyFile) -> - KeyFileTmp = KeyFile ++ ".tmp", - Cmd = [OpenSSLCmd, " rsa", - " -in ", - KeyFile, - " -out ", - KeyFileTmp], - cmd(Cmd, []), - ok = file:rename(KeyFileTmp, KeyFile). + cmd(Cmd2, Env); +create_self_signed_cert(Root, CAName, Cnf, C) -> + CARoot = filename:join([Root, CAName]), + CnfFile = filename:join([CARoot, "req.cnf"]), + file:write_file(CnfFile, Cnf), + KeyFile = filename:join([CARoot, "private", "key.pem"]), + CertFile = filename:join([CARoot, "cert.pem"]), + Cmd = [C#config.openssl_cmd, " req" + " -new" + " -x509" + " -config ", CnfFile, + " -keyout ", KeyFile, + " -outform PEM", + " -out ", CertFile], + Env = [{"ROOTDIR", filename:absname(Root)}], + cmd(Cmd, Env). + create_ca_dir(Root, CAName, Cnf) -> CARoot = filename:join([Root, CAName]), + ok = filelib:ensure_dir(CARoot), file:make_dir(CARoot), create_dirs(CARoot, ["certs", "crl", "newcerts", "private"]), create_rnd(Root, filename:join([CAName, "private"])), create_files(CARoot, [{"serial", "01\n"}, + {"crlnumber", "01"}, {"index.txt", ""}, {"ca.cnf", Cnf}]). -create_req(Root, OpenSSLCmd, CnfFile, KeyFile, ReqFile) -> - Cmd = [OpenSSLCmd, " req" +create_req(Root, CnfFile, KeyFile, ReqFile, C = #config{ecc_certs = true}) -> + Cmd = [C#config.openssl_cmd, " ecparam" + " -out ", KeyFile, + " -name secp521r1 ", + %" -name sect283k1 ", + " -genkey "], + Env = [{"ROOTDIR", filename:absname(Root)}], + cmd(Cmd, Env), + Cmd2 = [C#config.openssl_cmd, " req" + " -new ", + " -key ", KeyFile, + " -outform PEM ", + " -out ", ReqFile, + " -config ", CnfFile], + cmd(Cmd2, Env); + %fix_key_file(KeyFile). +create_req(Root, CnfFile, KeyFile, ReqFile, C) -> + Cmd = [C#config.openssl_cmd, " req" " -new" " -config ", CnfFile, + " -outform PEM ", " -keyout ", KeyFile, " -out ", ReqFile], - Env = [{"ROOTDIR", Root}], - cmd(Cmd, Env), - fix_key_file(OpenSSLCmd, KeyFile). + Env = [{"ROOTDIR", filename:absname(Root)}], + cmd(Cmd, Env). + %fix_key_file(KeyFile). + -sign_req(Root, OpenSSLCmd, CA, CertType, ReqFile, CertFile) -> +sign_req(Root, CA, CertType, ReqFile, CertFile, C) -> CACnfFile = filename:join([Root, CA, "ca.cnf"]), - Cmd = [OpenSSLCmd, " ca" + Cmd = [C#config.openssl_cmd, " ca" " -batch" " -notext" " -config ", CACnfFile, " -extensions ", CertType, " -in ", ReqFile, " -out ", CertFile], - Env = [{"ROOTDIR", Root}], + Env = [{"ROOTDIR", filename:absname(Root)}], cmd(Cmd, Env). %% @@ -194,19 +311,19 @@ cmd(Cmd, Env) -> FCmd = lists:flatten(Cmd), Port = open_port({spawn, FCmd}, [stream, eof, exit_status, stderr_to_stdout, {env, Env}]), - eval_cmd(Port). + eval_cmd(Port, FCmd). -eval_cmd(Port) -> +eval_cmd(Port, Cmd) -> receive {Port, {data, _}} -> - eval_cmd(Port); + eval_cmd(Port, Cmd); {Port, eof} -> ok end, receive {Port, {exit_status, Status}} when Status /= 0 -> %% io:fwrite("exit status: ~w~n", [Status]), - exit({eval_cmd, Status}) + exit({eval_cmd, Cmd, Status}) after 0 -> ok end. @@ -215,7 +332,7 @@ eval_cmd(Port) -> %% Contents of configuration files %% -req_cnf(DN) -> +req_cnf(C) -> ["# Purpose: Configuration for requests (end users and CAs)." "\n" "ROOTDIR = $ENV::ROOTDIR\n" @@ -224,7 +341,7 @@ req_cnf(DN) -> "[req]\n" "input_password = secret\n" "output_password = secret\n" - "default_bits = 1024\n" + "default_bits = ", integer_to_list(C#config.default_bits), "\n" "RANDFILE = $ROOTDIR/RAND\n" "encrypt_key = no\n" "default_md = sha1\n" @@ -235,12 +352,12 @@ req_cnf(DN) -> "\n" "[name]\n" - "commonName = ", DN#dn.commonName, "\n" - "organizationalUnitName = ", DN#dn.organizationalUnitName, "\n" - "organizationName = ", DN#dn.organizationName, "\n" - "localityName = ", DN#dn.localityName, "\n" - "countryName = ", DN#dn.countryName, "\n" - "emailAddress = ", DN#dn.emailAddress, "\n" + "commonName = ", C#config.commonName, "\n" + "organizationalUnitName = ", C#config.organizationalUnitName, "\n" + "organizationName = ", C#config.organizationName, "\n" + "localityName = ", C#config.localityName, "\n" + "countryName = ", C#config.countryName, "\n" + "emailAddress = ", C#config.emailAddress, "\n" "\n" "[ca_ext]\n" @@ -249,8 +366,7 @@ req_cnf(DN) -> "subjectKeyIdentifier = hash\n" "subjectAltName = email:copy\n"]. - -ca_cnf(CA) -> +ca_cnf(C) -> ["# Purpose: Configuration for CAs.\n" "\n" "ROOTDIR = $ENV::ROOTDIR\n" @@ -258,21 +374,23 @@ ca_cnf(CA) -> "\n" "[ca]\n" - "dir = $ROOTDIR/", CA, "\n" + "dir = $ROOTDIR/", C#config.commonName, "\n" "certs = $dir/certs\n" "crl_dir = $dir/crl\n" "database = $dir/index.txt\n" "new_certs_dir = $dir/newcerts\n" "certificate = $dir/cert.pem\n" "serial = $dir/serial\n" - "crl = $dir/crl.pem\n" + "crl = $dir/crl.pem\n", + ["crlnumber = $dir/crlnumber\n" || C#config.v2_crls], "private_key = $dir/private/key.pem\n" "RANDFILE = $dir/private/RAND\n" "\n" - "x509_extensions = user_cert\n" + "x509_extensions = user_cert\n", + ["crl_extensions = crl_ext\n" || C#config.v2_crls], "unique_subject = no\n" "default_days = 3600\n" - "default_md = sha1\n" + "default_md = sha256\n" "preserve = no\n" "policy = policy_match\n" "\n" @@ -286,6 +404,14 @@ ca_cnf(CA) -> "emailAddress = supplied\n" "\n" + "[crl_ext]\n" + "authorityKeyIdentifier=keyid:always,issuer:always\n", + ["issuingDistributionPoint=critical, @idpsec\n" || C#config.issuing_distribution_point], + + "[idpsec]\n" + "fullname=URI:http://localhost:8000/",C#config.commonName,"/crl.pem\n" + %"fullname=URI:file://$ROOTDIR/",C#config.commonName,"/crl.pem\n" + "[user_cert]\n" "basicConstraints = CA:false\n" "keyUsage = nonRepudiation, digitalSignature, keyEncipherment\n" @@ -293,6 +419,11 @@ ca_cnf(CA) -> "authorityKeyIdentifier = keyid,issuer:always\n" "subjectAltName = email:copy\n" "issuerAltName = issuer:copy\n" + "crlDistributionPoints=@crl_section\n" + + "[crl_section]\n" + "URI.1=file://$ROOTDIR/",C#config.commonName,"/crl.pem\n" + "URI.2=http://localhost:8000/",C#config.commonName,"/crl.pem\n" "\n" "[user_cert_digital_signature_only]\n" @@ -310,4 +441,7 @@ ca_cnf(CA) -> "subjectKeyIdentifier = hash\n" "authorityKeyIdentifier = keyid:always,issuer:always\n" "subjectAltName = email:copy\n" - "issuerAltName = issuer:copy\n"]. + "issuerAltName = issuer:copy\n" + "crlDistributionPoints=@crl_section\n" + ]. + diff --git a/lib/ssl/test/ssl_crl_SUITE.erl b/lib/ssl/test/ssl_crl_SUITE.erl new file mode 100644 index 0000000000..8afd926ee6 --- /dev/null +++ b/lib/ssl/test/ssl_crl_SUITE.erl @@ -0,0 +1,520 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2013. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% +%% + +-module(ssl_crl_SUITE). + +%% Note: This directive should only be used in test suites. +-compile(export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("public_key/include/public_key.hrl"). + +-define(TIMEOUT, 120000). +-define(LONG_TIMEOUT, 600000). +-define(SLEEP, 1000). +-define(OPENSSL_RENEGOTIATE, "R\n"). +-define(OPENSSL_QUIT, "Q\n"). +-define(OPENSSL_GARBAGE, "P\n"). +-define(EXPIRE, 10). + +%%-------------------------------------------------------------------- +%% Common Test interface functions ----------------------------------- +%%-------------------------------------------------------------------- + +suite() -> [{ct_hooks,[ts_install_cth]}]. + +all() -> + [ + {group, basic}, + {group, v1_crl}, + {group, idp_crl} + ]. + +groups() -> + [{basic, [], basic_tests()}, + {v1_crl, [], v1_crl_tests()}, + {idp_crl, [], idp_crl_tests()}]. + +basic_tests() -> + [crl_verify_valid, crl_verify_revoked]. + +v1_crl_tests() -> + [crl_verify_valid, crl_verify_revoked]. + +idp_crl_tests() -> + [crl_verify_valid, crl_verify_revoked]. + +init_per_suite(Config0) -> + ct:log("wtf~n"), + io:format("Config ~p~n", [Config0]), + Dog = ct:timetrap(?LONG_TIMEOUT *2), + case os:find_executable("openssl") of + false -> + {skip, "Openssl not found"}; + _ -> + catch crypto:stop(), + try crypto:start() of + ok -> + ssl:start(), + [{watchdog, Dog} | Config0] + catch _:_ -> + {skip, "Crypto did not start"} + end + end. + +end_per_suite(_Config) -> + ssl:stop(), + application:stop(crypto). + +init_per_group(v1_crl, Config) -> + ssl:start(), + CertDir = filename:join(?config(priv_dir, Config), "v1_crl"), + Result = + (catch make_certs:all(?config(data_dir, Config), + CertDir, + [{v2_crls, false}])), + ct:log("Make certs ~p~n", [Result]), + [{make_cert_result, Result}, {cert_dir, CertDir} | Config]; +init_per_group(idp_crl, Config) -> + ssl:start(), + CertDir = filename:join(?config(priv_dir, Config), "idp_crl"), + Result = + (catch make_certs:all(?config(data_dir, Config), + CertDir, + [{issuing_distribution_point, true}])), + ct:log("Make certs ~p~n", [Result]), + %% start a HTTP server to serve the CRLs + {ok, Httpd} = inets:start(httpd, [{port, 8000}, {server_name, "localhost"}, + {server_root, "/tmp"}, + {document_root, CertDir}, + {modules, [mod_get]}]), + [{make_cert_result, Result}, {cert_dir, CertDir}, {httpd, Httpd} | Config]; +init_per_group(_GroupName, Config) -> + ssl:start(), + CertDir = ?config(priv_dir, Config), + Result = + (catch make_certs:all(?config(data_dir, Config), + CertDir)), + ct:log("Make certs ~p~n", [Result]), + [{make_cert_result, Result}, {cert_dir, CertDir} | Config]. + +end_per_group(_GroupName, Config) -> + case ?config(httpd, Config) of + undefined -> ok; + Pid -> + ok = inets:stop(httpd, Pid) + end, + Config. + +crl_verify_valid() -> + [{doc,"Verify a simple valid CRL chain"}]. +crl_verify_valid(Config) when is_list(Config) -> + process_flag(trap_exit, true), + %ServerOpts = ?config(server_opts, Config), + + ct:log("server opts ~p~n", [Config]), + + PrivDir = ?config(cert_dir, Config), + + ServerOpts = [{keyfile, filename:join([PrivDir, "server", "key.pem"])}, + {certfile, filename:join([PrivDir, "server", "cert.pem"])}, + {cacertfile, filename:join([PrivDir, "server", "cacerts.pem"])}], + + {ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config), + + Data = "From openssl to erlang", + + Server = ssl_test_lib:start_server([{node, ServerNode}, {port, 0}, + {from, self()}, + {mfa, {?MODULE, erlang_ssl_receive, [Data]}}, + %{mfa, {ssl_test_lib, no_result, []}}, + {options, ServerOpts}]), + Port = ssl_test_lib:inet_port(Server), + + CACerts = load_cert(filename:join([PrivDir, "erlangCA", "cacerts.pem"])), + + ClientOpts = [{cacerts, CACerts}, + {verify, verify_peer}, + {verify_fun, {fun validate_function/3, {CACerts, []}}}], + + + Client = ssl_test_lib:start_client([{node, ClientNode}, {port, Port}, + {host, Hostname}, + {from, self()}, + {mfa, {?MODULE, + erlang_ssl_send, [Data]}}, + %{mfa, {ssl_test_lib, no_result, []}}, + {options, ClientOpts}]), + ssl_test_lib:check_result(Client, ok), + ssl_test_lib:check_result(Server, ok), + + %% Clean close down! Server needs to be closed first !! + ssl_test_lib:close(Server), + ssl_test_lib:close(Client), + process_flag(trap_exit, false). + +crl_verify_revoked() -> + [{doc,"Verify a simple valid CRL chain"}]. +crl_verify_revoked(Config) when is_list(Config) -> + process_flag(trap_exit, true), + %ServerOpts = ?config(server_opts, Config), + + ct:log("server opts ~p~n", [Config]), + + PrivDir = ?config(cert_dir, Config), + + ServerOpts = [{keyfile, filename:join([PrivDir, "revoked", "key.pem"])}, + {certfile, filename:join([PrivDir, "revoked", "cert.pem"])}, + {cacertfile, filename:join([PrivDir, "revoked", "cacerts.pem"])}], + + {ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config), + + Data = "From openssl to erlang", + + Server = ssl_test_lib:start_server([{node, ServerNode}, {port, 0}, + {from, self()}, + %{mfa, {?MODULE, erlang_ssl_receive, [Data]}}, + {mfa, {ssl_test_lib, no_result, []}}, + {options, ServerOpts}]), + Port = ssl_test_lib:inet_port(Server), + + CACerts = load_cert(filename:join([PrivDir, "erlangCA", "cacerts.pem"])), + + ClientOpts = [{cacerts, CACerts}, + {verify, verify_peer}, + {verify_fun, {fun validate_function/3, {CACerts, []}}}], + + + {connect_failed, _} = ssl_test_lib:start_client([{node, ClientNode}, {port, Port}, + {host, Hostname}, + {from, self()}, + %{mfa, {?MODULE, + %erlang_ssl_receive, [Data]}}, + {mfa, {ssl_test_lib, no_result, []}}, + {options, ClientOpts}]), + + %% Clean close down! Server needs to be closed first !! + ssl_test_lib:close(Server), + process_flag(trap_exit, false). + +erlang_ssl_receive(Socket, Data) -> + ct:log("Connection info: ~p~n", + [ssl:connection_info(Socket)]), + receive + {ssl, Socket, Data} -> + io:format("Received ~p~n",[Data]), + %% open_ssl server sometimes hangs waiting in blocking read + ssl:send(Socket, "Got it"), + ok; + {ssl, Socket, Byte} when length(Byte) == 1 -> + erlang_ssl_receive(Socket, tl(Data)); + {Port, {data,Debug}} when is_port(Port) -> + io:format("openssl ~s~n",[Debug]), + erlang_ssl_receive(Socket,Data); + Other -> + ct:fail({unexpected_message, Other}) + after 4000 -> + ct:fail({did_not_get, Data}) + end. + + +erlang_ssl_send(Socket, Data) -> + ct:log("Connection info: ~p~n", + [ssl:connection_info(Socket)]), + ssl:send(Socket, Data), + ok. + +load_certs(undefined) -> + undefined; +load_certs(CertDir) -> + case file:list_dir(CertDir) of + {ok, Certs} -> + load_certs(lists:map(fun(Cert) -> filename:join(CertDir, Cert) + end, Certs), []); + {error, _} -> + undefined + end. + +load_certs([], Acc) -> + io:format("Successfully loaded ~p CA certificates~n", [length(Acc)]), + Acc; +load_certs([Cert|Certs], Acc) -> + case filelib:is_dir(Cert) of + true -> + load_certs(Certs, Acc); + _ -> + %io:format("Loading certificate ~p~n", [Cert]), + load_certs(Certs, load_cert(Cert) ++ Acc) + end. + +load_cert(Cert) -> + {ok, Bin} = file:read_file(Cert), + case filename:extension(Cert) of + ".der" -> + %% no decoding necessary + [Bin]; + _ -> + %% assume PEM otherwise + Contents = public_key:pem_decode(Bin), + [DER || {Type, DER, Cipher} <- Contents, Type == 'Certificate', Cipher == 'not_encrypted'] + end. + +%% @doc Validator function for SSL negotiation. +%% +validate_function(Cert, valid_peer, State) -> + io:format("validing peer ~p with ~p intermediate certs~n", + [get_common_name(Cert), + length(element(2, State))]), + %% peer certificate validated, now check the CRL + Res = (catch check_crl(Cert, State)), + io:format("CRL validate result for ~p: ~p~n", + [get_common_name(Cert), Res]), + {Res, State}; +validate_function(Cert, valid, {TrustedCAs, IntermediateCerts}=State) -> + case public_key:pkix_is_self_signed(Cert) of + true -> + io:format("root certificate~n"), + %% this is a root cert, no CRL + {valid, {TrustedCAs, [Cert|IntermediateCerts]}}; + false -> + %% check is valid CA certificate, add to the list of + %% intermediates + Res = (catch check_crl(Cert, State)), + io:format("CRL intermediate CA validate result for ~p: ~p~n", + [get_common_name(Cert), Res]), + {Res, {TrustedCAs, [Cert|IntermediateCerts]}} + end; +validate_function(_Cert, _Event, State) -> + %io:format("ignoring event ~p~n", [_Event]), + {valid, State}. + +%% @doc Given a certificate, find CRL distribution points for the given +%% certificate, fetch, and attempt to validate each CRL through +%% issuer_function/4. +%% +check_crl(Cert, State) -> + %% pull the CRL distribution point(s) out of the certificate, if any + case pubkey_cert:select_extension(?'id-ce-cRLDistributionPoints', + pubkey_cert:extensions_list(Cert#'OTPCertificate'.tbsCertificate#'OTPTBSCertificate'.extensions)) of + undefined -> + io:format("no CRL distribution points for ~p~n", + [get_common_name(Cert)]), + %% fail; we can't validate if there's no CRL + no_crl; + CRLExtension -> + CRLDistPoints = CRLExtension#'Extension'.extnValue, + DPointsAndCRLs = lists:foldl(fun(Point, Acc) -> + %% try to read the CRL over http or from a + %% local file + case fetch_point(Point) of + not_available -> + Acc; + Res -> + [{Point, Res} | Acc] + end + end, [], CRLDistPoints), + public_key:pkix_crls_validate(Cert, + DPointsAndCRLs, + [{issuer_fun, + {fun issuer_function/4, State}}]) + end. + +%% @doc Given a list of distribution points for CRLs, certificates and +%% both trusted and intermediary certificates, attempt to build and +%% authority chain back via build_chain to verify that it is valid. +%% +issuer_function(_DP, CRL, _Issuer, {TrustedCAs, IntermediateCerts}) -> + %% XXX the 'Issuer' we get passed here is the AuthorityKeyIdentifier, + %% which we are not currently smart enough to understand + %% Read the CA certs out of the file + Certs = [public_key:pkix_decode_cert(DER, otp) || DER <- TrustedCAs], + %% get the real issuer out of the CRL + Issuer = public_key:pkix_normalize_name( + pubkey_cert_records:transform( + CRL#'CertificateList'.tbsCertList#'TBSCertList'.issuer, decode)), + %% assume certificates are ordered from root to tip + case find_issuer(Issuer, IntermediateCerts ++ Certs) of + undefined -> + io:format("unable to find certificate matching CRL issuer ~p~n", + [Issuer]), + error; + IssuerCert -> + case build_chain({public_key:pkix_encode('OTPCertificate', + IssuerCert, + otp), + IssuerCert}, IntermediateCerts, Certs, []) of + undefined -> + error; + {OTPCert, Path} -> + {ok, OTPCert, Path} + end + end. + +%% @doc Attempt to build authority chain back using intermediary +%% certificates, falling back on trusted certificates if the +%% intermediary chain of certificates does not fully extend to the +%% root. +%% +%% Returns: {RootCA :: #OTPCertificate{}, Chain :: [der_encoded()]} +%% +build_chain({DER, Cert}, IntCerts, TrustedCerts, Acc) -> + %% check if this cert is self-signed, if it is, we've reached the + %% root of the chain + Issuer = public_key:pkix_normalize_name( + Cert#'OTPCertificate'.tbsCertificate#'OTPTBSCertificate'.issuer), + Subject = public_key:pkix_normalize_name( + Cert#'OTPCertificate'.tbsCertificate#'OTPTBSCertificate'.subject), + case Issuer == Subject of + true -> + case find_issuer(Issuer, TrustedCerts) of + undefined -> + io:format("self-signed certificate is NOT trusted~n"), + undefined; + TrustedCert -> + %% return the cert from the trusted list, to prevent + %% issuer spoofing + {TrustedCert, + [public_key:pkix_encode( + 'OTPCertificate', TrustedCert, otp)|Acc]} + end; + false -> + Match = lists:foldl( + fun(C, undefined) -> + S = public_key:pkix_normalize_name(C#'OTPCertificate'.tbsCertificate#'OTPTBSCertificate'.subject), + %% compare the subject to the current issuer + case Issuer == S of + true -> + %% we've found our man + {public_key:pkix_encode('OTPCertificate', C, otp), C}; + false -> + undefined + end; + (_E, A) -> + %% already matched + A + end, undefined, IntCerts), + case Match of + undefined when IntCerts /= TrustedCerts -> + %% continue the chain by using the trusted CAs + io:format("Ran out of intermediate certs, switching to trusted certs~n"), + build_chain({DER, Cert}, TrustedCerts, TrustedCerts, Acc); + undefined -> + io:format("Can't construct chain of trust beyond ~p~n", + [get_common_name(Cert)]), + %% can't find the current cert's issuer + undefined; + Match -> + build_chain(Match, IntCerts, TrustedCerts, [DER|Acc]) + end + end. + +%% @doc Given a certificate and a list of trusted or intermediary +%% certificates, attempt to find a match in the list or bail with +%% undefined. +find_issuer(Issuer, Certs) -> + lists:foldl( + fun(OTPCert, undefined) -> + %% check if this certificate matches the issuer + Normal = public_key:pkix_normalize_name( + OTPCert#'OTPCertificate'.tbsCertificate#'OTPTBSCertificate'.subject), + case Normal == Issuer of + true -> + OTPCert; + false -> + undefined + end; + (_E, Acc) -> + %% already found a match + Acc + end, undefined, Certs). + +%% @doc Find distribution points for a given CRL and then attempt to +%% fetch the CRL from the first available. +fetch_point(#'DistributionPoint'{distributionPoint={fullName, Names}}) -> + Decoded = [{NameType, + pubkey_cert_records:transform(Name, decode)} + || {NameType, Name} <- Names], + fetch(Decoded). + +%% @doc Given a list of locations to retrieve a CRL from, attempt to +%% retrieve either from a file or http resource and bail as soon as +%% it can be found. +%% +%% Currently, only hand a armored PEM or DER encoded file, with +%% defaulting to DER. +%% +fetch([]) -> + not_available; +fetch([{uniformResourceIdentifier, "file://"++File}|Rest]) -> + io:format("getting CRL from ~p~n", [File]), + try file:read_file(File) of + {ok, Bin} -> + %% assume PEM + [{'CertificateList', DER, _}=CertList] = public_key:pem_decode(Bin), + {DER, public_key:pem_entry_decode(CertList)}; + _ -> + fetch(Rest) + catch + _:_ -> + fetch(Rest) + end; +fetch([{uniformResourceIdentifier, "http"++_=URL}|Rest]) -> + io:format("getting CRL from ~p~n", [URL]), + _ = inets:start(), + case httpc:request(get, {URL, []}, [], [{body_format, binary}]) of + {ok, {_Status, _Headers, Body}} -> + case Body of + <<"-----BEGIN", _/binary>> -> + [{'CertificateList', + DER, _}=CertList] = public_key:pem_decode(Body), + {DER, public_key:pem_entry_decode(CertList)}; + _ -> + %% assume DER encoded + CertList = public_key:pem_entry_decode( + {'CertificateList', Body, not_encrypted}), + {Body, CertList} + end; + {error, _Reason} -> + io:format("failed to get CRL ~p~n", [_Reason]), + fetch(Rest) + end; +fetch([Loc|Rest]) -> + %% unsupported CRL location + io:format("unable to fetch CRL from unsupported location ~p~n", + [Loc]), + fetch(Rest). + +%% get the common name attribute out of an OTPCertificate record +get_common_name(OTPCert) -> + %% You'd think there'd be an easier way than this giant mess, but I + %% couldn't find one. + {rdnSequence, Subject} = OTPCert#'OTPCertificate'.tbsCertificate#'OTPTBSCertificate'.subject, + case [Attribute#'AttributeTypeAndValue'.value || [Attribute] <- Subject, + Attribute#'AttributeTypeAndValue'.type == ?'id-at-commonName'] of + [Att] -> + case Att of + {teletexString, Str} -> Str; + {printableString, Str} -> Str; + {utf8String, Bin} -> binary_to_list(Bin) + end; + _ -> + unknown + end. + diff --git a/lib/ssl/test/ssl_test_lib.erl b/lib/ssl/test/ssl_test_lib.erl index 7ed9adfcd9..1ebccf13ea 100644 --- a/lib/ssl/test/ssl_test_lib.erl +++ b/lib/ssl/test/ssl_test_lib.erl @@ -152,7 +152,9 @@ start_client(Args) -> case lists:member(return_socket, Args) of true -> { Result, Socket }; false -> Result - end + end; + {connect_failed, Reason} -> + {connect_failed, Reason} end. run_client_init(Opts) -> @@ -205,7 +207,9 @@ run_client(Opts) -> end; {error, Reason} -> ct:log("Client: connection failed: ~p ~n", [Reason]), - Pid ! {self(), {error, Reason}} +%%% FIXME: Which one of the two following??? + Pid ! {self(), {error, Reason}}, + Pid ! {connect_failed, Reason} end. close(Pid) -> -- cgit v1.2.3