diff options
Diffstat (limited to 'lib/public_key/src')
-rw-r--r-- | lib/public_key/src/pubkey_cert.erl | 253 | ||||
-rw-r--r-- | lib/public_key/src/public_key.erl | 25 |
2 files changed, 273 insertions, 5 deletions
diff --git a/lib/public_key/src/pubkey_cert.erl b/lib/public_key/src/pubkey_cert.erl index f45f2c2e9a..13833830a7 100644 --- a/lib/public_key/src/pubkey_cert.erl +++ b/lib/public_key/src/pubkey_cert.erl @@ -32,12 +32,25 @@ is_issuer/2, issuer_id/2, distribution_points/1, is_fixed_dh_cert/1, verify_data/1, verify_fun/4, select_extension/2, match_name/3, - extensions_list/1, cert_auth_key_id/1, time_str_2_gregorian_sec/1]). + extensions_list/1, cert_auth_key_id/1, time_str_2_gregorian_sec/1, + gen_test_certs/1]). -define(NULL, 0). - + +-export_type([chain_opts/0, test_config/0]). + +-type cert_opt() :: {digest, public_key:digest_type()} | + {key, public_key:key_params() | public_key:private_key()} | + {validity, {From::erlang:timestamp(), To::erlang:timestamp()}} | + {extensions, [#'Extension'{}]}. +-type chain_end() :: root | peer. +-type chain_opts() :: #{chain_end() := [cert_opt()], intermediates => [[cert_opt()]]}. +-type conf_opt() :: {cert, public_key:der_encoded()} | + {key, public_key:der_encoded()} | + {cacerts, [public_key:der_encoded()]}. +-type test_config() :: #{server_config := [conf_opt()], client_config := [conf_opt()]}. %%==================================================================== -%% Internal application API +%% Internal application APIu %%==================================================================== %%-------------------------------------------------------------------- @@ -417,6 +430,31 @@ match_name(Fun, Name, PermittedName, [Head | Tail]) -> false -> match_name(Fun, Name, Head, Tail) end. +%%% +-spec gen_test_certs(#{server_chain:= chain_opts(), client_chain:= chain_opts()}) -> test_config(). + +%% Generates server and and client configuration for testing +%% purposes. All certificate options have default values +gen_test_certs(#{client_chain := #{root := ClientRootConf, + intermediates := ClientCAs, + peer := ClientPeer}, + server_chain := + #{root := ServerRootConf, + intermediates := ServerCAs, + peer := ServerPeer}}) -> + SRootKey = gen_key(proplists:get_value(key, ServerRootConf, default_key_gen())), + CRootKey = gen_key(proplists:get_value(key, ClientRootConf, default_key_gen())), + ServerRoot = root_cert("server", SRootKey, ClientRootConf), + ClientRoot = root_cert("client", CRootKey, ServerRootConf), + + [{ServerDERCert, ServerDERKey} | ServerCAsKeys] = config(server, ServerRoot, + SRootKey, lists:reverse([ServerPeer | lists:reverse(ServerCAs)])), + [{ClientDERCert, ClientDERKey} | ClientCAsKeys] = config(client, ClientRoot, + CRootKey, lists:reverse([ClientPeer | lists:reverse(ClientCAs)])), + ServerDERCA = ca_config(ClientRoot, ServerCAsKeys), + ClientDERCA = ca_config(ServerRoot, ClientCAsKeys), + #{server_config => [{cert, ServerDERCert}, {key, ServerDERKey}, {cacerts, ServerDERCA}], + client_config => [{cert, ClientDERCert}, {key, ClientDERKey}, {cacerts, ClientDERCA}]}. %%-------------------------------------------------------------------- %%% Internal functions @@ -1064,3 +1102,212 @@ missing_basic_constraints(OtpCert, SelfSigned, ValidationState, VerifyFun, UserS Len - 1}, UserState} end. + + gen_key(KeyGen) -> + case is_key(KeyGen) of + true -> + KeyGen; + false -> + public_key:generate_key(KeyGen) + end. + +is_key(#'DSAPrivateKey'{}) -> + true; +is_key(#'RSAPrivateKey'{}) -> + true; +is_key(#'ECPrivateKey'{}) -> + true; +is_key(_) -> + false. + +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). + +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({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}. + +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)}}. + +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'. + +config(Role, Root, Key, Opts) -> + cert_chain(Role, Root, Key, Opts). + +cert_chain(Role, Root, RootKey, Opts) -> + cert_chain(Role, Root, RootKey, Opts, 0, []). + +cert_chain(Role, IssuerCert, IssuerKey, [PeerOpts], _, Acc) -> + Key = gen_key(proplists:get_value(key, PeerOpts, default_key_gen())), + Cert = cert(Role, public_key:pkix_decode_cert(IssuerCert, otp), + IssuerKey, Key, "admin", " Peer cert", PeerOpts, peer), + [{Cert, Key}, {IssuerCert, IssuerKey} | Acc]; +cert_chain(Role, IssuerCert, IssuerKey, [CAOpts | Rest], N, Acc) -> + Key = gen_key(proplists:get_value(key, CAOpts, default_key_gen())), + Cert = cert(Role, public_key:pkix_decode_cert(IssuerCert, otp), IssuerKey, Key, "webadmin", + " Intermidiate CA " ++ integer_to_list(N), CAOpts, ca), + cert_chain(Role, Cert, Key, Rest, N+1, [{IssuerCert, IssuerKey} | Acc]). + +cert(Role, #'OTPCertificate'{tbsCertificate = #'OTPTBSCertificate'{subject = Issuer}}, + PrivKey, Key, Contact, Name, Opts, Type) -> + TBS = cert_template(), + OTPTBS = TBS#'OTPTBSCertificate'{ + signature = sign_algorithm(PrivKey, Opts), + issuer = Issuer, + validity = validity(Opts), + subject = subject(Contact, atom_to_list(Role) ++ Name), + subjectPublicKeyInfo = public_key(Key), + extensions = extensions(Role, Type, Opts) + + }, + public_key:pkix_sign(OTPTBS, PrivKey). + +ca_config(Root, CAsKeys) -> + [Root | [CA || {CA, _} <- CAsKeys]]. + +default_key_gen() -> + case crypto:ec_curves() of + [] -> + {rsa, 2048, 17}; + [Curve |_] -> + Oid = pubkey_cert_records:namedCurves(Curve), + {namedCurve, Oid} + end. + +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}}. + +extensions(Role, Type, Opts) -> + Exts = proplists:get_value(extensions, Opts, []), + add_default_extensions(Role, Type, Exts). + +add_default_extensions(_, ca, Exts) -> + Default = [#'Extension'{extnID = ?'id-ce-keyUsage', + extnValue = [keyCertSign, cRLSign], + critical = false}, + #'Extension'{extnID = ?'id-ce-basicConstraints', + extnValue = #'BasicConstraints'{cA = true}, + critical = true}], + add_default_extensions(Default, Exts); + +add_default_extensions(server, peer, Exts) -> + Hostname = net_adm:localhost(), + Default = [#'Extension'{extnID = ?'id-ce-keyUsage', + extnValue = [digitalSignature, keyAgreement], + critical = false}, + #'Extension'{extnID = ?'id-ce-subjectAltName', + extnValue = [{dNSName, Hostname}], + critical = false} + ], + add_default_extensions(Default, Exts); + +add_default_extensions(_, peer, Exts) -> + Exts. + +add_default_extensions(Defaults0, Exts) -> + Defaults = lists:filtermap(fun(#'Extension'{extnID = ID} = Ext) -> + case lists:keymember(ID, 2, Exts) of + true -> + false; + false -> + {true, Ext} + end + end, Defaults0), + Exts ++ Defaults. + diff --git a/lib/public_key/src/public_key.erl b/lib/public_key/src/public_key.erl index 9a61184f8a..cc01b61433 100644 --- a/lib/public_key/src/public_key.erl +++ b/lib/public_key/src/public_key.erl @@ -58,11 +58,13 @@ pkix_match_dist_point/2, pkix_crl_verify/2, pkix_crl_issuer/1, - short_name_hash/1 + short_name_hash/1, + pkix_test_data/1 ]). -export_type([public_key/0, private_key/0, pem_entry/0, - pki_asn1_type/0, asn1_type/0, ssh_file/0, der_encoded/0]). + pki_asn1_type/0, asn1_type/0, ssh_file/0, der_encoded/0, + key_params/0, digest_type/0]). -type public_key() :: rsa_public_key() | dsa_public_key() | ec_public_key(). -type private_key() :: rsa_private_key() | dsa_private_key() | ec_private_key(). @@ -75,6 +77,8 @@ -type ecpk_parameters_api() :: ecpk_parameters() | #'ECParameters'{} | {namedCurve, Name::atom()}. -type ec_public_key() :: {#'ECPoint'{}, ecpk_parameters_api()}. -type ec_private_key() :: #'ECPrivateKey'{}. +-type key_params() :: #'DHParameter'{} | {namedCurve, oid()} | #'ECParameters'{} | + {rsa, Size::integer(), PubExp::integer()}. -type der_encoded() :: binary(). -type pki_asn1_type() :: 'Certificate' | 'RSAPrivateKey' | 'RSAPublicKey' | 'DSAPrivateKey' | 'DSAPublicKey' | 'DHParameter' @@ -102,6 +106,7 @@ -type crl_reason() :: unspecified | keyCompromise | cACompromise | affiliationChanged | superseded | cessationOfOperation | certificateHold | privilegeWithdrawn | aACompromise. -type oid() :: tuple(). +-type chain_type() :: server_chain | client_chain. -define(UINT32(X), X:32/unsigned-big-integer). -define(DER_NULL, <<5, 0>>). @@ -1027,6 +1032,22 @@ short_name_hash({rdnSequence, _Attributes} = Name) -> <<HashValue:32/little, _/binary>> = crypto:hash(sha, HashThis), string:to_lower(string:right(integer_to_list(HashValue, 16), 8, $0)). + +%%-------------------------------------------------------------------- +-spec pkix_test_data(#{chain_type() := pubkey_cert:chain_opts()}) -> + pubkey_cert:test_config(). + +%% Description: Generates OpenSSL-style hash of a name. +%%-------------------------------------------------------------------- + +pkix_test_data(#{client_chain := ClientChain0, + server_chain := ServerChain0}) -> + Default = #{intermediates => []}, + ClientChain = maps:merge(Default, ClientChain0), + ServerChain = maps:merge(Default, ServerChain0), + pubkey_cert:gen_test_certs(#{client_chain => ClientChain, + server_chain => ServerChain}). + %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- |