Explain what is needed for the acme configuration and other small changes

1. Add a request handler in ejabberd_http and explain how to configure the http listener so that the challenges can be solved.
2. Make acme configuration optional by providing defaults in ejabberd_acme.
3. Save the CA that the account has been created in so that it creates a new account when connecting to a new CA.
4. Small spec change in acme configuration.
This commit is contained in:
Konstantinos Kallas 2017-11-14 14:12:33 +02:00
parent 78f494dd2e
commit ce99db0595
5 changed files with 72 additions and 38 deletions

View File

@ -161,7 +161,6 @@ listen:
"/ws": ejabberd_http_ws "/ws": ejabberd_http_ws
"/bosh": mod_bosh "/bosh": mod_bosh
"/api": mod_http_api "/api": mod_http_api
"/.well-known": acme_challenge
## "/pub/archive": mod_http_fileserver ## "/pub/archive": mod_http_fileserver
web_admin: true web_admin: true
## register: true ## register: true
@ -662,6 +661,17 @@ language: "en"
###. ==== ###. ====
###' ACME ###' ACME
##
## In order to use the acme certificate acquiring through "Let's Encrypt"
## an http listener has to be configured to listen to port 80 so that
## the authorization challenges posed by "Let's Encrypt" can be solved.
##
## A simple way of doing this would be to add the following in the listen
## configuration field:
## -
## port: 80
## ip: "::"
## module: ejabberd_http
acme: acme:

View File

@ -7,8 +7,9 @@
}). }).
-record(data_acc, { -record(data_acc, {
id :: list(), id :: list(),
key :: jose_jwk:key() ca_url :: url(),
key :: jose_jwk:key()
}). }).
-type data_acc() :: #data_acc{}. -type data_acc() :: #data_acc{}.
@ -23,6 +24,9 @@
%% Types %% Types
%% %%
%% Acme configuration
-type acme_config() :: [{ca_url, url()} | {contact, bitstring()}].
%% The main data type that ejabberd_acme keeps %% The main data type that ejabberd_acme keeps
-type acme_data() :: proplist(). -type acme_data() :: proplist().

View File

@ -2,8 +2,8 @@
-export ([key_authorization/2, -export ([key_authorization/2,
solve_challenge/3, solve_challenge/3,
process/2,
process/2 acme_handler/0
]). ]).
%% Challenge Types %% Challenge Types
%% ================ %% ================
@ -18,6 +18,10 @@
-include("ejabberd_http.hrl"). -include("ejabberd_http.hrl").
-include("ejabberd_acme.hrl"). -include("ejabberd_acme.hrl").
%% This is the default endpoint for the http challenge
%% This function is called by the http_listener
acme_handler() ->
{[<<".well-known">>],acme_challenge}.
%% TODO: Maybe validate request here?? %% TODO: Maybe validate request here??
process(LocalPath, _Request) -> process(LocalPath, _Request) ->

View File

