mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-12 15:57:31 +01:00
880 lines
26 KiB
Erlang
880 lines
26 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net>
|
|
%%% Created : 4 Mar 2017 by Evgeny Khramtsov <ekhramtsov@process-one.net>
|
|
%%%
|
|
%%%
|
|
%%% ejabberd, Copyright (C) 2002-2017 ProcessOne
|
|
%%%
|
|
%%% This program is free software; you can redistribute it and/or
|
|
%%% modify it under the terms of the GNU General Public License as
|
|
%%% published by the Free Software Foundation; either version 2 of the
|
|
%%% License, or (at your option) any later version.
|
|
%%%
|
|
%%% This program is distributed in the hope that it will be useful,
|
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
%%% General Public License for more details.
|
|
%%%
|
|
%%% You should have received a copy of the GNU General Public License along
|
|
%%% with this program; if not, write to the Free Software Foundation, Inc.,
|
|
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
%%%
|
|
%%%-------------------------------------------------------------------
|
|
-module(ejabberd_pkix).
|
|
|
|
-behaviour(gen_server).
|
|
-behaviour(ejabberd_config).
|
|
|
|
%% API
|
|
-export([start_link/0, add_certfile/1, format_error/1, opt_type/1,
|
|
get_certfile/1, try_certfile/1, route_registered/1,
|
|
config_reloaded/0, certs_dir/0, ca_file/0]).
|
|
%% gen_server callbacks
|
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
|
terminate/2, code_change/3]).
|
|
|
|
-include_lib("public_key/include/public_key.hrl").
|
|
-include("logger.hrl").
|
|
|
|
-record(state, {validate = true :: boolean(),
|
|
notify = false :: boolean(),
|
|
paths = [] :: [file:filename()],
|
|
certs = #{} :: map(),
|
|
graph :: digraph:graph(),
|
|
keys = [] :: [public_key:private_key()]}).
|
|
|
|
-type state() :: #state{}.
|
|
-type cert() :: #'OTPCertificate'{}.
|
|
-type priv_key() :: public_key:private_key().
|
|
-type pub_key() :: #'RSAPublicKey'{} | {integer(), #'Dss-Parms'{}} | #'ECPoint'{}.
|
|
-type bad_cert_reason() :: cert_expired | invalid_issuer | invalid_signature |
|
|
name_not_permitted | missing_basic_constraint |
|
|
invalid_key_usage | selfsigned_peer | unknown_sig_algo |
|
|
unknown_ca | missing_priv_key.
|
|
-type bad_cert() :: {bad_cert, bad_cert_reason()}.
|
|
-type cert_error() :: not_cert | not_der | not_pem | encrypted.
|
|
-export_type([cert_error/0]).
|
|
|
|
-define(CA_CACHE, ca_cache).
|
|
|
|
%%%===================================================================
|
|
%%% API
|
|
%%%===================================================================
|
|
-spec add_certfile(filename:filename())
|
|
-> ok | {error, cert_error() | file:posix()}.
|
|
add_certfile(Path) ->
|
|
gen_server:call(?MODULE, {add_certfile, prep_path(Path)}).
|
|
|
|
-spec try_certfile(filename:filename()) -> binary().
|
|
try_certfile(Path0) ->
|
|
Path = prep_path(Path0),
|
|
case load_certfile(Path) of
|
|
{ok, _, _} -> Path;
|
|
{error, _} -> erlang:error(badarg)
|
|
end.
|
|
|
|
route_registered(Route) ->
|
|
gen_server:call(?MODULE, {route_registered, Route}).
|
|
|
|
-spec format_error(cert_error() | file:posix()) -> string().
|
|
format_error(not_cert) ->
|
|
"no PEM encoded certificates found";
|
|
format_error(not_pem) ->
|
|
"failed to decode from PEM format";
|
|
format_error(not_der) ->
|
|
"failed to decode from DER format";
|
|
format_error(encrypted) ->
|
|
"encrypted certificate";
|
|
format_error({bad_cert, cert_expired}) ->
|
|
"certificate is no longer valid as its expiration date has passed";
|
|
format_error({bad_cert, invalid_issuer}) ->
|
|
"certificate issuer name does not match the name of the "
|
|
"issuer certificate";
|
|
format_error({bad_cert, invalid_signature}) ->
|
|
"certificate was not signed by its issuer certificate";
|
|
format_error({bad_cert, name_not_permitted}) ->
|
|
"invalid Subject Alternative Name extension";
|
|
format_error({bad_cert, missing_basic_constraint}) ->
|
|
"certificate, required to have the basic constraints extension, "
|
|
"does not have a basic constraints extension";
|
|
format_error({bad_cert, invalid_key_usage}) ->
|
|
"certificate key is used in an invalid way according "
|
|
"to the key-usage extension";
|
|
format_error({bad_cert, selfsigned_peer}) ->
|
|
"self-signed certificate";
|
|
format_error({bad_cert, unknown_sig_algo}) ->
|
|
"certificate is signed using unknown algorithm";
|
|
format_error({bad_cert, unknown_ca}) ->
|
|
"certificate is signed by unknown CA";
|
|
format_error({bad_cert, missing_priv_key}) ->
|
|
"no matching private key found for certificate in the chain";
|
|
format_error({bad_cert, Unknown}) ->
|
|
lists:flatten(io_lib:format("~w", [Unknown]));
|
|
format_error(Why) ->
|
|
case file:format_error(Why) of
|
|
"unknown POSIX error" ->
|
|
atom_to_list(Why);
|
|
Reason ->
|
|
Reason
|
|
end.
|
|
|
|
-spec get_certfile(binary()) -> {ok, binary()} | error.
|
|
get_certfile(Domain) ->
|
|
case ejabberd_idna:domain_utf8_to_ascii(Domain) of
|
|
false ->
|
|
error;
|
|
ASCIIDomain ->
|
|
case ets:lookup(?MODULE, ASCIIDomain) of
|
|
[] ->
|
|
case binary:split(ASCIIDomain, <<".">>, [trim]) of
|
|
[_, Host] ->
|
|
case ets:lookup(?MODULE, <<"*.", Host/binary>>) of
|
|
[{_, Path}|_] ->
|
|
{ok, Path};
|
|
[] ->
|
|
error
|
|
end;
|
|
_ ->
|
|
error
|
|
end;
|
|
[{_, Path}|_] ->
|
|
{ok, Path}
|
|
end
|
|
end.
|
|
|
|
start_link() ->
|
|
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
|
|
|
config_reloaded() ->
|
|
case use_cache() of
|
|
true -> init_cache();
|
|
false -> delete_cache()
|
|
end,
|
|
gen_server:call(?MODULE, config_reloaded, 60000).
|
|
|
|
opt_type(ca_path) ->
|
|
fun(Path) -> binary_to_list(Path) end;
|
|
opt_type(ca_file) ->
|
|
fun(Path) ->
|
|
binary_to_list(misc:try_read_file(Path))
|
|
end;
|
|
opt_type(certfiles) ->
|
|
fun(CertList) ->
|
|
[binary_to_list(Path) || Path <- CertList]
|
|
end;
|
|
opt_type(O) when O == c2s_certfile; O == s2s_certfile; O == domain_certfile ->
|
|
fun(File) ->
|
|
?WARNING_MSG("option '~s' is deprecated, use 'certfiles' instead", [O]),
|
|
misc:try_read_file(File)
|
|
end;
|
|
opt_type(_) ->
|
|
[ca_path, ca_file, certfiles, c2s_certfile, s2s_certfile, domain_certfile].
|
|
|
|
%%%===================================================================
|
|
%%% gen_server callbacks
|
|
%%%===================================================================
|
|
init([]) ->
|
|
Notify = start_fs(),
|
|
process_flag(trap_exit, true),
|
|
ets:new(?MODULE, [named_table, public]),
|
|
ejabberd_hooks:add(route_registered, ?MODULE, route_registered, 50),
|
|
ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 30),
|
|
Validate = case os:type() of
|
|
{win32, _} -> false;
|
|
_ ->
|
|
code:ensure_loaded(public_key),
|
|
erlang:function_exported(
|
|
public_key, short_name_hash, 1)
|
|
end,
|
|
if Validate -> check_ca();
|
|
true -> ok
|
|
end,
|
|
G = digraph:new([acyclic]),
|
|
init_cache(),
|
|
State = #state{validate = Validate, notify = Notify, graph = G},
|
|
case filelib:ensure_dir(filename:join(certs_dir(), "foo")) of
|
|
ok ->
|
|
clean_dir(certs_dir()),
|
|
case add_certfiles(State) of
|
|
{ok, State1} ->
|
|
{ok, State1};
|
|
{error, Why} ->
|
|
{stop, Why}
|
|
end;
|
|
{error, Why} ->
|
|
?CRITICAL_MSG("Failed to create directory ~s: ~s",
|
|
[certs_dir(), file:format_error(Why)]),
|
|
{stop, Why}
|
|
end.
|
|
|
|
handle_call({add_certfile, Path}, _, State) ->
|
|
case add_certfile(Path, State) of
|
|
{ok, State1} ->
|
|
if State /= State1 ->
|
|
case build_chain_and_check(State1) of
|
|
{ok, State2} ->
|
|
{reply, ok, State2};
|
|
Err ->
|
|
{reply, Err, State1}
|
|
end;
|
|
true ->
|
|
{reply, ok, State1}
|
|
end;
|
|
{Err, State1} ->
|
|
{reply, Err, State1}
|
|
end;
|
|
handle_call({route_registered, Host}, _, State) ->
|
|
case add_certfiles(Host, State) of
|
|
{ok, NewState} ->
|
|
case get_certfile(Host) of
|
|
{ok, _} -> ok;
|
|
error ->
|
|
?WARNING_MSG("No certificate found matching '~s': strictly "
|
|
"configured clients or servers will reject "
|
|
"connections with this host; obtain "
|
|
"a certificate for this (sub)domain from any "
|
|
"trusted CA such as Let's Encrypt "
|
|
"(www.letsencrypt.org)",
|
|
[Host])
|
|
end,
|
|
{reply, ok, NewState};
|
|
{error, _} ->
|
|
{reply, ok, State}
|
|
end;
|
|
handle_call(config_reloaded, _From, State) ->
|
|
State1 = State#state{paths = [], certs = #{}, keys = []},
|
|
case add_certfiles(State1) of
|
|
{ok, State2} ->
|
|
{reply, ok, State2};
|
|
{error, _} = Err ->
|
|
{reply, Err, State}
|
|
end;
|
|
handle_call(_Request, _From, State) ->
|
|
Reply = ok,
|
|
{reply, Reply, State}.
|
|
|
|
handle_cast(_Msg, State) ->
|
|
{noreply, State}.
|
|
|
|
handle_info({_, {fs, file_event}, {File, Events}}, State) ->
|
|
?DEBUG("got FS events for ~s: ~p", [File, Events]),
|
|
Path = iolist_to_binary(File),
|
|
case lists:member(modified, Events) of
|
|
true ->
|
|
case lists:member(Path, State#state.paths) of
|
|
true ->
|
|
handle_cast(config_reloaded, State);
|
|
false ->
|
|
{noreply, State}
|
|
end;
|
|
false ->
|
|
{noreply, State}
|
|
end;
|
|
handle_info(_Info, State) ->
|
|
?WARNING_MSG("unexpected info: ~p", [_Info]),
|
|
{noreply, State}.
|
|
|
|
terminate(_Reason, _State) ->
|
|
ejabberd_hooks:delete(route_registered, ?MODULE, route_registered, 50),
|
|
ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 30),
|
|
clean_dir(certs_dir()).
|
|
|
|
code_change(_OldVsn, State, _Extra) ->
|
|
{ok, State}.
|
|
|
|
%%%===================================================================
|
|
%%% Internal functions
|
|
%%%===================================================================
|
|
-spec certfiles_from_config_options() -> [atom()].
|
|
certfiles_from_config_options() ->
|
|
[c2s_certfile, s2s_certfile, domain_certfile].
|
|
|
|
-spec get_certfiles_from_config_options(state()) -> [binary()].
|
|
get_certfiles_from_config_options(_State) ->
|
|
Global = case ejabberd_config:get_option(certfiles) of
|
|
undefined ->
|
|
[];
|
|
Paths ->
|
|
lists:flatmap(fun wildcard/1, Paths)
|
|
end,
|
|
Local = lists:flatmap(
|
|
fun(OptHost) ->
|
|
case ejabberd_config:get_option(OptHost) of
|
|
undefined -> [];
|
|
Path -> [Path]
|
|
end
|
|
end, [{Opt, Host}
|
|
|| Opt <- certfiles_from_config_options(),
|
|
Host <- ejabberd_config:get_myhosts()]),
|
|
[iolist_to_binary(P) || P <- lists:usort(Local ++ Global)].
|
|
|
|
-spec add_certfiles(state()) -> {ok, state()} | {error, bad_cert()}.
|
|
add_certfiles(State) ->
|
|
?DEBUG("Reading certificates", []),
|
|
Paths = get_certfiles_from_config_options(State),
|
|
State1 = lists:foldl(
|
|
fun(Path, Acc) ->
|
|
{_, NewAcc} = add_certfile(Path, Acc),
|
|
NewAcc
|
|
end, State, Paths),
|
|
case build_chain_and_check(State1) of
|
|
ok -> {ok, State1};
|
|
{error, _} = Err -> Err
|
|
end.
|
|
|
|
-spec add_certfiles(binary(), state()) -> {ok, state()} | {error, bad_cert()}.
|
|
add_certfiles(Host, State) ->
|
|
State1 = lists:foldl(
|
|
fun(Opt, AccState) ->
|
|
case ejabberd_config:get_option({Opt, Host}) of
|
|
undefined -> AccState;
|
|
Path ->
|
|
{_, NewAccState} = add_certfile(Path, AccState),
|
|
NewAccState
|
|
end
|
|
end, State, certfiles_from_config_options()),
|
|
if State /= State1 ->
|
|
case build_chain_and_check(State1) of
|
|
ok -> {ok, State1};
|
|
{error, _} = Err -> Err
|
|
end;
|
|
true ->
|
|
{ok, State}
|
|
end.
|
|
|
|
-spec add_certfile(file:filename_all(), state()) -> {ok, state()} |
|
|
{{error, cert_error()}, state()}.
|
|
add_certfile(Path, State) ->
|
|
case lists:member(Path, State#state.paths) of
|
|
true ->
|
|
{ok, State};
|
|
false ->
|
|
case load_certfile(Path) of
|
|
{ok, Certs, Keys} ->
|
|
NewCerts = lists:foldl(
|
|
fun(Cert, Acc) ->
|
|
maps:put(Cert, Path, Acc)
|
|
end, State#state.certs, Certs),
|
|
{ok, State#state{paths = [Path|State#state.paths],
|
|
certs = NewCerts,
|
|
keys = Keys ++ State#state.keys}};
|
|
{error, Why} = Err ->
|
|
?ERROR_MSG("failed to read certificate from ~s: ~s",
|
|
[Path, format_error(Why)]),
|
|
{Err, State}
|
|
end
|
|
end.
|
|
|
|
-spec build_chain_and_check(state()) -> ok | {error, bad_cert()}.
|
|
build_chain_and_check(State) ->
|
|
?DEBUG("Building certificates graph", []),
|
|
CertPaths = get_cert_paths(maps:keys(State#state.certs), State#state.graph),
|
|
?DEBUG("Finding matched certificate keys", []),
|
|
case match_cert_keys(CertPaths, State#state.keys) of
|
|
{ok, Chains} ->
|
|
?DEBUG("Storing certificate chains", []),
|
|
CertFilesWithDomains = store_certs(Chains, []),
|
|
ets:delete_all_objects(?MODULE),
|
|
lists:foreach(
|
|
fun({Path, Domain}) ->
|
|
ets:insert(?MODULE, {Domain, Path})
|
|
end, CertFilesWithDomains),
|
|
?DEBUG("Validating certificates", []),
|
|
Errors = validate(CertPaths, State#state.validate),
|
|
?DEBUG("Subscribing to file events", []),
|
|
subscribe(State),
|
|
lists:foreach(
|
|
fun({Cert, Why}) ->
|
|
Path = maps:get(Cert, State#state.certs),
|
|
?WARNING_MSG("Failed to validate certificate from ~s: ~s",
|
|
[Path, format_error(Why)])
|
|
end, Errors);
|
|
{error, Cert, Why} ->
|
|
Path = maps:get(Cert, State#state.certs),
|
|
?ERROR_MSG("Failed to build certificate chain for ~s: ~s",
|
|
[Path, format_error(Why)]),
|
|
{error, Why}
|
|
end.
|
|
|
|
-spec store_certs([{[cert()], priv_key()}],
|
|
[{binary(), binary()}]) -> [{binary(), binary()}].
|
|
store_certs([{Certs, Key}|Chains], Acc) ->
|
|
CertPEMs = public_key:pem_encode(
|
|
lists:map(
|
|
fun(Cert) ->
|
|
Type = element(1, Cert),
|
|
DER = public_key:pkix_encode(Type, Cert, otp),
|
|
{'Certificate', DER, not_encrypted}
|
|
end, Certs)),
|
|
KeyPEM = public_key:pem_encode(
|
|
[{element(1, Key),
|
|
public_key:der_encode(element(1, Key), Key),
|
|
not_encrypted}]),
|
|
PEMs = <<CertPEMs/binary, KeyPEM/binary>>,
|
|
Cert = hd(Certs),
|
|
Domains = xmpp_stream_pkix:get_cert_domains(Cert),
|
|
FileName = filename:join(certs_dir(), str:sha(PEMs)),
|
|
case file:write_file(FileName, PEMs) of
|
|
ok ->
|
|
file:change_mode(FileName, 8#600),
|
|
NewAcc = [{FileName, Domain} || Domain <- Domains] ++ Acc,
|
|
store_certs(Chains, NewAcc);
|
|
{error, Why} ->
|
|
?ERROR_MSG("Failed to write to ~s: ~s",
|
|
[FileName, file:format_error(Why)]),
|
|
store_certs(Chains, [])
|
|
end;
|
|
store_certs([], Acc) ->
|
|
Acc.
|
|
|
|
-spec load_certfile(file:filename_all()) -> {ok, [cert()], [priv_key()]} |
|
|
{error, cert_error() | file:posix()}.
|
|
load_certfile(Path) ->
|
|
try
|
|
{ok, Data} = file:read_file(Path),
|
|
pem_decode(Data)
|
|
catch _:{badmatch, {error, _} = Err} ->
|
|
Err
|
|
end.
|
|
|
|
-spec pem_decode(binary()) -> {ok, [cert()], [priv_key()]} |
|
|
{error, cert_error()}.
|
|
pem_decode(Data) ->
|
|
try public_key:pem_decode(Data) of
|
|
PemEntries ->
|
|
case decode_certs(PemEntries) of
|
|
{error, _} = Err ->
|
|
Err;
|
|
Objects ->
|
|
case lists:partition(
|
|
fun(#'OTPCertificate'{}) -> true;
|
|
(_) -> false
|
|
end, Objects) of
|
|
{[], []} ->
|
|
{error, not_cert};
|
|
{Certs, PrivKeys} ->
|
|
{ok, Certs, PrivKeys}
|
|
end
|
|
end
|
|
catch _:_ ->
|
|
{error, not_pem}
|
|
end.
|
|
|
|
-spec decode_certs([public_key:pem_entry()]) -> {[cert()], [priv_key()]} |
|
|
{error, not_der | encrypted}.
|
|
decode_certs(PemEntries) ->
|
|
try lists:foldr(
|
|
fun(_, {error, _} = Err) ->
|
|
Err;
|
|
({_, _, Flag}, _) when Flag /= not_encrypted ->
|
|
{error, encrypted};
|
|
({'Certificate', Der, _}, Acc) ->
|
|
[public_key:pkix_decode_cert(Der, otp)|Acc];
|
|
({'PrivateKeyInfo', Der, not_encrypted}, Acc) ->
|
|
#'PrivateKeyInfo'{privateKeyAlgorithm =
|
|
#'PrivateKeyInfo_privateKeyAlgorithm'{
|
|
algorithm = Algo},
|
|
privateKey = Key} =
|
|
public_key:der_decode('PrivateKeyInfo', Der),
|
|
case Algo of
|
|
?'rsaEncryption' ->
|
|
[public_key:der_decode(
|
|
'RSAPrivateKey', iolist_to_binary(Key))|Acc];
|
|
?'id-dsa' ->
|
|
[public_key:der_decode(
|
|
'DSAPrivateKey', iolist_to_binary(Key))|Acc];
|
|
?'id-ecPublicKey' ->
|
|
[public_key:der_decode(
|
|
'ECPrivateKey', iolist_to_binary(Key))|Acc];
|
|
_ ->
|
|
Acc
|
|
end;
|
|
({Tag, Der, _}, Acc) when Tag == 'RSAPrivateKey';
|
|
Tag == 'DSAPrivateKey';
|
|
Tag == 'ECPrivateKey' ->
|
|
[public_key:der_decode(Tag, Der)|Acc];
|
|
(_, Acc) ->
|
|
Acc
|
|
end, [], PemEntries)
|
|
catch _:_ ->
|
|
{error, not_der}
|
|
end.
|
|
|
|
-spec validate([{path, [cert()]}], boolean()) -> [{cert(), bad_cert()}].
|
|
validate(Paths, true) ->
|
|
{ok, Re} = re:compile("^[a-f0-9]+\\.[0-9]+$", [unicode]),
|
|
Hashes = case file:list_dir(ca_dir()) of
|
|
{ok, Files} ->
|
|
lists:foldl(
|
|
fun(File, Acc) ->
|
|
try re:run(File, Re) of
|
|
{match, _} ->
|
|
[Hash|_] = string:tokens(File, "."),
|
|
Path = filename:join(ca_dir(), File),
|
|
dict:append(Hash, Path, Acc);
|
|
nomatch ->
|
|
Acc
|
|
catch _:badarg ->
|
|
?ERROR_MSG("Regexp failure on ~w", [File]),
|
|
Acc
|
|
end
|
|
end, dict:new(), Files);
|
|
{error, Why} ->
|
|
?ERROR_MSG("Failed to list directory ~s: ~s",
|
|
[ca_dir(), file:format_error(Why)]),
|
|
dict:new()
|
|
end,
|
|
lists:filtermap(
|
|
fun({path, Path}) ->
|
|
case validate_path(Path, Hashes) of
|
|
ok ->
|
|
false;
|
|
{error, Cert, Reason} ->
|
|
{true, {Cert, Reason}}
|
|
end
|
|
end, Paths);
|
|
validate(_, _) ->
|
|
[].
|
|
|
|
-spec validate_path([cert()], dict:dict()) -> ok | {error, cert(), bad_cert()}.
|
|
validate_path([Cert|_] = Certs, Cache) ->
|
|
case find_local_issuer(Cert, Cache) of
|
|
{ok, IssuerCert} ->
|
|
try public_key:pkix_path_validation(IssuerCert, Certs, []) of
|
|
{ok, _} ->
|
|
ok;
|
|
{error, Reason} ->
|
|
{error, Cert, Reason}
|
|
catch error:function_clause ->
|
|
case erlang:get_stacktrace() of
|
|
[{public_key, pkix_sign_types, _, _}|_] ->
|
|
{error, Cert, {bad_cert, unknown_sig_algo}};
|
|
ST ->
|
|
%% Bug in public_key application
|
|
erlang:raise(error, function_clause, ST)
|
|
end
|
|
end;
|
|
{error, Reason} ->
|
|
case public_key:pkix_is_self_signed(Cert) of
|
|
true ->
|
|
{error, Cert, {bad_cert, selfsigned_peer}};
|
|
false ->
|
|
{error, Cert, Reason}
|
|
end
|
|
end.
|
|
|
|
-spec ca_dir() -> string().
|
|
ca_dir() ->
|
|
ejabberd_config:get_option(ca_path, "/etc/ssl/certs").
|
|
|
|
-spec ca_file() -> string() | undefined.
|
|
ca_file() ->
|
|
ejabberd_config:get_option(ca_file).
|
|
|
|
-spec certs_dir() -> string().
|
|
certs_dir() ->
|
|
MnesiaDir = mnesia:system_info(directory),
|
|
filename:join(MnesiaDir, "certs").
|
|
|
|
-spec clean_dir(file:filename_all()) -> ok.
|
|
clean_dir(Dir) ->
|
|
?DEBUG("Cleaning directory ~s", [Dir]),
|
|
Files = wildcard(filename:join(Dir, "*")),
|
|
lists:foreach(
|
|
fun(Path) ->
|
|
case filelib:is_file(Path) of
|
|
true ->
|
|
file:delete(Path);
|
|
false ->
|
|
ok
|
|
end
|
|
end, Files).
|
|
|
|
-spec check_ca() -> ok.
|
|
check_ca() ->
|
|
CAFile = ca_file(),
|
|
case wildcard(filename:join(ca_dir(), "*.0")) of
|
|
[] when CAFile == undefined ->
|
|
Hint = "configuring 'ca_path' or 'ca_file' options might help",
|
|
case file:list_dir(ca_dir()) of
|
|
{error, Why} ->
|
|
?WARNING_MSG("failed to read CA directory ~s: ~s; ~s",
|
|
[ca_dir(), file:format_error(Why), Hint]);
|
|
{ok, _} ->
|
|
?WARNING_MSG("CA directory ~s doesn't contain "
|
|
"hashed certificate files; ~s",
|
|
[ca_dir(), Hint])
|
|
end;
|
|
_ ->
|
|
ok
|
|
end.
|
|
|
|
-spec find_local_issuer(cert(), dict:dict()) -> {ok, cert()} |
|
|
{error, {bad_cert, unknown_ca}}.
|
|
find_local_issuer(Cert, Hashes) ->
|
|
case find_issuer_in_dir(Cert, Hashes) of
|
|
{ok, IssuerCert} ->
|
|
{ok, IssuerCert};
|
|
{error, Reason} ->
|
|
case ca_file() of
|
|
undefined -> {error, Reason};
|
|
CAFile -> find_issuer_in_file(Cert, CAFile)
|
|
end
|
|
end.
|
|
|
|
-spec find_issuer_in_dir(cert(), dict:dict())
|
|
-> {{ok, cert()} | {error, {bad_cert, unknown_ca}}, dict:dict()}.
|
|
find_issuer_in_dir(Cert, Cache) ->
|
|
{ok, {_, IssuerID}} = public_key:pkix_issuer_id(Cert, self),
|
|
Hash = short_name_hash(IssuerID),
|
|
Files = case dict:find(Hash, Cache) of
|
|
{ok, L} -> L;
|
|
error -> []
|
|
end,
|
|
lists:foldl(
|
|
fun(_, {ok, _IssuerCert} = Acc) ->
|
|
Acc;
|
|
(Path, Err) ->
|
|
case read_ca_file(Path) of
|
|
{ok, [IssuerCert|_]} ->
|
|
case public_key:pkix_is_issuer(Cert, IssuerCert) of
|
|
true ->
|
|
{ok, IssuerCert};
|
|
false ->
|
|
Err
|
|
end;
|
|
error ->
|
|
Err
|
|
end
|
|
end, {error, {bad_cert, unknown_ca}}, Files).
|
|
|
|
-spec find_issuer_in_file(cert(), file:filename_all() | undefined)
|
|
-> {ok, cert()} | {error, {bad_cert, unknown_ca}}.
|
|
find_issuer_in_file(_Cert, undefined) ->
|
|
{error, {bad_cert, unknown_ca}};
|
|
find_issuer_in_file(Cert, CAFile) ->
|
|
case read_ca_file(CAFile) of
|
|
{ok, IssuerCerts} ->
|
|
lists:foldl(
|
|
fun(_, {ok, _} = Res) ->
|
|
Res;
|
|
(IssuerCert, Err) ->
|
|
case public_key:pkix_is_issuer(Cert, IssuerCert) of
|
|
true -> {ok, IssuerCert};
|
|
false -> Err
|
|
end
|
|
end, {error, {bad_cert, unknown_ca}}, IssuerCerts);
|
|
error ->
|
|
{error, {bad_cert, unknown_ca}}
|
|
end.
|
|
|
|
-spec read_ca_file(file:filename_all()) -> {ok, [cert()]} | error.
|
|
read_ca_file(Path) ->
|
|
case use_cache() of
|
|
true ->
|
|
ets_cache:lookup(?CA_CACHE, Path,
|
|
fun() -> do_read_ca_file(Path) end);
|
|
false ->
|
|
do_read_ca_file(Path)
|
|
end.
|
|
|
|
-spec do_read_ca_file(file:filename_all()) -> {ok, [cert()]} | error.
|
|
do_read_ca_file(Path) ->
|
|
try
|
|
{ok, Data} = file:read_file(Path),
|
|
{ok, IssuerCerts, _} = pem_decode(Data),
|
|
{ok, IssuerCerts}
|
|
catch _:{badmatch, {error, Why}} ->
|
|
?ERROR_MSG("Failed to read CA certificate "
|
|
"from \"~s\": ~s",
|
|
[Path, format_error(Why)]),
|
|
error
|
|
end.
|
|
|
|
-spec match_cert_keys([{path, [cert()]}], [priv_key()])
|
|
-> {ok, [{cert(), priv_key()}]} | {error, {bad_cert, missing_priv_key}}.
|
|
match_cert_keys(CertPaths, PrivKeys) ->
|
|
KeyPairs = [{pubkey_from_privkey(PrivKey), PrivKey} || PrivKey <- PrivKeys],
|
|
match_cert_keys(CertPaths, KeyPairs, []).
|
|
|
|
-spec match_cert_keys([{path, [cert()]}], [{pub_key(), priv_key()}],
|
|
[{cert(), priv_key()}])
|
|
-> {ok, [{[cert()], priv_key()}]} | {error, cert(), {bad_cert, missing_priv_key}}.
|
|
match_cert_keys([{path, Certs}|CertPaths], KeyPairs, Result) ->
|
|
[Cert|_] = RevCerts = lists:reverse(Certs),
|
|
PubKey = pubkey_from_cert(Cert),
|
|
case lists:keyfind(PubKey, 1, KeyPairs) of
|
|
false ->
|
|
{error, Cert, {bad_cert, missing_priv_key}};
|
|
{_, PrivKey} ->
|
|
match_cert_keys(CertPaths, KeyPairs, [{RevCerts, PrivKey}|Result])
|
|
end;
|
|
match_cert_keys([], _, Result) ->
|
|
{ok, Result}.
|
|
|
|
-spec pubkey_from_cert(cert()) -> pub_key().
|
|
pubkey_from_cert(Cert) ->
|
|
TBSCert = Cert#'OTPCertificate'.tbsCertificate,
|
|
PubKeyInfo = TBSCert#'OTPTBSCertificate'.subjectPublicKeyInfo,
|
|
SubjPubKey = PubKeyInfo#'OTPSubjectPublicKeyInfo'.subjectPublicKey,
|
|
case PubKeyInfo#'OTPSubjectPublicKeyInfo'.algorithm of
|
|
#'PublicKeyAlgorithm'{
|
|
algorithm = ?rsaEncryption} ->
|
|
SubjPubKey;
|
|
#'PublicKeyAlgorithm'{
|
|
algorithm = ?'id-dsa',
|
|
parameters = {params, DSSParams}} ->
|
|
{SubjPubKey, DSSParams};
|
|
#'PublicKeyAlgorithm'{
|
|
algorithm = ?'id-ecPublicKey'} ->
|
|
SubjPubKey
|
|
end.
|
|
|
|
-spec pubkey_from_privkey(priv_key()) -> pub_key().
|
|
pubkey_from_privkey(#'RSAPrivateKey'{modulus = Modulus,
|
|
publicExponent = Exp}) ->
|
|
#'RSAPublicKey'{modulus = Modulus,
|
|
publicExponent = Exp};
|
|
pubkey_from_privkey(#'DSAPrivateKey'{p = P, q = Q, g = G, y = Y}) ->
|
|
{Y, #'Dss-Parms'{p = P, q = Q, g = G}};
|
|
pubkey_from_privkey(#'ECPrivateKey'{publicKey = Key}) ->
|
|
#'ECPoint'{point = Key}.
|
|
|
|
-spec get_cert_paths([cert()], digraph:graph()) -> [{path, [cert()]}].
|
|
get_cert_paths(Certs, G) ->
|
|
{NewCerts, OldCerts} =
|
|
lists:partition(
|
|
fun(Cert) ->
|
|
case digraph:vertex(G, Cert) of
|
|
false ->
|
|
digraph:add_vertex(G, Cert),
|
|
true;
|
|
{_, _} ->
|
|
false
|
|
end
|
|
end, Certs),
|
|
add_edges(G, NewCerts, OldCerts),
|
|
add_edges(G, OldCerts, NewCerts),
|
|
add_edges(G, NewCerts, NewCerts),
|
|
lists:flatmap(
|
|
fun(Cert) ->
|
|
case digraph:in_degree(G, Cert) of
|
|
0 ->
|
|
get_cert_path(G, [Cert]);
|
|
_ ->
|
|
[]
|
|
end
|
|
end, Certs).
|
|
|
|
add_edges(G, [Cert1|T], L) ->
|
|
case public_key:pkix_is_self_signed(Cert1) of
|
|
true ->
|
|
ok;
|
|
false ->
|
|
lists:foreach(
|
|
fun(Cert2) when Cert1 /= Cert2 ->
|
|
case public_key:pkix_is_issuer(Cert1, Cert2) of
|
|
true ->
|
|
digraph:add_edge(G, Cert1, Cert2);
|
|
false ->
|
|
ok
|
|
end;
|
|
(_) ->
|
|
ok
|
|
end, L)
|
|
end,
|
|
add_edges(G, T, L);
|
|
add_edges(_, [], _) ->
|
|
ok.
|
|
|
|
get_cert_path(G, [Root|_] = Acc) ->
|
|
case digraph:out_edges(G, Root) of
|
|
[] ->
|
|
[{path, Acc}];
|
|
Es ->
|
|
lists:flatmap(
|
|
fun(E) ->
|
|
{_, _, V, _} = digraph:edge(G, E),
|
|
get_cert_path(G, [V|Acc])
|
|
end, Es)
|
|
end.
|
|
|
|
-spec prep_path(filename:filename()) -> binary().
|
|
prep_path(Path0) ->
|
|
case filename:pathtype(Path0) of
|
|
relative ->
|
|
{ok, CWD} = file:get_cwd(),
|
|
iolist_to_binary(filename:join(CWD, Path0));
|
|
_ ->
|
|
iolist_to_binary(Path0)
|
|
end.
|
|
|
|
-ifdef(SHORT_NAME_HASH).
|
|
short_name_hash(IssuerID) ->
|
|
public_key:short_name_hash(IssuerID).
|
|
-else.
|
|
short_name_hash(_) ->
|
|
"".
|
|
-endif.
|
|
|
|
-spec subscribe(state()) -> ok.
|
|
subscribe(#state{notify = true} = State) ->
|
|
lists:foreach(
|
|
fun(Path) ->
|
|
Dir = filename:dirname(Path),
|
|
Name = list_to_atom(integer_to_list(erlang:phash2(Dir))),
|
|
case fs:start_link(Name, Dir) of
|
|
{ok, _} ->
|
|
?DEBUG("Subscribed to FS events from ~s", [Dir]),
|
|
fs:subscribe(Name);
|
|
{error, _} ->
|
|
ok
|
|
end
|
|
end, State#state.paths);
|
|
subscribe(_) ->
|
|
ok.
|
|
|
|
-spec start_fs() -> boolean().
|
|
start_fs() ->
|
|
application:load(fs),
|
|
application:set_env(fs, backwards_compatible, false),
|
|
case application:ensure_all_started(fs) of
|
|
{ok, _} -> true;
|
|
{error, {already_loaded, _}} -> true;
|
|
{error, Reason} ->
|
|
?ERROR_MSG("Failed to load 'fs' Erlang application: ~p; "
|
|
"certificates change detection will be disabled. "
|
|
"You should now manually run `ejabberdctl "
|
|
"reload_config` whenever certificates are changed "
|
|
"on disc",
|
|
[Reason]),
|
|
false
|
|
end.
|
|
|
|
wildcard(Path) when is_binary(Path) ->
|
|
wildcard(binary_to_list(Path));
|
|
wildcard(Path) ->
|
|
filelib:wildcard(Path).
|
|
|
|
-spec use_cache() -> boolean().
|
|
use_cache() ->
|
|
ejabberd_config:use_cache(global).
|
|
|
|
-spec init_cache() -> ok.
|
|
init_cache() ->
|
|
ets_cache:new(?CA_CACHE, cache_opts()).
|
|
|
|
-spec delete_cache() -> ok.
|
|
delete_cache() ->
|
|
ets_cache:delete(?CA_CACHE).
|
|
|
|
-spec cache_opts() -> [proplists:property()].
|
|
cache_opts() ->
|
|
MaxSize = ejabberd_config:cache_size(global),
|
|
CacheMissed = ejabberd_config:cache_missed(global),
|
|
LifeTime = case ejabberd_config:cache_life_time(global) of
|
|
infinity -> infinity;
|
|
I -> timer:seconds(I)
|
|
end,
|
|
[{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}].
|