Make ACME code working with ejabberd_pkix

This commit is contained in:
Evgeniy Khramtsov 2017-11-17 11:59:40 +03:00
parent c05626a1ba
commit ce98226603
3 changed files with 84 additions and 75 deletions

View File

@ -1,21 +1,23 @@
-module (ejabberd_acme).
-behaviour(gen_server).
-behavior(ejabberd_config).
-export([%% Ejabberdctl Commands
get_certificates/1,
%% ejabberdctl commands
-export([get_certificates/1,
renew_certificates/0,
list_certificates/1,
revoke_certificate/1,
%% Command Options Validity
is_valid_account_opt/1,
revoke_certificate/1]).
%% Command Options Validity
-export([is_valid_account_opt/1,
is_valid_verbose_opt/1,
is_valid_domain_opt/1,
is_valid_revoke_cert/1,
%% Called by ejabberd_pkix
certificate_exists/1,
%% Key Related
generate_key/0,
to_public/1
]).
is_valid_revoke_cert/1]).
%% Key Related
-export([generate_key/0, to_public/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([start_link/0, opt_type/1]).
-include("ejabberd.hrl").
-include("logger.hrl").
@ -24,17 +26,45 @@
-include("ejabberd_acme.hrl").
-include_lib("public_key/include/public_key.hrl").
-export([opt_type/1]).
-behavior(ejabberd_config).
%%
%% Default ACME configuration
%%
-define(DEFAULT_CONFIG_CONTACT, <<"mailto:example-admin@example.com">>).
-define(DEFAULT_CONFIG_CA_URL, "https://acme-v01.api.letsencrypt.org").
-record(state, {}).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
init([]) ->
case filelib:ensure_dir(filename:join(acme_certs_dir(), "foo")) of
ok ->
register_certfiles(),
{ok, #state{}};
{error, Why} ->
?CRITICAL_MSG("Failed to create directory ~s: ~s",
[acme_certs_dir(), file:format_error(Why)]),
{stop, Why}
end.
handle_call(_Request, _From, State) ->
{stop, {unexpected_call, _Request, _From}, State}.
handle_cast(_Msg, State) ->
?WARNING_MSG("unexpected cast: ~p", [_Msg]),
{noreply, State}.
handle_info(_Info, State) ->
?WARNING_MSG("unexpected info: ~p", [_Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
%% Command Functions
@ -591,26 +621,6 @@ domain_certificate_exists(Domain) ->
Certs = read_certificates_persistent(),
lists:keyfind(Domain, 1, Certs).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
%% Called by ejabberd_pkix to check
%% if a certificate exists for a
%% specific host
%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec certificate_exists(bitstring()) -> {true, file:filename()} | false.
certificate_exists(Host) ->
Certificates = read_certificates_persistent(),
case lists:keyfind(Host, 1 , Certificates) of
false ->
false;
{Host, #data_cert{path=Path}} ->
{true, Path}
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
%% Certificate Request Functions
@ -951,8 +961,8 @@ data_remove_certificate(Data, _DataCert = #data_cert{domain=Domain}) ->
-spec persistent_file() -> file:filename().
persistent_file() ->
MnesiaDir = mnesia:system_info(directory),
filename:join(MnesiaDir, "acme.DAT").
AcmeDir = acme_certs_dir(),
filename:join(AcmeDir, "acme.DAT").
%% The persistent file should be read and written only by its owner
-spec persistent_file_mode() -> 384.
@ -1032,9 +1042,9 @@ save_certificate({error, _, _} = Error) ->
Error;
save_certificate({ok, DomainName, Cert}) ->
try
CertDir = get_config_cert_dir(),
CertDir = acme_certs_dir(),
DomainString = bitstring_to_list(DomainName),
CertificateFile = filename:join([CertDir, DomainString ++ "_cert.pem"]),
CertificateFile = filename:join([CertDir, DomainString ++ ".pem"]),
%% TODO: At some point do the following using a Transaction so
%% that there is no certificate saved if it cannot be added in
%% certificate persistent storage
@ -1045,7 +1055,7 @@ save_certificate({ok, DomainName, Cert}) ->
path = CertificateFile
},
add_certificate_persistent(DataCert),
ejabberd_pkix:add_certfile(CertificateFile),
ok = ejabberd_pkix:add_certfile(CertificateFile),
{ok, DomainName, saved}
catch
throw:Throw ->
@ -1064,6 +1074,15 @@ save_renewed_certificate({ok, _, no_expire} = Cert) ->
save_renewed_certificate({ok, DomainName, Cert}) ->
save_certificate({ok, DomainName, Cert}).
-spec register_certfiles() -> ok.
register_certfiles() ->
Dir = acme_certs_dir(),
Paths = filelib:wildcard(filename:join(Dir, "*.pem")),
lists:foreach(
fun(Path) ->
ejabberd_pkix:add_certfile(Path)
end, Paths).
-spec write_cert(file:filename(), binary(), bitstring()) -> {ok, bitstring(), saved}.
write_cert(CertificateFile, Cert, DomainName) ->
case file:write_file(CertificateFile, Cert) of
@ -1121,17 +1140,9 @@ get_config_hosts() ->
Hosts
end.
-spec get_config_cert_dir() -> file:filename().
get_config_cert_dir() ->
case ejabberd_config:get_option(cert_dir, undefined) of
undefined ->
?WARNING_MSG("No cert_dir configuration has been specified in configuration", []),
mnesia:system_info(directory);
%% throw({error, configuration});
CertDir ->
CertDir
end.
-spec acme_certs_dir() -> file:filename().
acme_certs_dir() ->
filename:join(ejabberd_pkix:certs_dir(), "acme").
generate_key() ->
jose_jwk:generate_key({ec, secp256r1}).
@ -1151,16 +1162,9 @@ parse_acme_opt({ca_url, CaUrl}) when is_bitstring(CaUrl) ->
parse_acme_opt({contact, Contact}) when is_bitstring(Contact) ->
{contact, Contact}.
parse_cert_dir_opt(Opt) when is_bitstring(Opt) ->
true = filelib:is_dir(Opt),
Opt.
-spec opt_type(acme) -> fun((acme_config()) -> (acme_config()));
(cert_dir) -> fun((bitstring()) -> (bitstring()));
(atom()) -> [atom()].
opt_type(acme) ->
fun parse_acme_opts/1;
opt_type(cert_dir) ->
fun parse_cert_dir_opt/1;
opt_type(_) ->
[acme, cert_dir].
[acme].

View File

@ -28,7 +28,7 @@
%% 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]).
config_reloaded/0, certs_dir/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
@ -190,8 +190,17 @@ init([]) ->
end.
handle_call({add_certfile, Path}, _, State) ->
{Result, NewState} = add_certfile(Path, State),
{reply, Result, NewState};
case add_certfile(Path, State) of
{ok, State1} ->
case build_chain_and_check(State1) of
{ok, State2} ->
{reply, ok, State2};
Err ->
{reply, Err, State}
end;
{Err, State1} ->
{reply, Err, State1}
end;
handle_call({route_registered, Host}, _, State) ->
case add_certfiles(Host, State) of
{ok, NewState} ->
@ -301,14 +310,7 @@ add_certfiles(Host, State) ->
NewAccState
end
end, State, certfiles_from_config_options()),
State2 = case ejabberd_acme:certificate_exists(Host) of
{true, Path} ->
{_, State3} = add_certfile(Path, State1),
State3;
false ->
State1
end,
if State /= State2 ->
if State /= State1 ->
case build_chain_and_check(State1) of
ok -> {ok, State1};
{error, _} = Err -> Err

View File

@ -156,6 +156,8 @@ init([]) ->
permanent, 5000, worker, [cyrsasl]},
PKIX = {ejabberd_pkix, {ejabberd_pkix, start_link, []},
permanent, 5000, worker, [ejabberd_pkix]},
ACME = {ejabberd_acme, {ejabberd_acme, start_link, []},
permanent, 5000, worker, [ejabberd_acme]},
IQ = {ejabberd_iq, {ejabberd_iq, start_link, []},
permanent, 5000, worker, [ejabberd_iq]},
{ok, {{one_for_one, 10, 1},
@ -168,6 +170,7 @@ init([]) ->
Commands,
Admin,
PKIX,
ACME,
Listener,
SystemMonitor,
S2S,