aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ejabberd.yml.example4
-rw-r--r--src/ejabberd_acme.erl154
-rw-r--r--src/ejabberd_acme_comm.erl8
-rw-r--r--src/ejabberd_admin.erl18
-rw-r--r--src/ejabberd_pkix.erl27
5 files changed, 158 insertions, 53 deletions
diff --git a/ejabberd.yml.example b/ejabberd.yml.example
index 85754e1bb..ee3dda24c 100644
--- a/ejabberd.yml.example
+++ b/ejabberd.yml.example
@@ -664,11 +664,11 @@ language: "en"
###' ACME
##
-## Must contain a contact and a directory that the Http Challenges can be solved at
+## Must contain a contact and the ACME CA url
##
acme:
contact: "mailto:cert-admin-ejabberd@example.com"
- http_dir: "/home/konstantinos/Desktop/Programming/test-server-for-acme/"
+ ca_url: "http://localhost:4000"
cert_dir: "/usr/local/var/lib/ejabberd/"
diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl
index 2370dc906..260d994b8 100644
--- a/src/ejabberd_acme.erl
+++ b/src/ejabberd_acme.erl
@@ -2,13 +2,16 @@
-export([%% Ejabberdctl Commands
get_certificates/2,
- renew_certificates/1,
+ renew_certificates/0,
list_certificates/1,
- revoke_certificate/2,
+ revoke_certificate/1,
%% Command Options Validity
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
@@ -21,6 +24,9 @@
-include("ejabberd_acme.hrl").
-include_lib("public_key/include/public_key.hrl").
+-export([opt_type/1]).
+
+-behavior(ejabberd_config).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@@ -53,6 +59,11 @@ is_valid_domain_opt(DomainString) ->
SeparatedDomains ->
true
end.
+
+-spec is_valid_revoke_cert(string()) -> boolean().
+is_valid_revoke_cert(DomainOrFile) ->
+ lists:prefix("file:", DomainOrFile) orelse
+ lists:prefix("domain:", DomainOrFile).
@@ -60,9 +71,10 @@ is_valid_domain_opt(DomainString) ->
%% Get Certificate
%%
--spec get_certificates(url(), domains_opt()) -> string() | {'error', _}.
-get_certificates(CAUrl, Domains) ->
+-spec get_certificates(domains_opt()) -> string() | {'error', _}.
+get_certificates(Domains) ->
try
+ CAUrl = get_config_ca_url(),
get_certificates0(CAUrl, Domains)
catch
throw:Throw ->
@@ -270,9 +282,10 @@ ensure_account_exists() ->
%%
%% Renew Certificates
%%
--spec renew_certificates(url()) -> string() | {'error', _}.
-renew_certificates(CAUrl) ->
+-spec renew_certificates() -> string() | {'error', _}.
+renew_certificates() ->
try
+ CAUrl = get_config_ca_url(),
renew_certificates0(CAUrl)
catch
throw:Throw ->
@@ -299,7 +312,7 @@ renew_certificates0(CAUrl) ->
%% Format the result to send back to ejabberdctl
format_get_certificates_result(SavedCerts).
--spec renew_certificate(url(), data_cert(), jose_jwk:key()) ->
+-spec renew_certificate(url(), {bitstring(), data_cert()}, jose_jwk:key()) ->
{'ok', bitstring(), _} |
{'error', bitstring(), _}.
renew_certificate(CAUrl, {DomainName, _} = Cert, PrivateKey) ->
@@ -311,7 +324,7 @@ renew_certificate(CAUrl, {DomainName, _} = Cert, PrivateKey) ->
end.
--spec cert_to_expire(data_cert()) -> boolean().
+-spec cert_to_expire({bitstring(), data_cert()}) -> boolean().
cert_to_expire({DomainName, #data_cert{pem = Pem}}) ->
Certificate = pem_to_certificate(Pem),
Validity = get_utc_validity(Certificate),
@@ -380,7 +393,7 @@ format_certificate(DataCert, Verbose) ->
fail_format_certificate(DomainName)
end.
--spec format_certificate_plain(bitstring(), string(), string()) -> string().
+-spec format_certificate_plain(bitstring(), {expired | ok, string()}, string()) -> string().
format_certificate_plain(DomainName, NotAfter, Path) ->
Result = lists:flatten(io_lib:format(
" Domain: ~s~n"
@@ -389,7 +402,7 @@ format_certificate_plain(DomainName, NotAfter, Path) ->
[DomainName, format_validity(NotAfter), Path])),
Result.
--spec format_certificate_verbose(bitstring(), string(), bitstring()) -> string().
+-spec format_certificate_verbose(bitstring(), {expired | ok, string()}, bitstring()) -> string().
format_certificate_verbose(DomainName, NotAfter, PemCert) ->
Result = lists:flatten(io_lib:format(
" Domain: ~s~n"
@@ -458,11 +471,11 @@ get_utc_validity(#'Certificate'{tbsCertificate = TbsCertificate}) ->
%% Revoke Certificate
%%
-%% Add a try-catch to this stub
--spec revoke_certificate(url(), string()) -> {ok, deleted} | {error, _}.
-revoke_certificate(CAUrl, Domain) ->
+-spec revoke_certificate(string()) -> {ok, deleted} | {error, _}.
+revoke_certificate(DomainOrFile) ->
try
- revoke_certificate0(CAUrl, Domain)
+ CAUrl = get_config_ca_url(),
+ revoke_certificate0(CAUrl, DomainOrFile)
catch
throw:Throw ->
Throw;
@@ -471,29 +484,51 @@ revoke_certificate(CAUrl, Domain) ->
{error, revoke_certificate}
end.
--spec revoke_certificate0(url(), string()) -> {ok, deleted} | {error, not_found}.
-revoke_certificate0(CAUrl, Domain) ->
- BinDomain = list_to_bitstring(Domain),
- case domain_certificate_exists(BinDomain) of
- {BinDomain, Certificate} ->
- ok = revoke_certificate1(CAUrl, Certificate),
+-spec revoke_certificate0(url(), string()) -> {ok, deleted}.
+revoke_certificate0(CAUrl, DomainOrFile) ->
+ ParsedCert = parse_revoke_cert_argument(DomainOrFile),
+ revoke_certificate1(CAUrl, ParsedCert).
+
+-spec revoke_certificate1(url(), {domain, bitstring()} | {file, file:filename()}) ->
+ {ok, deleted}.
+revoke_certificate1(CAUrl, {domain, Domain}) ->
+ case domain_certificate_exists(Domain) of
+ {Domain, Cert = #data_cert{pem=PemCert}} ->
+ ok = revoke_certificate2(CAUrl, PemCert),
+ ok = remove_certificate_persistent(Cert),
{ok, deleted};
false ->
- {error, not_found}
+ ?ERROR_MSG("Certificate for domain: ~p not found", [Domain]),
+ throw({error, not_found})
+ end;
+revoke_certificate1(CAUrl, {file, File}) ->
+ case file:read_file(File) of
+ {ok, Pem} ->
+ ok = revoke_certificate2(CAUrl, Pem),
+ {ok, deleted};
+ {error, Reason} ->
+ ?ERROR_MSG("Error: ~p reading pem certificate-key file: ~p", [Reason, File]),
+ throw({error, Reason})
end.
+
--spec revoke_certificate1(url(), data_cert()) -> ok.
-revoke_certificate1(CAUrl, Cert = #data_cert{pem=PemEncodedCert}) ->
+-spec revoke_certificate2(url(), pem()) -> ok.
+revoke_certificate2(CAUrl, PemEncodedCert) ->
{Certificate, CertPrivateKey} = prepare_certificate_revoke(PemEncodedCert),
{ok, Dirs, Nonce} = ejabberd_acme_comm:directory(CAUrl),
Req = [{<<"certificate">>, Certificate}],
{ok, [], Nonce1} = ejabberd_acme_comm:revoke_cert(Dirs, CertPrivateKey, Req, Nonce),
- ok = remove_certificate_persistent(Cert),
ok.
--spec prepare_certificate_revoke(pem()) -> bitstring().
+-spec parse_revoke_cert_argument(string()) -> {domain, bitstring()} | {file, file:filename()}.
+parse_revoke_cert_argument([$f, $i, $l, $e, $:|File]) ->
+ {file, File};
+parse_revoke_cert_argument([$d, $o, $m, $a, $i, $n, $: | Domain]) ->
+ {domain, list_to_bitstring(Domain)}.
+
+-spec prepare_certificate_revoke(pem()) -> {bitstring(), jose_jwk:key()}.
prepare_certificate_revoke(PemEncodedCert) ->
PemList = public_key:pem_decode(PemEncodedCert),
PemCertEnc = lists:keyfind('Certificate', 1, PemList),
@@ -501,7 +536,7 @@ prepare_certificate_revoke(PemEncodedCert) ->
DerCert = public_key:der_encode('Certificate', PemCert),
Base64Cert = base64url:encode(DerCert),
- Key = find_private_key_in_pem(PemEncodedCert),
+ {ok, Key} = find_private_key_in_pem(PemEncodedCert),
{Base64Cert, Key}.
-spec domain_certificate_exists(bitstring()) -> {bitstring(), data_cert()} | false.
@@ -512,16 +547,31 @@ domain_certificate_exists(Domain) ->
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
+%% 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
%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% For now we accept only generating a key of
%% specific type for signing the csr
-%% TODO: Make this function handle more signing keys
-%% 1. Derive oid from Key
-%% 2. Derive the whole algo objects from Key
-%% TODO: Encode Strings using length using a library function
-spec make_csr(proplist()) -> {binary(), jose_jwk:key()}.
make_csr(Attributes) ->
@@ -672,7 +722,6 @@ get_challenges(Body) ->
-spec not_before_not_after() -> {binary(), binary()}.
not_before_not_after() ->
- %% TODO: Make notBefore and notAfter configurable somewhere
{MegS, Sec, MicS} = erlang:timestamp(),
NotBefore = xmpp_util:encode_timestamp({MegS, Sec, MicS}),
%% The certificate will be valid for 90 Days after today
@@ -729,7 +778,7 @@ find_private_key_in_pem(Pem) ->
PemKey ->
Key = public_key:pem_entry_decode(PemKey),
JoseKey = jose_jwk:from_key(Key),
- JoseKey
+ {ok, JoseKey}
end.
@@ -972,6 +1021,18 @@ get_config_contact() ->
throw({error, configuration_contact})
end.
+-spec get_config_ca_url() -> string().
+get_config_ca_url() ->
+ Acme = get_config_acme(),
+ case lists:keyfind(ca_url, 1, Acme) of
+ {ca_url, CAUrl} ->
+ CAUrl;
+ false ->
+ ?ERROR_MSG("No CA url has been specified", []),
+ throw({error, configuration_ca_url})
+ end.
+
+
-spec get_config_hosts() -> [bitstring()].
get_config_hosts() ->
case ejabberd_config:get_option(hosts, undefined) of
@@ -1006,3 +1067,32 @@ generate_key() ->
jose_jwk:generate_key({ec, secp256r1}).
-endif.
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% Option Parsing Code
+%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+parse_acme_opts(AcmeOpt) ->
+ [parse_acme_opt(Opt) || Opt <- AcmeOpt].
+
+
+parse_acme_opt({ca_url, CaUrl}) when is_bitstring(CaUrl) ->
+ {ca_url, binary_to_list(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(([{ca_url, string()} | {contact, bitstring()}]) ->
+ ([{ca_url, string()} | {contact, bitstring()}]));
+ (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].
diff --git a/src/ejabberd_acme_comm.erl b/src/ejabberd_acme_comm.erl
index b66d5f610..99eaff87b 100644
--- a/src/ejabberd_acme_comm.erl
+++ b/src/ejabberd_acme_comm.erl
@@ -291,7 +291,7 @@ prepare_get_request(Url, HandleRespFun, ResponseType) ->
-spec sign_json_jose(jose_jwk:key(), bitstring(), nonce()) -> {_, jws()}.
sign_json_jose(Key, Json, Nonce) ->
- PubKey = jose_jwk:to_public(Key),
+ PubKey = ejabberd_acme:to_public(Key),
{_, BinaryPubKey} = jose_jwk:to_binary(PubKey),
PubKeyJson = jiffy:decode(BinaryPubKey),
%% TODO: Ensure this works for all cases
@@ -383,11 +383,11 @@ decode(Json) ->
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec failed_http_request({ok, _} | {error, _}, url()) -> {error, _}.
-failed_http_request({ok, {{_, Code, _}, _Head, Body}}, Url) ->
+failed_http_request({ok, {{_, Code, Reason}, _Head, Body}}, Url) ->
?ERROR_MSG("Got unexpected status code from <~s>: ~B, Body: ~s",
[Url, Code, Body]),
- {error, unexpected_code};
+ throw({error, {unexpected_code, Code, Reason}});
failed_http_request({error, Reason}, Url) ->
?ERROR_MSG("Error making a request to <~s>: ~p",
[Url, Reason]),
- {error, Reason}.
+ throw({error, Reason}).
diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl
index e41a53994..8d022e606 100644
--- a/src/ejabberd_admin.erl
+++ b/src/ejabberd_admin.erl
@@ -268,8 +268,8 @@ get_commands_spec() ->
#ejabberd_commands{name = revoke_certificate, tags = [acme],
desc = "Revokes the selected certificate",
module = ?MODULE, function = revoke_certificate,
- args_desc = ["The domain of the certificate in question"],
- args = [{domain, string}],
+ args_desc = ["The domain or file (in pem format) of the certificate in question {domain:Domain | file:File}"],
+ args = [{domain_or_file, string}],
result = {res, restuple}},
#ejabberd_commands{name = import_piefxis, tags = [mnesia],
@@ -578,13 +578,13 @@ import_dir(Path) ->
get_certificate(Domains) ->
case ejabberd_acme:is_valid_domain_opt(Domains) of
true ->
- ejabberd_acme:get_certificates("http://localhost:4000", Domains);
+ ejabberd_acme:get_certificates(Domains);
false ->
String = io_lib:format("Invalid domains: ~p", [Domains])
end.
renew_certificate() ->
- ejabberd_acme:renew_certificates("http://localhost:4000").
+ ejabberd_acme:renew_certificates().
list_certificates(Verbose) ->
case ejabberd_acme:is_valid_verbose_opt(Verbose) of
@@ -595,8 +595,14 @@ list_certificates(Verbose) ->
{invalid_option, String}
end.
-revoke_certificate(Domain) ->
- ejabberd_acme:revoke_certificate("http://localhost:4000", Domain).
+revoke_certificate(DomainOrFile) ->
+ case ejabberd_acme:is_valid_revoke_cert(DomainOrFile) of
+ true ->
+ ejabberd_acme:revoke_certificate(DomainOrFile);
+ false ->
+ String = io_lib:format("Bad argument: ~s", [DomainOrFile]),
+ {invalid_argument, String}
+ end.
%%%
%%% Purge DB
diff --git a/src/ejabberd_pkix.erl b/src/ejabberd_pkix.erl
index f9f0472f6..89b33b8aa 100644
--- a/src/ejabberd_pkix.erl
+++ b/src/ejabberd_pkix.erl
@@ -204,15 +204,24 @@ add_certfiles(State) ->
end, State, ejabberd_config:get_myhosts()).
add_certfiles(Host, State) ->
- 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, [c2s_certfile, s2s_certfile, domain_certfile]).
+ NewState =
+ 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, [c2s_certfile, s2s_certfile, domain_certfile]),
+ %% Add acme certificate if it exists
+ case ejabberd_acme:certificate_exists(Host) of
+ {true, Path} ->
+ {_, FinalState} = add_certfile(Path, NewState),
+ FinalState;
+ false ->
+ NewState
+ end.
add_certfile(Path, State) ->
case maps:get(Path, State#state.certs, undefined) of