Here we consider the creation of example certificates.
The openssl command is a utility that comes with the OpenSSL distribution. It provides a variety of subcommands. Each subcommand is invoked as
openssl subcmd <options and arguments>
where subcmd denotes the subcommand in question.
We shall use the following subcommands to create certificates for the purpose of testing Erlang/OTP SSL:
We create the following certificates:
An openssl configuration file consist of a number of sections, where each section starts with one line containing [ section_name ], where section_name is the name of the section. The first section of the file is either unnamed, or is named [ default ]. For further details see the OpenSSL config(5) manual page.
The required sections for the subcommands we are going to use are as follows:
subcommand | required/default section | override command line option | configuration file option |
req | [req] | - | -config FILE |
ca | [ca] | -name section | -config FILE |
The Erlang root CA is created with the command
openssl req -new -x509 -config /some/path/req.cnf \ -keyout /some/path/key.pem -out /some/path/cert.pem
where the option -new indicates that we want to create a new certificate request and the option -x509 implies that a self-signed certificate is created.
The OTP CA is created by first creating a certificate request with the command
openssl req -new -config /some/path/req.cnf \ -keyout /some/path/key.pem -out /some/path/req.pem
and the ask the Erlang CA to sign it:
openssl ca -batch -notext -config /some/path/req.cnf \ -extensions ca_cert -in /some/path/req.pem -out /some/path/cert.pem
where the option -extensions refers to a section in the configuration file saying that it should create a CA certificate, and not a plain user certificate.
The client and server certificates are created similarly, except that the option -extensions then has the value user_cert.
The following module create_certs is used by the Erlang/OTP SSL application for generating certificates to be used in tests. The source code is also found in ssl-X.Y.Z/examples/certs/src.
The purpose of the create_certs:all/1 function is to make it possible to provide from the erl command line, the full path name of the openssl command.
Note that the module creates temporary OpenSSL configuration files for the req and ca subcommands.
%% The purpose of this module is to create example certificates for %% testing. %% Run it as: %% %% erl -noinput -run make_certs all "/path/to/openssl" -s erlang halt %% -module(make_certs). -export([all/0, all/1]). -record(dn, {commonName, organizationalUnitName = "Erlang OTP", organizationName = "Ericsson AB", localityName = "Stockholm", countryName = "SE", emailAddress = "peter@erix.ericsson.se"}). all() -> all(["openssl"]). all([OpenSSLCmd]) -> Root = filename:dirname(filename:dirname((code:which(?MODULE)))), %% io:fwrite("Root : ~s~n", [Root]), NRoot = filename:join([Root, "etc"]), file:make_dir(NRoot), create_rnd(Root, "etc"), % For all requests rootCA(NRoot, OpenSSLCmd, "erlangCA"), intermediateCA(NRoot, OpenSSLCmd, "otpCA", "erlangCA"), endusers(NRoot, OpenSSLCmd, "otpCA", ["client", "server"]), collect_certs(NRoot, ["erlangCA", "otpCA"], ["client", "server"]), remove_rnd(Root, "etc"). 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. intermediateCA(Root, OpenSSLCmd, CA, ParentCA) -> CA = "otpCA", create_ca_dir(Root, CA, ca_cnf(CA)), CARoot = filename:join([Root, CA]), DN = #dn{commonName = CA}, CnfFile = filename:join([CARoot, "req.cnf"]), file:write_file(CnfFile, req_cnf(DN)), KeyFile = filename:join([CARoot, "private", "key.pem"]), ReqFile = filename:join([CARoot, "req.pem"]), create_req(Root, OpenSSLCmd, CnfFile, KeyFile, ReqFile), 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) -> 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)), KeyFile = filename:join([UsrRoot, "key.pem"]), ReqFile = filename:join([UsrRoot, "req.pem"]), create_req(Root, OpenSSLCmd, CnfFile, KeyFile, ReqFile), CertFile = filename:join([UsrRoot, "cert.pem"]), sign_req(Root, OpenSSLCmd, CA, "user_cert", ReqFile, CertFile). 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). create_self_signed_cert(Root, OpenSSLCmd, CAName, Cnf) -> 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" " -new" " -x509" " -config ", CnfFile, " -keyout ", KeyFile, " -out ", CertFile], Env = [{"ROOTDIR", Root}], cmd(Cmd, Env). create_ca_dir(Root, CAName, Cnf) -> CARoot = filename:join([Root, CAName]), file:make_dir(CARoot), create_dirs(CARoot, ["certs", "crl", "newcerts", "private"]), create_rnd(Root, filename:join([CAName, "private"])), create_files(CARoot, [{"serial", "01\n"}, {"index.txt", ""}, {"ca.cnf", Cnf}]). create_req(Root, OpenSSLCmd, CnfFile, KeyFile, ReqFile) -> Cmd = [OpenSSLCmd, " req" " -new" " -config ", CnfFile, " -keyout ", KeyFile, " -out ", ReqFile], Env = [{"ROOTDIR", Root}], cmd(Cmd, Env). sign_req(Root, OpenSSLCmd, CA, CertType, ReqFile, CertFile) -> CACnfFile = filename:join([Root, CA, "ca.cnf"]), Cmd = [OpenSSLCmd, " ca" " -batch" " -notext" " -config ", CACnfFile, " -extensions ", CertType, " -in ", ReqFile, " -out ", CertFile], Env = [{"ROOTDIR", 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(Root, Dir) -> From = filename:join([Root, "rnd", "RAND"]), To = filename:join([Root, Dir, "RAND"]), file:copy(From, To). remove_rnd(Root, Dir) -> File = filename:join([Root, Dir, "RAND"]), file:delete(File). cmd(Cmd, Env) -> FCmd = lists:flatten(Cmd), Port = open_port({spawn, FCmd}, [stream, eof, exit_status, {env, Env}]), eval_cmd(Port). eval_cmd(Port) -> receive {Port, {data, _}} -> eval_cmd(Port); {Port, eof} -> ok end, receive {Port, {exit_status, Status}} when Status /= 0 -> %% io:fwrite("exit status: ~w~n", [Status]), erlang:halt(Status) after 0 -> ok end. %% %% Contents of configuration files %% req_cnf(DN) -> ["# Purpose: Configuration for requests (end users and CAs)." "\n" "ROOTDIR = $ENV::ROOTDIR\n" "\n" "[req]\n" "input_password = secret\n" "output_password = secret\n" "default_bits = 1024\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 = ", 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" "\n" "[ca_ext]\n" "basicConstraints = critical, CA:true\n" "keyUsage = cRLSign, keyCertSign\n" "subjectKeyIdentifier = hash\n" "subjectAltName = email:copy\n"]. ca_cnf(CA) -> ["# Purpose: Configuration for CAs.\n" "\n" "ROOTDIR = $ENV::ROOTDIR\n" "default_ca = ca\n" "\n" "[ca]\n" "dir = $ROOTDIR/", CA, "\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" "private_key = $dir/private/key.pem\n" "RANDFILE = $dir/private/RAND\n" "\n" "x509_extensions = user_cert\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" "[user_cert]\n" "basicConstraints = CA:false\n" "keyUsage = nonRepudiation, digitalSignature, keyEncipherment\n" "subjectKeyIdentifier = hash\n" "authorityKeyIdentifier = keyid,issuer:always\n" "subjectAltName = email:copy\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"].