%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2007-2018. 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% %% %%---------------------------------------------------------------------- %% Purpose: Storage for trusted certificates %%---------------------------------------------------------------------- -module(ssl_pkix_db). -include("ssl_internal.hrl"). -include_lib("public_key/include/public_key.hrl"). -include_lib("kernel/include/file.hrl"). -include_lib("kernel/include/logger.hrl"). -export([create/1, create_pem_cache/1, add_crls/3, remove_crls/2, remove/1, add_trusted_certs/3, extract_trusted_certs/1, remove_trusted_certs/2, insert/3, remove/2, clear/1, db_size/1, ref_count/3, lookup_trusted_cert/4, foldl/3, select_cert_by_issuer/2, decode_pem_file/1, lookup/2]). %%==================================================================== %% Internal application API %%==================================================================== %%-------------------------------------------------------------------- -spec create(atom()) -> [db_handle(),...]. %% %% Description: Creates a new certificate db. %% Note: lookup_trusted_cert/4 may be called from any process but only %% the process that called create may call the other functions. %%-------------------------------------------------------------------- create(PEMCacheName) -> [%% Let connection process delete trusted certs %% that can only belong to one connection. (Supplied directly %% on DER format to ssl:connect/listen.) ets:new(ssl_otp_cacertificate_db, [set, public]), %% Let connection processes call ref_count/3 directly {ets:new(ssl_otp_ca_file_ref, [set, public]), ets:new(ssl_otp_ca_ref_file_mapping, [set, protected]) }, %% Lookups in named table owned by ssl_pem_cache process PEMCacheName, %% Default cache {ets:new(ssl_otp_crl_cache, [set, protected]), ets:new(ssl_otp_crl_issuer_mapping, [bag, protected])} ]. create_pem_cache(Name) -> ets:new(Name, [named_table, set, protected]). %%-------------------------------------------------------------------- -spec remove([db_handle()]) -> ok. %% %% Description: Removes database db %%-------------------------------------------------------------------- remove(Dbs) -> lists:foreach(fun({Db0, Db1}) -> true = ets:delete(Db0), true = ets:delete(Db1); (undefined) -> ok; (Name) when is_atom(Name) -> NormalName = ssl_pem_cache:name(normal), DistName = ssl_pem_cache:name(dist), case Name of NormalName -> ok; DistName -> ok; _ -> true = ets:delete(Name) end; (Db) -> true = ets:delete(Db) end, Dbs). %%-------------------------------------------------------------------- -spec lookup_trusted_cert(db_handle(), certdb_ref(), serialnumber(), issuer()) -> undefined | {ok, {der_cert(), #'OTPCertificate'{}}}. %% %% Description: Retrives the trusted certificate identified by %% . Ref is used as it is specified %% for each connection which certificates are trusted. %%-------------------------------------------------------------------- lookup_trusted_cert(DbHandle, Ref, SerialNumber, Issuer) when is_reference(Ref) -> case lookup({Ref, SerialNumber, Issuer}, DbHandle) of undefined -> undefined; [Certs] -> {ok, Certs} end; lookup_trusted_cert(_DbHandle, {extracted,Certs}, SerialNumber, Issuer) -> try [throw(Cert) || {decoded, {{_Ref,CertSerial,CertIssuer}, Cert}} <- Certs, CertSerial =:= SerialNumber, CertIssuer =:= Issuer], undefined catch Cert -> {ok, Cert} end. %%-------------------------------------------------------------------- -spec add_trusted_certs(pid(), {erlang:timestamp(), string()} | {der, list()}, [db_handle()]) -> {ok, [db_handle()]}. %% %% Description: Adds the trusted certificates from file to the %% runtime database. Returns Ref that should be handed to lookup_trusted_cert %% together with the cert serialnumber and issuer. %%-------------------------------------------------------------------- add_trusted_certs(_Pid, {extracted, _} = Certs, _) -> {ok, Certs}; add_trusted_certs(_Pid, {der, DerList}, [CertDb, _,_ | _]) -> NewRef = make_ref(), add_certs_from_der(DerList, NewRef, CertDb), {ok, NewRef}; add_trusted_certs(_Pid, File, [ _, {RefDb, FileMapDb} | _] = Db) -> case lookup(File, FileMapDb) of [Ref] -> ref_count(Ref, RefDb, 1), {ok, Ref}; undefined -> new_trusted_cert_entry(File, Db) end. extract_trusted_certs({der, DerList}) -> {ok, {extracted, certs_from_der(DerList)}}; extract_trusted_certs(File) -> case file:read_file(File) of {ok, PemBin} -> Content = public_key:pem_decode(PemBin), DerList = [Cert || {'Certificate', Cert, not_encrypted} <- Content], {ok, {extracted, certs_from_der(DerList)}}; Error -> %% Have to simulate a failure happening in a server for %% external handlers. {error, {badmatch, Error}} end. -spec decode_pem_file(binary()) -> {ok, term()} | {error, term()}. decode_pem_file(File) -> case file:read_file(File) of {ok, PemBin} -> Content = public_key:pem_decode(PemBin), {ok, Content}; Error -> %% Have to simulate a failure happening in a server for %% external handlers. {error, {badmatch, Error}} end. %%-------------------------------------------------------------------- -spec remove_trusted_certs(reference(), db_handle()) -> ok. %% %% Description: Removes all trusted certificates refernced by . %%-------------------------------------------------------------------- remove_trusted_certs(Ref, CertsDb) -> remove_certs(Ref, CertsDb). %%-------------------------------------------------------------------- -spec remove(term(), db_handle()) -> ok. %% %% Description: Removes an element in a . %%-------------------------------------------------------------------- remove(Key, Db) -> ets:delete(Db, Key), ok. %%-------------------------------------------------------------------- -spec remove(term(), term(), db_handle()) -> ok. %% %% Description: Removes an element in a . %%-------------------------------------------------------------------- remove(Key, Data, Db) -> ets:delete_object(Db, {Key, Data}), ok. %%-------------------------------------------------------------------- -spec lookup(term(), db_handle()) -> [term()] | undefined. %% %% Description: Looks up an element in a . %%-------------------------------------------------------------------- lookup(Key, Db) -> case ets:lookup(Db, Key) of [] -> undefined; Contents -> Pick = fun({_, Data}) -> Data; ({_,_,Data}) -> Data end, [Pick(Data) || Data <- Contents] end. %%-------------------------------------------------------------------- -spec foldl(fun((_,_) -> term()), term(), db_handle()) -> term(). %% %% Description: Calls Fun(Elem, AccIn) on successive elements of the %% cache, starting with AccIn == Acc0. Fun/2 must return a new %% accumulator which is passed to the next call. The function returns %% the final value of the accumulator. Acc0 is returned if the certifate %% db is empty. %%-------------------------------------------------------------------- foldl(Fun, Acc0, Cache) -> ets:foldl(Fun, Acc0, Cache). select_cert_by_issuer(Cache, Issuer) -> ets:select(Cache, [{{{'_','_', Issuer},{'_', '$1'}},[],['$$']}]). %%-------------------------------------------------------------------- -spec ref_count(term(), db_handle(), integer()) -> integer(). %% %% Description: Updates a reference counter in a . %%-------------------------------------------------------------------- ref_count({extracted, _}, _Db, _N) -> not_cached; ref_count(Key, {Db, _}, N) -> ref_count(Key, Db, N); ref_count(Key, Db, N) -> ets:update_counter(Db,Key,N). %%-------------------------------------------------------------------- -spec clear(db_handle()) -> ok. %% %% Description: Clears the cache %%-------------------------------------------------------------------- clear(Db) -> true = ets:delete_all_objects(Db), ok. %%-------------------------------------------------------------------- -spec db_size(db_handle()) -> integer(). %% %% Description: Returns the size of the db %%-------------------------------------------------------------------- db_size(Db) -> ets:info(Db, size). %%-------------------------------------------------------------------- -spec insert(Key::term(), Data::term(), Db::db_handle()) -> ok. %% %% Description: Inserts data into %%-------------------------------------------------------------------- insert(Key, Data, Db) -> true = ets:insert(Db, {Key, Data}), ok. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- init_ref_db(Ref, File, {RefDb, FileMapDb}) -> true = ets:insert(RefDb, {Ref, 1}), true = ets:insert(FileMapDb, {File, Ref}). remove_certs(Ref, CertsDb) -> true = ets:match_delete(CertsDb, {{Ref, '_', '_'}, '_'}), ok. add_certs_from_der(DerList, Ref, CertsDb) -> Add = fun(Cert) -> add_certs(Cert, Ref, CertsDb) end, [Add(Cert) || Cert <- DerList], ok. certs_from_der(DerList) -> Ref = make_ref(), [Decoded || Cert <- DerList, Decoded <- [decode_certs(Ref, Cert)], Decoded =/= undefined]. add_certs_from_pem(PemEntries, Ref, CertsDb) -> Add = fun(Cert) -> add_certs(Cert, Ref, CertsDb) end, [Add(Cert) || {'Certificate', Cert, not_encrypted} <- PemEntries], ok. add_certs(Cert, Ref, CertsDb) -> try {decoded, {Key, Val}} = decode_certs(Ref, Cert), insert(Key, Val, CertsDb) catch error:_ -> ok end. decode_certs(Ref, Cert) -> try ErlCert = public_key:pkix_decode_cert(Cert, otp), TBSCertificate = ErlCert#'OTPCertificate'.tbsCertificate, SerialNumber = TBSCertificate#'OTPTBSCertificate'.serialNumber, Issuer = public_key:pkix_normalize_name( TBSCertificate#'OTPTBSCertificate'.issuer), {decoded, {{Ref, SerialNumber, Issuer}, {Cert, ErlCert}}} catch error:_ -> Report = io_lib:format("SSL WARNING: Ignoring a CA cert as " "it could not be correctly decoded.~n", []), ?LOG_NOTICE(Report), undefined end. new_trusted_cert_entry(File, [CertsDb, RefsDb, _ | _]) -> case decode_pem_file(File) of {ok, Content} -> Ref = make_ref(), init_ref_db(Ref, File, RefsDb), ok = ssl_pem_cache:insert(File, Content), add_certs_from_pem(Content, Ref, CertsDb), {ok, Ref}; Error -> Error end. add_crls([_,_,_, {_, Mapping} | _], ?NO_DIST_POINT, CRLs) -> [add_crls(CRL, Mapping) || CRL <- CRLs]; add_crls([_,_,_, {Cache, Mapping} | _], Path, CRLs) -> insert(Path, CRLs, Cache), [add_crls(CRL, Mapping) || CRL <- CRLs]. add_crls(CRL, Mapping) -> insert(crl_issuer(CRL), CRL, Mapping). remove_crls([_,_,_, {_, Mapping} | _], {?NO_DIST_POINT, CRLs}) -> [rm_crls(CRL, Mapping) || CRL <- CRLs]; remove_crls([_,_,_, {Cache, Mapping} | _], Path) -> case lookup(Path, Cache) of undefined -> ok; CRLs -> remove(Path, Cache), [rm_crls(CRL, Mapping) || CRL <- CRLs] end. rm_crls(CRL, Mapping) -> remove(crl_issuer(CRL), CRL, Mapping). crl_issuer(DerCRL) -> CRL = public_key:der_decode('CertificateList', DerCRL), TBSCRL = CRL#'CertificateList'.tbsCertList, TBSCRL#'TBSCertList'.issuer.