%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2007-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %% -module(make_certs). -compile([export_all, nowarn_export_all]). %-export([all/1, all/2, rootCA/2, intermediateCA/3, endusers/3, enduser/3, revoke/3, gencrl/2, verify/3]). -record(config, {commonName, organizationalUnitName = "Erlang OTP", organizationName = "Ericsson AB", localityName = "Stockholm", countryName = "SE", emailAddress = "peter@erix.ericsson.se", default_bits = 2048, v2_crls = true, ecc_certs = false, issuing_distribution_point = false, crl_port = 8000, openssl_cmd = "openssl", hostname = "host.example.com"}). default_config() -> #config{hostname = net_adm:localhost()}. make_config(Args) -> make_config(Args, default_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([{crl_port, Port}|T], C) when is_integer(Port) -> make_config(T, C#config{crl_port = Port}); 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}); make_config([{hostname, Hostname}|T], C) when is_list(Hostname) -> make_config(T, C#config{hostname = Hostname}). all([DataDir, PrivDir]) -> all(DataDir, PrivDir). all(DataDir, PrivDir) -> 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, "erlangCA", C), intermediateCA(PrivDir, "otpCA", "erlangCA", C), endusers(PrivDir, "otpCA", ["client", "server", "revoked", "a.server", "b.server"], C), endusers(PrivDir, "erlangCA", ["localhost"], C), %% Create keycert files SDir = filename:join([PrivDir, "server"]), SC = filename:join([SDir, "cert.pem"]), SK = filename:join([SDir, "key.pem"]), SKC = filename:join([SDir, "keycert.pem"]), append_files([SK, SC], SKC), CDir = filename:join([PrivDir, "client"]), CC = filename:join([CDir, "cert.pem"]), CK = filename:join([CDir, "key.pem"]), CKC = filename:join([CDir, "keycert.pem"]), append_files([CK, CC], CKC), 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]), do_append_files(FileNames, ResultFile). do_append_files([], RF) -> ok = file:close(RF); do_append_files([F|Fs], RF) -> {ok, Data} = file:read_file(F), ok = file:write(RF, Data), do_append_files(Fs, RF). rootCA(Root, Name, C) -> create_ca_dir(Root, Name, ca_cnf(Root, C#config{commonName = Name})), create_self_signed_cert(Root, Name, req_cnf(Root, 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, CA, ParentCA, C) -> create_ca_dir(Root, CA, ca_cnf(Root, C#config{commonName = CA})), CARoot = filename:join([Root, CA]), CnfFile = filename:join([CARoot, "req.cnf"]), file:write_file(CnfFile, req_cnf(Root, C#config{commonName = CA})), KeyFile = filename:join([CARoot, "private", "key.pem"]), ReqFile = filename:join([CARoot, "req.pem"]), create_req(Root, CnfFile, KeyFile, ReqFile, C), CertFile = filename:join([CARoot, "cert.pem"]), 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"]), file:write_file(CnfFile, req_cnf(Root, C#config{commonName = User})), KeyFile = filename:join([UsrRoot, "key.pem"]), ReqFile = filename:join([UsrRoot, "req.pem"]), create_req(Root, CnfFile, KeyFile, ReqFile, C), %create_req(Root, CnfFile, KeyFile, ReqFile), CertFileAllUsage = filename:join([UsrRoot, "cert.pem"]), sign_req(Root, CA, "user_cert", ReqFile, CertFileAllUsage, C), CertFileDigitalSigOnly = filename:join([UsrRoot, "digital_signature_only_cert.pem"]), 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) -> %% By default, the CRL is valid for a week from now. gencrl(Root, CA, C, 24*7). gencrl(Root, CA, C, CrlHours) -> CACnfFile = filename:join([Root, CA, "ca.cnf"]), CACRLFile = filename:join([Root, CA, "crl.pem"]), Cmd = [C#config.openssl_cmd, " ca" " -gencrl ", " -crlhours ", integer_to_list(CrlHours), " -out ", CACRLFile, " -config ", CACnfFile], Env = [{"ROOTDIR", filename:absname(Root)}], cmd(Cmd, Env). %% This function sets the number of seconds until the next CRL is due. gencrl_sec(Root, CA, C, CrlSecs) -> CACnfFile = filename:join([Root, CA, "ca.cnf"]), CACRLFile = filename:join([Root, CA, "crl.pem"]), Cmd = [C#config.openssl_cmd, " ca" " -gencrl ", " -crlsec ", integer_to_list(CrlSecs), " -out ", CACRLFile, " -config ", CACnfFile], Env = [{"ROOTDIR", filename:absname(Root)}], cmd(Cmd, Env). can_generate_expired_crls(C) -> %% OpenSSL can generate CRLs with an expiration date in the past, %% if we pass a negative number for -crlhours. However, LibreSSL %% rejects this with the error "invalid argument -24: too small". %% Let's check which one we have. Cmd = [C#config.openssl_cmd, " ca -crlhours -24"], Output = os:cmd(Cmd), 0 =:= string:str(Output, "too small"). 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 = [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, " -key ", KeyFile, " -outform PEM ", " -out ", CertFile], 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, 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", filename:absname(Root)}], cmd(Cmd, Env). %fix_key_file(KeyFile). sign_req(Root, CA, CertType, ReqFile, CertFile, C) -> CACnfFile = filename:join([Root, CA, "ca.cnf"]), Cmd = [C#config.openssl_cmd, " ca" " -batch" " -notext" " -config ", CACnfFile, " -extensions ", CertType, " -in ", ReqFile, " -out ", CertFile], Env = [{"ROOTDIR", filename:absname(Root)}], cmd(Cmd, Env). %% %% Misc %% create_dirs(Root, Dirs) -> lists:foreach(fun(Dir) -> file:make_dir(filename:join([Root, Dir])) end, Dirs). create_files(Root, NameContents) -> lists:foreach( fun({Name, Contents}) -> file:write_file(filename:join([Root, Name]), Contents) end, NameContents). create_rnd(FromDir, ToDir) -> From = filename:join([FromDir, "RAND"]), To = filename:join([ToDir, "RAND"]), file:copy(From, To). remove_rnd(Dir) -> File = filename:join([Dir, "RAND"]), file:delete(File). cmd(Cmd, Env) -> FCmd = lists:flatten(Cmd), Port = open_port({spawn, FCmd}, [stream, eof, exit_status, stderr_to_stdout, {env, Env}]), eval_cmd(Port, FCmd). eval_cmd(Port, Cmd) -> receive {Port, {data, _}} -> eval_cmd(Port, Cmd); {Port, eof} -> ok end, receive {Port, {exit_status, 0}} -> ok; {Port, {exit_status, Status}} -> exit({eval_cmd, Cmd, Status}) after 0 -> ok end. %% %% Contents of configuration files %% req_cnf(Root, C) -> ["# Purpose: Configuration for requests (end users and CAs)." "\n" "ROOTDIR = " ++ Root ++ "\n" "\n" "[req]\n" "input_password = secret\n" "output_password = secret\n" "default_bits = ", integer_to_list(C#config.default_bits), "\n" "RANDFILE = $ROOTDIR/RAND\n" "encrypt_key = no\n" "default_md = sha1\n" "#string_mask = pkix\n" "x509_extensions = ca_ext\n" "prompt = no\n" "distinguished_name= name\n" "\n" "[name]\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" "basicConstraints = critical, CA:true\n" "keyUsage = cRLSign, keyCertSign\n" "subjectKeyIdentifier = hash\n" "subjectAltName = email:copy\n"]. ca_cnf( Root, #config{ issuing_distribution_point = true, hostname = Hostname} = C) -> ["# Purpose: Configuration for CAs.\n" "\n" "ROOTDIR = " ++ Root ++ "\n" "default_ca = ca\n" "\n" "[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", ["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", ["crl_extensions = crl_ext\n" || C#config.v2_crls], "unique_subject = no\n" "default_days = 3600\n" "default_md = sha1\n" "preserve = no\n" "policy = policy_match\n" "\n" "[policy_match]\n" "commonName = supplied\n" "organizationalUnitName = optional\n" "organizationName = match\n" "countryName = match\n" "localityName = match\n" "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" "[user_cert]\n" "basicConstraints = CA:false\n" "keyUsage = nonRepudiation, digitalSignature, keyEncipherment\n" "subjectKeyIdentifier = hash\n" "authorityKeyIdentifier = keyid,issuer:always\n" "subjectAltName = DNS.1:" ++ Hostname ++ "\n" "issuerAltName = issuer:copy\n" "crlDistributionPoints=@crl_section\n" "[crl_section]\n" %% intentionally invalid "URI.1=http://localhost/",C#config.commonName,"/crl.pem\n" "URI.2=http://localhost:",integer_to_list(C#config.crl_port),"/",C#config.commonName,"/crl.pem\n" "\n" "[user_cert_digital_signature_only]\n" "basicConstraints = CA:false\n" "keyUsage = digitalSignature\n" "subjectKeyIdentifier = hash\n" "authorityKeyIdentifier = keyid,issuer:always\n" "subjectAltName = DNS.1:" ++ Hostname ++ "\n" "issuerAltName = issuer:copy\n" "\n" "[ca_cert]\n" "basicConstraints = critical,CA:true\n" "keyUsage = cRLSign, keyCertSign\n" "subjectKeyIdentifier = hash\n" "authorityKeyIdentifier = keyid:always,issuer:always\n" "subjectAltName = DNS.1:" ++ Hostname ++ "\n" "issuerAltName = issuer:copy\n" "crlDistributionPoints=@crl_section\n" ]; ca_cnf( Root, #config{ issuing_distribution_point = false, hostname = Hostname } = C) -> ["# Purpose: Configuration for CAs.\n" "\n" "ROOTDIR = " ++ Root ++ "\n" "default_ca = ca\n" "\n" "[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", ["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", ["crl_extensions = crl_ext\n" || C#config.v2_crls], "unique_subject = no\n" "default_days = 3600\n" "default_md = sha1\n" "preserve = no\n" "policy = policy_match\n" "\n" "[policy_match]\n" "commonName = supplied\n" "organizationalUnitName = optional\n" "organizationName = match\n" "countryName = match\n" "localityName = match\n" "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" "[user_cert]\n" "basicConstraints = CA:false\n" "keyUsage = nonRepudiation, digitalSignature, keyEncipherment\n" "subjectKeyIdentifier = hash\n" "authorityKeyIdentifier = keyid,issuer:always\n" "subjectAltName = DNS.1:" ++ Hostname ++ "\n" "issuerAltName = issuer:copy\n" %"crlDistributionPoints=@crl_section\n" %%"[crl_section]\n" %% intentionally invalid %%"URI.1=http://localhost/",C#config.commonName,"/crl.pem\n" %%"URI.2=http://localhost:",integer_to_list(C#config.crl_port),"/",C#config.commonName,"/crl.pem\n" %%"\n" "[user_cert_digital_signature_only]\n" "basicConstraints = CA:false\n" "keyUsage = digitalSignature\n" "subjectKeyIdentifier = hash\n" "authorityKeyIdentifier = keyid,issuer:always\n" "subjectAltName = DNS.1:" ++ Hostname ++ "\n" "issuerAltName = issuer:copy\n" "\n" "[ca_cert]\n" "basicConstraints = critical,CA:true\n" "keyUsage = cRLSign, keyCertSign\n" "subjectKeyIdentifier = hash\n" "authorityKeyIdentifier = keyid:always,issuer:always\n" "subjectAltName = email:copy\n" "issuerAltName = issuer:copy\n" %"crlDistributionPoints=@crl_section\n" ].