@ -28,6 +28,12 @@
-behavior(ejabberd_config). -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").
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% %%
@ -91,13 +97,16 @@ get_certificates0(CAUrl, Domains) ->
get_certificates1(CAUrl, Domains, PrivateKey). get_certificates1(CAUrl, Domains, PrivateKey).
-spec retrieve_or_create_account(url()) -> {'ok', string(), jose_jwk:key()}.
retrieve_or_create_account(CAUrl) -> retrieve_or_create_account(CAUrl) ->
case read_account_persistent() of case read_account_persistent() of
none -> none ->
create_save_new_account(CAUrl); create_save_new_account(CAUrl);
{ok, AccId, PrivateKey} ->
{ok, AccId, PrivateKey} {ok, AccId, CAUrl, PrivateKey} ->
{ok, AccId, PrivateKey};
{ok, _AccId, _, _PrivateKey} ->
create_save_new_account(CAUrl)
end. end.
@ -182,7 +191,7 @@ create_save_new_account(CAUrl) ->
{ok, Id} = create_new_account(CAUrl, Contact, PrivateKey), {ok, Id} = create_new_account(CAUrl, Contact, PrivateKey),
%% Write Persistent Data %% Write Persistent Data
ok = write_account_persistent({Id, PrivateKey}), ok = write_account_persistent({Id, CAUrl, PrivateKey}),
{ok, Id, PrivateKey}. {ok, Id, PrivateKey}.
@ -272,14 +281,17 @@ create_new_certificate(CAUrl, {DomainName, AllSubDomains}, PrivateKey) ->
throw({error, DomainName, certificate}) throw({error, DomainName, certificate})
end. end.
-spec ensure_account_exists() -> {ok, string(), jose_jwk:key()}. -spec ensure_account_exists(url()) -> {ok, string(), jose_jwk:key()}.
ensure_account_exists() -> ensure_account_exists(CAUrl) ->
case read_account_persistent() of case read_account_persistent() of
none -> none ->
?ERROR_MSG("No existing account", []), ?ERROR_MSG("No existing account", []),
throw({error, no_old_account}); throw({error, no_old_account});
{ok, AccId, PrivateKey} -> {ok, AccId, CAUrl, PrivateKey} ->
{ok, AccId, PrivateKey} {ok, AccId, PrivateKey};
{ok, _AccId, OtherCAUrl, _PrivateKey} ->
?ERROR_MSG("Account is connected to another CA: ~s", [OtherCAUrl]),
throw({error, account_in_other_CA})
end. end.
@ -302,7 +314,7 @@ renew_certificates() ->
-spec renew_certificates0(url()) -> string(). -spec renew_certificates0(url()) -> string().
renew_certificates0(CAUrl) -> renew_certificates0(CAUrl) ->
%% Get the current account %% Get the current account
{ok, _AccId, PrivateKey} = ensure_account_exists(), {ok, _AccId, PrivateKey} = ensure_account_exists(CAUrl),
%% Find all hosts that we have certificates for %% Find all hosts that we have certificates for
Certs = read_certificates_persistent(), Certs = read_certificates_persistent(),
@ -883,18 +895,18 @@ data_empty() ->
%% Account %% Account
%% %%
-spec data_get_account(acme_data()) -> {ok, list(), jose_jwk:key()} | none. -spec data_get_account(acme_data()) -> {ok, list(), url(), jose_jwk:key()} | none.
data_get_account(Data) -> data_get_account(Data) ->
case lists:keyfind(account, 1, Data) of case lists:keyfind(account, 1, Data) of
{account, #data_acc{id = AccId, key = PrivateKey}} -> {account, #data_acc{id = AccId, ca_url = CAUrl, key = PrivateKey}} ->
{ok, AccId, PrivateKey}; {ok, AccId, CAUrl, PrivateKey};
false -> false ->
none none
end. end.
-spec data_set_account(acme_data(), {list(), jose_jwk:key()}) -> acme_data(). -spec data_set_account(acme_data(), {list(), url(), jose_jwk:key()}) -> acme_data().
data_set_account(Data, {AccId, PrivateKey}) -> data_set_account(Data, {AccId, CAUrl, PrivateKey}) ->
NewAcc = {account, #data_acc{id = AccId, key = PrivateKey}}, NewAcc = {account, #data_acc{id = AccId, ca_url = CAUrl, key = PrivateKey}},
lists:keystore(account, 1, Data, NewAcc). lists:keystore(account, 1, Data, NewAcc).
%% %%
@ -983,13 +995,13 @@ create_persistent() ->
throw({error, Reason}) throw({error, Reason})
end. end.
-spec write_account_persistent({list(), jose_jwk:key()}) -> ok | no_return(). -spec write_account_persistent({list(), url(), jose_jwk:key()}) -> ok | no_return().
write_account_persistent({AccId, PrivateKey}) -> write_account_persistent({AccId, CAUrl, PrivateKey}) ->
{ok, Data} = read_persistent(), {ok, Data} = read_persistent(),
NewData = data_set_account(Data, {AccId, PrivateKey}), NewData = data_set_account(Data, {AccId, CAUrl, PrivateKey}),
ok = write_persistent(NewData). ok = write_persistent(NewData).
-spec read_account_persistent() -> {ok, list(), jose_jwk:key()} | none. -spec read_account_persistent() -> {ok, list(), url(), jose_jwk:key()} | none.
read_account_persistent() -> read_account_persistent() ->
{ok, Data} = read_persistent(), {ok, Data} = read_persistent(),
data_get_account(Data). data_get_account(Data).
@ -1060,12 +1072,13 @@ write_cert(CertificateFile, Cert, DomainName) ->
throw({error, DomainName, saving}) throw({error, DomainName, saving})
end. end.
-spec get_config_acme() -> [{atom(), bitstring()}]. -spec get_config_acme() -> acme_config().
get_config_acme() -> get_config_acme() ->
case ejabberd_config:get_option(acme, undefined) of case ejabberd_config:get_option(acme, undefined) of
undefined -> undefined ->
?ERROR_MSG("No acme configuration has been specified", []), ?WARNING_MSG("No acme configuration has been specified", []),
throw({error, configuration}); %% throw({error, configuration});
[];
Acme -> Acme ->
Acme Acme
end. end.
@ -1077,19 +1090,21 @@ get_config_contact() ->
{contact, Contact} -> {contact, Contact} ->
Contact; Contact;
false -> false ->
?ERROR_MSG("No contact has been specified", []), ?WARNING_MSG("No contact has been specified in configuration", []),
throw({error, configuration_contact}) ?DEFAULT_CONFIG_CONTACT
%% throw({error, configuration_contact})
end. end.
-spec get_config_ca_url() -> string(). -spec get_config_ca_url() -> url().
get_config_ca_url() -> get_config_ca_url() ->
Acme = get_config_acme(), Acme = get_config_acme(),
case lists:keyfind(ca_url, 1, Acme) of case lists:keyfind(ca_url, 1, Acme) of
{ca_url, CAUrl} -> {ca_url, CAUrl} ->
CAUrl; CAUrl;
false -> false ->
?ERROR_MSG("No CA url has been specified", []), ?ERROR_MSG("No CA url has been specified in configuration", []),
throw({error, configuration_ca_url}) ?DEFAULT_CONFIG_CA_URL
%% throw({error, configuration_ca_url})
end. end.
@ -1097,7 +1112,7 @@ get_config_ca_url() ->
get_config_hosts() -> get_config_hosts() ->
case ejabberd_config:get_option(hosts, undefined) of case ejabberd_config:get_option(hosts, undefined) of
undefined -> undefined ->
?ERROR_MSG("No hosts have been specified", []), ?ERROR_MSG("No hosts have been specified in configuration", []),
throw({error, configuration_hosts}); throw({error, configuration_hosts});
Hosts -> Hosts ->
Hosts Hosts
@ -1107,8 +1122,9 @@ get_config_hosts() ->
get_config_cert_dir() -> get_config_cert_dir() ->
case ejabberd_config:get_option(cert_dir, undefined) of case ejabberd_config:get_option(cert_dir, undefined) of
undefined -> undefined ->
?ERROR_MSG("No cert_dir configuration has been specified", []), ?WARNING_MSG("No cert_dir configuration has been specified in configuration", []),
throw({error, configuration}); mnesia:system_info(directory);
%% throw({error, configuration});
CertDir -> CertDir ->
CertDir CertDir
end. end.
@ -1136,8 +1152,7 @@ parse_cert_dir_opt(Opt) when is_bitstring(Opt) ->
true = filelib:is_dir(Opt), true = filelib:is_dir(Opt),
Opt. Opt.
-spec opt_type(acme) -> fun(([{ca_url, string()} | {contact, bitstring()}]) -> -spec opt_type(acme) -> fun((acme_config()) -> (acme_config()));
([{ca_url, string()} | {contact, bitstring()}]));
(cert_dir) -> fun((bitstring()) -> (bitstring())); (cert_dir) -> fun((bitstring()) -> (bitstring()));
(atom()) -> [atom()]. (atom()) -> [atom()].
opt_type(acme) -> opt_type(acme) ->

View File

@ -136,8 +136,9 @@ init({SockMod, Socket}, Opts) ->
true -> [{[], ejabberd_xmlrpc}]; true -> [{[], ejabberd_xmlrpc}];
false -> [] false -> []
end, end,
Acme = [acme_challenge:acme_handler()],
DefinedHandlers = proplists:get_value(request_handlers, Opts, []), DefinedHandlers = proplists:get_value(request_handlers, Opts, []),
RequestHandlers = DefinedHandlers ++ Captcha ++ Register ++ RequestHandlers = Acme ++ DefinedHandlers ++ Captcha ++ Register ++
Admin ++ Bind ++ XMLRPC, Admin ++ Bind ++ XMLRPC,
?DEBUG("S: ~p~n", [RequestHandlers]), ?DEBUG("S: ~p~n", [RequestHandlers]),