%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2017-2017. 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(x509_test). -include_lib("public_key/include/public_key.hrl"). -export([gen_test_certs/1, gen_pem_config_files/3]). gen_test_certs(Opts) -> SRootKey = gen_key(proplists:get_value(server_key_gen, Opts)), CRootKey = gen_key(proplists:get_value(client_key_gen, Opts)), ServerRoot = root_cert("server", SRootKey, Opts), ClientRoot = root_cert("client", CRootKey, Opts), [{ServerCert, ServerKey} | ServerCAsKeys] = config(server, ServerRoot, SRootKey, Opts), [{ClientCert, ClientKey} | ClientCAsKeys] = config(client, ClientRoot, CRootKey, Opts), ServerCAs = ca_config(ClientRoot, ServerCAsKeys), ClientCAs = ca_config(ServerRoot, ClientCAsKeys), [{server_config, [{cert, ServerCert}, {key, ServerKey}, {cacerts, ServerCAs}]}, {client_config, [{cert, ClientCert}, {key, ClientKey}, {cacerts, ClientCAs}]}]. gen_pem_config_files(GenCertData, ClientBase, ServerBase) -> ServerConf = proplists:get_value(server_config, GenCertData), ClientConf = proplists:get_value(client_config, GenCertData), ServerCaCertFile = ServerBase ++ "_server_cacerts.pem", ServerCertFile = ServerBase ++ "_server_cert.pem", ServerKeyFile = ServerBase ++ "_server_key.pem", ClientCaCertFile = ClientBase ++ "_client_cacerts.pem", ClientCertFile = ClientBase ++ "_client_cert.pem", ClientKeyFile = ClientBase ++ "_client_key.pem", do_gen_pem_config_files(ServerConf, ServerCertFile, ServerKeyFile, ServerCaCertFile), do_gen_pem_config_files(ClientConf, ClientCertFile, ClientKeyFile, ClientCaCertFile), [{server_config, [{certfile, ServerCertFile}, {keyfile, ServerKeyFile}, {cacertfile, ServerCaCertFile}]}, {client_config, [{certfile, ClientCertFile}, {keyfile, ClientKeyFile}, {cacertfile, ClientCaCertFile}]}]. do_gen_pem_config_files(Config, CertFile, KeyFile, CAFile) -> CAs = proplists:get_value(cacerts, Config), Cert = proplists:get_value(cert, Config), Key = proplists:get_value(key, Config), der_to_pem(CertFile, [cert_entry(Cert)]), der_to_pem(KeyFile, [key_entry(Key)]), der_to_pem(CAFile, ca_entries(CAs)). cert_entry(Cert) -> {'Certificate', Cert, not_encrypted}. key_entry(Key = #'RSAPrivateKey'{}) -> Der = public_key:der_encode('RSAPrivateKey', Key), {'RSAPrivateKey', Der, not_encrypted}; key_entry(Key = #'DSAPrivateKey'{}) -> Der = public_key:der_encode('DSAPrivateKey', Key), {'DSAPrivateKey', Der, not_encrypted}; key_entry(Key = #'ECPrivateKey'{}) -> Der = public_key:der_encode('ECPrivateKey', Key), {'ECPrivateKey', Der, not_encrypted}. ca_entries(CAs) -> [{'Certificate', CACert, not_encrypted} || CACert <- CAs]. gen_key(KeyGen) -> case is_key(KeyGen) of true -> KeyGen; false -> public_key:generate_key(KeyGen) end. root_cert(Role, PrivKey, Opts) -> TBS = cert_template(), Issuer = issuer("root", Role, " ROOT CA"), OTPTBS = TBS#'OTPTBSCertificate'{ signature = sign_algorithm(PrivKey, Opts), issuer = Issuer, validity = validity(Opts), subject = Issuer, subjectPublicKeyInfo = public_key(PrivKey), extensions = extensions(Role, ca, Opts) }, public_key:pkix_sign(OTPTBS, PrivKey). config(Role, Root, Key, Opts) -> KeyGenOpt = list_to_atom(atom_to_list(Role) ++ "_key_gen_chain"), KeyGens = proplists:get_value(KeyGenOpt, Opts, default_key_gen()), Keys = lists:map(fun gen_key/1, KeyGens), cert_chain(Role, Root, Key, Opts, Keys). cert_template() -> #'OTPTBSCertificate'{ version = v3, serialNumber = trunc(rand:uniform()*100000000)*10000 + 1, issuerUniqueID = asn1_NOVALUE, subjectUniqueID = asn1_NOVALUE }. issuer(Contact, Role, Name) -> subject(Contact, Role ++ Name). subject(Contact, Name) -> Opts = [{email, Contact ++ "@erlang.org"}, {name, Name}, {city, "Stockholm"}, {country, "SE"}, {org, "erlang"}, {org_unit, "automated testing"}], subject(Opts). subject(SubjectOpts) when is_list(SubjectOpts) -> Encode = fun(Opt) -> {Type,Value} = subject_enc(Opt), [#'AttributeTypeAndValue'{type=Type, value=Value}] end, {rdnSequence, [Encode(Opt) || Opt <- SubjectOpts]}. subject_enc({name, Name}) -> {?'id-at-commonName', {printableString, Name}}; subject_enc({email, Email}) -> {?'id-emailAddress', Email}; subject_enc({city, City}) -> {?'id-at-localityName', {printableString, City}}; subject_enc({state, State}) -> {?'id-at-stateOrProvinceName', {printableString, State}}; subject_enc({org, Org}) -> {?'id-at-organizationName', {printableString, Org}}; subject_enc({org_unit, OrgUnit}) -> {?'id-at-organizationalUnitName', {printableString, OrgUnit}}; subject_enc({country, Country}) -> {?'id-at-countryName', Country}; subject_enc({serial, Serial}) -> {?'id-at-serialNumber', Serial}; subject_enc({title, Title}) -> {?'id-at-title', {printableString, Title}}; subject_enc({dnQualifer, DnQ}) -> {?'id-at-dnQualifier', DnQ}; subject_enc(Other) -> Other. validity(Opts) -> DefFrom0 = calendar:gregorian_days_to_date(calendar:date_to_gregorian_days(date())-1), DefTo0 = calendar:gregorian_days_to_date(calendar:date_to_gregorian_days(date())+7), {DefFrom, DefTo} = proplists:get_value(validity, Opts, {DefFrom0, DefTo0}), Format = fun({Y,M,D}) -> lists:flatten(io_lib:format("~w~2..0w~2..0w000000Z",[Y,M,D])) end, #'Validity'{notBefore={generalTime, Format(DefFrom)}, notAfter ={generalTime, Format(DefTo)}}. extensions(Role, Type, Opts) -> Exts = proplists:get_value(extensions, Opts, []), lists:flatten([extension(Ext) || Ext <- default_extensions(Role, Type, Exts)]). %% Common extension: name_constraints, policy_constraints, ext_key_usage, inhibit_any, %% auth_key_id, subject_key_id, policy_mapping, default_extensions(_, ca, Exts) -> Def = [{key_usage, [keyCertSign, cRLSign]}, {basic_constraints, default}], add_default_extensions(Def, Exts); default_extensions(server, peer, Exts) -> Hostname = net_adm:localhost(), Def = [{key_usage, [digitalSignature, keyAgreement]}, {subject_alt, Hostname}], add_default_extensions(Def, Exts); default_extensions(_, peer, Exts) -> Exts. add_default_extensions(Def, Exts) -> Filter = fun({Key, _}, D) -> lists:keydelete(Key, 1, D); ({Key, _, _}, D) -> lists:keydelete(Key, 1, D) end, Exts ++ lists:foldl(Filter, Def, Exts). extension({_, undefined}) -> []; extension({basic_constraints, Data}) -> case Data of default -> #'Extension'{extnID = ?'id-ce-basicConstraints', extnValue = #'BasicConstraints'{cA=true}, critical=true}; false -> []; Len when is_integer(Len) -> #'Extension'{extnID = ?'id-ce-basicConstraints', extnValue = #'BasicConstraints'{cA=true, pathLenConstraint = Len}, critical = true}; _ -> #'Extension'{extnID = ?'id-ce-basicConstraints', extnValue = Data} end; extension({auth_key_id, {Oid, Issuer, SNr}}) -> #'Extension'{extnID = ?'id-ce-authorityKeyIdentifier', extnValue = #'AuthorityKeyIdentifier'{ keyIdentifier = Oid, authorityCertIssuer = Issuer, authorityCertSerialNumber = SNr}, critical = false}; extension({key_usage, Value}) -> #'Extension'{extnID = ?'id-ce-keyUsage', extnValue = Value, critical = false}; extension({subject_alt, Hostname}) -> #'Extension'{extnID = ?'id-ce-subjectAltName', extnValue = [{dNSName, Hostname}], critical = false}; extension({Id, Data, Critical}) -> #'Extension'{extnID = Id, extnValue = Data, critical = Critical}. public_key(#'RSAPrivateKey'{modulus=N, publicExponent=E}) -> Public = #'RSAPublicKey'{modulus=N, publicExponent=E}, Algo = #'PublicKeyAlgorithm'{algorithm= ?rsaEncryption, parameters='NULL'}, #'OTPSubjectPublicKeyInfo'{algorithm = Algo, subjectPublicKey = Public}; public_key(#'DSAPrivateKey'{p=P, q=Q, g=G, y=Y}) -> Algo = #'PublicKeyAlgorithm'{algorithm= ?'id-dsa', parameters={params, #'Dss-Parms'{p=P, q=Q, g=G}}}, #'OTPSubjectPublicKeyInfo'{algorithm = Algo, subjectPublicKey = Y}; public_key(#'ECPrivateKey'{version = _Version, privateKey = _PrivKey, parameters = Params, publicKey = PubKey}) -> Algo = #'PublicKeyAlgorithm'{algorithm= ?'id-ecPublicKey', parameters=Params}, #'OTPSubjectPublicKeyInfo'{algorithm = Algo, subjectPublicKey = #'ECPoint'{point = PubKey}}. sign_algorithm(#'RSAPrivateKey'{}, Opts) -> Type = rsa_digest_oid(proplists:get_value(digest, Opts, sha1)), #'SignatureAlgorithm'{algorithm = Type, parameters = 'NULL'}; sign_algorithm(#'DSAPrivateKey'{p=P, q=Q, g=G}, _Opts) -> #'SignatureAlgorithm'{algorithm = ?'id-dsa-with-sha1', parameters = {params,#'Dss-Parms'{p=P, q=Q, g=G}}}; sign_algorithm(#'ECPrivateKey'{parameters = Parms}, Opts) -> Type = ecdsa_digest_oid(proplists:get_value(digest, Opts, sha1)), #'SignatureAlgorithm'{algorithm = Type, parameters = Parms}. rsa_digest_oid(sha1) -> ?'sha1WithRSAEncryption'; rsa_digest_oid(sha512) -> ?'sha512WithRSAEncryption'; rsa_digest_oid(sha384) -> ?'sha384WithRSAEncryption'; rsa_digest_oid(sha256) -> ?'sha256WithRSAEncryption'; rsa_digest_oid(md5) -> ?'md5WithRSAEncryption'. ecdsa_digest_oid(sha1) -> ?'ecdsa-with-SHA1'; ecdsa_digest_oid(sha512) -> ?'ecdsa-with-SHA512'; ecdsa_digest_oid(sha384) -> ?'ecdsa-with-SHA384'; ecdsa_digest_oid(sha256) -> ?'ecdsa-with-SHA256'. ca_config(Root, CAsKeys) -> [Root | [CA || {CA, _} <- CAsKeys]]. cert_chain(Role, Root, RootKey, Opts, Keys) -> cert_chain(Role, Root, RootKey, Opts, Keys, 0, []). cert_chain(Role, IssuerCert, IssuerKey, Opts, [Key], _, Acc) -> PeerOpts = list_to_atom(atom_to_list(Role) ++ "_peer_opts"), Cert = cert(Role, public_key:pkix_decode_cert(IssuerCert, otp), IssuerKey, Key, "admin", " Peer cert", Opts, PeerOpts, peer), [{Cert, Key}, {IssuerCert, IssuerKey} | Acc]; cert_chain(Role, IssuerCert, IssuerKey, Opts, [Key | Keys], N, Acc) -> CAOpts = list_to_atom(atom_to_list(Role) ++ "_ca_" ++ integer_to_list(N)), Cert = cert(Role, public_key:pkix_decode_cert(IssuerCert, otp), IssuerKey, Key, "webadmin", " Intermidiate CA " ++ integer_to_list(N), Opts, CAOpts, ca), cert_chain(Role, Cert, Key, Opts, Keys, N+1, [{IssuerCert, IssuerKey} | Acc]). cert(Role, #'OTPCertificate'{tbsCertificate = #'OTPTBSCertificate'{subject = Issuer, serialNumber = SNr }}, PrivKey, Key, Contact, Name, Opts, CertOptsName, Type) -> CertOpts = proplists:get_value(CertOptsName, Opts, []), TBS = cert_template(), OTPTBS = TBS#'OTPTBSCertificate'{ signature = sign_algorithm(PrivKey, Opts), issuer = Issuer, validity = validity(CertOpts), subject = subject(Contact, atom_to_list(Role) ++ Name), subjectPublicKeyInfo = public_key(Key), extensions = extensions(Role, Type, add_default_extensions([{auth_key_id, {auth_key_oid(Role), Issuer, SNr}}], CertOpts)) }, public_key:pkix_sign(OTPTBS, PrivKey). is_key(#'DSAPrivateKey'{}) -> true; is_key(#'RSAPrivateKey'{}) -> true; is_key(#'ECPrivateKey'{}) -> true; is_key(_) -> false. der_to_pem(File, Entries) -> PemBin = public_key:pem_encode(Entries), file:write_file(File, PemBin). default_key_gen() -> case tls_v1:ecc_curves(0) of [] -> [{rsa, 2048, 17}, {rsa, 2048, 17}]; [_|_] -> [{namedCurve, hd(tls_v1:ecc_curves(0))}, {namedCurve, hd(tls_v1:ecc_curves(0))}] end. auth_key_oid(server) -> ?'id-kp-serverAuth'; auth_key_oid(client) -> ?'id-kp-clientAuth'.