aboutsummaryrefslogblamecommitdiffstats
path: root/lib/ssl/test/x509_test.erl
blob: 5cd5c8eca796c4d3252eb078bdc21c5c02cae69c (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/4]).

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, CertFileBase, KeyFileBase, CAFileBase) ->
    ServerConf = proplists:get_value(server_config, GenCertData),
    ClientConf = proplists:get_value(client_config, GenCertData),
    
    ServerCaCertFile = filename:join("server_", CAFileBase),
    ServerCertFile = filename:join("server_", CertFileBase),
    ServerKeyFile = filename:join("server_", KeyFileBase),
    
    ClientCaCertFile = filename:join("client_", CAFileBase),
    ClientCertFile = filename:join("client_", CertFileBase),
    ClientKeyFile = filename:join("client_", KeyFileBase),
    
    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(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, [{namedCurve, hd(tls_v1:ecc_curves(0))}, 
                                                    {namedCurve, hd(tls_v1:ecc_curves(0))}]),
    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(Opts) ->
    case proplists:get_value(extensions, Opts, []) of
	false -> 
	    asn1_NOVALUE;
	Exts  -> 
	    lists:flatten([extension(Ext) || Ext <- default_extensions(Exts)])
    end.

default_extensions(Exts) ->
    Def = [{key_usage,undefined}, 
	   {subject_altname, undefined},
	   {issuer_altname, undefined},
	   {basic_constraints, default},
	   {name_constraints, undefined},
	   {policy_constraints, undefined},
	   {ext_key_usage, undefined},
	   {inhibit_any, undefined},
	   {auth_key_id, undefined},
	   {subject_key_id, undefined},
	   {policy_mapping, undefined}],
    Filter = fun({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({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) ->
    Cert = cert(Role, public_key:pkix_decode_cert(IssuerCert, otp), IssuerKey, Key, "admin", " Peer cert", Opts),
    [{Cert, Key}, {IssuerCert, IssuerKey} | Acc];
cert_chain(Role, IssuerCert, IssuerKey, Opts, [Key | Keys], N, Acc) ->
    Cert = cert(Role, public_key:pkix_decode_cert(IssuerCert, otp), IssuerKey, Key, "webadmin", 
                " Intermidiate CA " ++ integer_to_list(N), Opts),
    cert_chain(Role, Cert, Key, Opts, Keys, N+1, [{IssuerCert, IssuerKey} | Acc]).
        
cert(Role, #'OTPCertificate'{tbsCertificate = #'OTPTBSCertificate'{subject = Issuer}}, 
     PrivKey, Key, Contact, Name, Opts) ->
    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(Opts)
              },
    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).