aboutsummaryrefslogblamecommitdiffstats
path: root/lib/ssl/test/x509_test.erl
blob: 4da1537ef62e2b5bf2c081ca3723732b2296363a (plain) (tree)





















                                                                           
                    
 
                                                   
 
                                                     
 










                                                                                            
 
                                                            


                                                                 


                                                           
    



                                                           







                                                      



                                                                                  
 
 































                                                               
                                 







                                                           
                                                       

                                           

                                

                                                                      






























































                                                                                          
                               
                                                      
                                                                                   



                                                                                       
                                  


                                                 
 




                                                         
    


                                    
                                    
                                 


                                                 

                                           

                            
















                                                                                           










                                                                  



                                                   


























































                                                                                   
                                                                
                                                                    
                                                                             

                                                                      
                                                                              
                                                                                                
                                                                             

                                                                                  




                                                                                     



                                                         
                                               

                                                                      
                                                   

                                                                                                                 














                                            








                                                    




                        
%%
%% %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'.