Skip to content

Commit

Permalink
public_key, ssl: Provide certitifate test data generation function in…
Browse files Browse the repository at this point in the history
… public_key

The ssl application uses the new function in many of its test cases.
  • Loading branch information
IngelaAndin committed Sep 20, 2017
1 parent bec2edd commit af68c9d
Show file tree
Hide file tree
Showing 8 changed files with 855 additions and 521 deletions.
86 changes: 84 additions & 2 deletions lib/public_key/doc/src/public_key.xml
Expand Up @@ -119,6 +119,10 @@
<tag><c>ec_private_key() =</c></tag>
<item><p><c>#'ECPrivateKey'{}</c></p></item>

<tag><c>key_params() =</c></tag>
<item><p> #'DHParameter'{} | {namedCurve, oid()} | #'ECParameters'{}
| {rsa, Size::integer(), PubExp::integer()} </p></item>

<tag><c>public_crypt_options() =</c></tag>
<item><p><c>[{rsa_pad, rsa_padding()}]</c></p></item>

Expand Down Expand Up @@ -347,8 +351,7 @@
<name>generate_key(Params) -> {Public::binary(), Private::binary()} | #'ECPrivateKey'{} | #'RSAPrivateKey'{}</name>
<fsummary>Generates a new keypair.</fsummary>
<type>
<v>Params = #'DHParameter'{} | {namedCurve, oid()} | #'ECParameters'{}
| {rsa, Size::integer(), PubExp::integer} </v>
<v>Params = key_params()</v>
</type>
<desc>
<p>Generates a new keypair. Note that except for Diffie-Hellman
Expand Down Expand Up @@ -769,6 +772,85 @@ fun(#'DistributionPoint'{}, #'CertificateList'{},
</desc>
</func>

<func>
<name>pkix_test_data(Options) -> Config </name>
<fsummary>Creates certificate test data.</fsummary>
<type>
<v>Options = #{chain_type() := chain_opts()} </v>
<d>Options for ROOT, Intermediate and Peer certs</d>

<v>chain_type() = server_chain | client_chain </v>

<v>chain_opts() = #{chain_end() := [cert_opt()],
intermediates => [[cert_opt()]]}</v>
<d>A valid chain must have at least a ROOT and a peer cert</d>

<v>chain_end() = root | peer </v>

<v>cert_opt() = {Key, Value}</v>
<d>For available options see <seealso marker="#cert_opt"> cert_opt()</seealso> below.</d>

<v>Config = #{server_config := [conf_opt()],
client_config := [conf_opt()]}</v>

<v>conf_opt() = {cert, der_encoded()} | {key, der_encoded()} |{cacerts, [der_encoded()]}</v>
<d>This is a subset of the type <seealso marker="ssl#type-ssloption"> ssl:ssl_option()</seealso> </d>
</type>

<desc>
<p>Creates certificate test data to facilitate automated testing
of applications using X509-certificates often through
SSL/TLS. The test data can be used when you have control
over both the client and the server in a test scenario.
</p>

<p> The <marker id="cert_opt"/> cert_opt() type consists of the following options: </p>
<taglist>
<tag> {digest, digest_type()}</tag>
<item><p>Hash algorithm to be used for
signing the certificate together with the key option. Defaults to sha that is sha1.
</p></item>
<tag> {key, key_params() | private_key()}</tag>
<item><p>Parameters to be used to call public_key:generate_key/1, to generate a key, or an existing
key. Defaults to generating an ECDSA key. Note this could fail if Erlang/OTP is compiled with a very old
cryptolib.</p></item>
<tag> {validity, {From::erlang:timestamp(), To::erlang:timestamp()}} </tag>
<item><p>The validity period of the certificate.</p></item>
<tag> {extensions, [#'Extension'{}]}</tag>
<item><p> Extensions to include in the certificate.</p>

<p>Default extensions included in CA certificates if not
otherwise specified are: </p>
<code>[#'Extension'{extnID = ?'id-ce-keyUsage',
extnValue = [keyCertSign, cRLSign],
critical = false},
#'Extension'{extnID = ?'id-ce-basicConstraints',
extnValue = #'BasicConstraints'{cA = true},
critical = true}]
</code>

<p>Default extensions included in the server peer cert if not
otherwise specified are: </p>
<code>[#'Extension'{extnID = ?'id-ce-keyUsage',
extnValue = [digitalSignature, keyAgreement],
critical = false},
#'Extension'{extnID = ?'id-ce-subjectAltName',
extnValue = [{dNSName, Hostname}],
critical = false}]
</code>
<p>Hostname is the result of calling net_adm:localhost() in the Erlang node
where this funcion is called.
</p></item>

</taglist>

<note><p>
Note that the generated certificates and keys does not provide a formally correct PKIX-trust-chain
and they can not be used to achieve real security. This function is provided for testing purposes only.
</p></note>
</desc>
</func>

<func>
<name>pkix_verify(Cert, Key) -> boolean()</name>
<fsummary>Verifies PKIX x.509 certificate signature.</fsummary>
Expand Down
253 changes: 250 additions & 3 deletions lib/public_key/src/pubkey_cert.erl
Expand Up @@ -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
%%====================================================================

%%--------------------------------------------------------------------
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

0 comments on commit af68c9d

Please sign in to comment.