Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/processone/ejabberd.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--include/ejabberd_acme.hrl21
-rw-r--r--src/ejabberd_acme.erl341
2 files changed, 219 insertions, 143 deletions
diff --git a/include/ejabberd_acme.hrl b/include/ejabberd_acme.hrl
index ef6afe601..4ef3bedbe 100644
--- a/include/ejabberd_acme.hrl
+++ b/include/ejabberd_acme.hrl
@@ -10,25 +10,40 @@
id :: list(),
key :: jose_jwk:key()
}).
+-type data_acc() :: #data_acc{}.
-record(data_cert, {
domain :: bitstring(),
- pem :: bitstring(),
- path :: bitstring()
+ pem :: pem(),
+ path :: string()
}).
+-type data_cert() :: #data_cert{}.
+%%
+%% Types
+%%
+%% The main data type that ejabberd_acme keeps
+-type acme_data() :: proplist().
+%% The list of certificates kept in data
+-type data_certs() :: proplist(bitstring(), data_cert()).
+
+%% The certificate saved in pem format
+-type pem() :: bitstring().
-type nonce() :: string().
-type url() :: string().
-type proplist() :: [{_, _}].
+-type proplist(X,Y) :: [{X,Y}].
-type dirs() :: #{string() => url()}.
-type jws() :: map().
-type handle_resp_fun() :: fun(({ok, proplist(), proplist()}) -> {ok, _, nonce()}).
-type acme_challenge() :: #challenge{}.
+%% Options
-type account_opt() :: string().
+-type verbose_opt() :: string().
+
--type pem_certificate() :: bitstring().
diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl
index db959e5ea..5cb5857d4 100644
--- a/src/ejabberd_acme.erl
+++ b/src/ejabberd_acme.erl
@@ -48,129 +48,7 @@ is_valid_verbose_opt("plain") -> true;
is_valid_verbose_opt("verbose") -> true;
is_valid_verbose_opt(_) -> false.
-%%
-%% Revoke Certificate
-%%
-
-%% Add a try-catch to this stub
-revoke_certificate(CAUrl, Domain) ->
- revoke_certificate0(CAUrl, Domain).
-
-revoke_certificate0(CAUrl, Domain) ->
- BinDomain = list_to_bitstring(Domain),
- case domain_certificate_exists(BinDomain) of
- {BinDomain, Certificate} ->
- ?INFO_MSG("Certificate: ~p found!!", [Certificate]),
- ok = revoke_certificate1(CAUrl, Certificate),
- {ok, deleted};
- false ->
- {error, not_found}
- end.
-
-revoke_certificate1(CAUrl, Cert = #data_cert{pem=PemEncodedCert}) ->
- {ok, _AccId, PrivateKey} = ensure_account_exists(),
-
- Certificate = prepare_certificate_revoke(PemEncodedCert),
-
- {ok, Dirs, Nonce} = ejabberd_acme_comm:directory(CAUrl),
-
- Req = [{<<"certificate">>, Certificate}],
- {ok, [], Nonce1} = ejabberd_acme_comm:revoke_cert(Dirs, PrivateKey, Req, Nonce),
- ok.
-
-prepare_certificate_revoke(PemEncodedCert) ->
- PemList = public_key:pem_decode(PemEncodedCert),
- PemCertEnc = lists:keyfind('Certificate', 1, PemList),
- PemCert = public_key:pem_entry_decode(PemCertEnc),
- DerCert = public_key:der_encode('Certificate', PemCert),
- Base64Cert = base64url:encode(DerCert),
- Base64Cert.
-
-domain_certificate_exists(Domain) ->
- {ok, Certs} = read_certificates_persistent(),
- lists:keyfind(Domain, 1, Certs).
-
-%%
-%% List Certificates
-%%
-
-list_certificates(Verbose) ->
- try
- list_certificates0(Verbose)
- catch
- throw:Throw ->
- Throw;
- E:R ->
- ?ERROR_MSG("Unknown ~p:~p, ~p", [E, R, erlang:get_stacktrace()]),
- {error, list_certificates}
- end.
-
-list_certificates0(Verbose) ->
- {ok, Certs} = read_certificates_persistent(),
- case Verbose of
- "plain" ->
- [format_certificate(DataCert) || {_Key, DataCert} <- Certs];
- "verbose" ->
- Certs
- end.
-
-%% TODO: Make this cleaner and more robust
-format_certificate(DataCert) ->
- #data_cert{
- domain = DomainName,
- pem = PemCert,
- path = Path
- } = DataCert,
-
- PemList = public_key:pem_decode(PemCert),
- PemEntryCert = lists:keyfind('Certificate', 1, PemList),
- Certificate = public_key:pem_entry_decode(PemEntryCert),
-
- %% Find the commonName
- _CommonName = get_commonName(Certificate),
-
- %% Find the notAfter date
- NotAfter = get_notAfter(Certificate),
-
- format_certificate1(DomainName, NotAfter, Path).
-
-format_certificate1(DomainName, NotAfter, Path) ->
- Result = lists:flatten(io_lib:format(
- " Domain: ~s~n"
- " Valid until: ~s UTC~n"
- " Path: ~s",
- [DomainName, NotAfter, Path])),
- Result.
-
-get_commonName(#'Certificate'{tbsCertificate = TbsCertificate}) ->
- #'TBSCertificate'{
- subject = {rdnSequence, SubjectList}
- } = TbsCertificate,
-
- %% TODO: Not the best way to find the commonName
- ShallowSubjectList = [Attribute || [Attribute] <- SubjectList],
- {_, _, CommonName} = lists:keyfind(attribute_oid(commonName), 2, ShallowSubjectList),
-
- %% TODO: Remove the length-encoding from the commonName before returning it
- CommonName.
-
-get_notAfter(#'Certificate'{tbsCertificate = TbsCertificate}) ->
- #'TBSCertificate'{
- validity = Validity
- } = TbsCertificate,
-
- %% TODO: Find a library function to decode utc time
- #'Validity'{notAfter = {utcTime, UtcTime}} = Validity,
- [Y1,Y2,MO1,MO2,D1,D2,H1,H2,MI1,MI2,S1,S2,$Z] = UtcTime,
- YEAR = case list_to_integer([Y1,Y2]) >= 50 of
- true -> "19" ++ [Y1,Y2];
- _ -> "20" ++ [Y1,Y2]
- end,
- NotAfter = lists:flatten(io_lib:format("~s-~s-~s ~s:~s:~s",
- [YEAR, [MO1,MO2], [D1,D2],
- [H1,H2], [MI1,MI2], [S1,S2]])),
- NotAfter.
%%
%% Get Certificate
@@ -212,7 +90,7 @@ get_certificates0(CAUrl, "new-account") ->
no_return().
get_certificates1(CAUrl, PrivateKey) ->
%% Read Config
- {ok, Hosts} = get_config_hosts(),
+ Hosts = get_config_hosts(),
%% Get a certificate for each host
PemCertKeys = [get_certificate(CAUrl, Host, PrivateKey) || Host <- Hosts],
@@ -225,7 +103,7 @@ get_certificates1(CAUrl, PrivateKey) ->
SavedCerts.
-spec get_certificate(url(), bitstring(), jose_jwk:key()) ->
- {'ok', bitstring(), pem_certificate()} |
+ {'ok', bitstring(), pem()} |
{'error', bitstring(), _}.
get_certificate(CAUrl, DomainName, PrivateKey) ->
?INFO_MSG("Getting a Certificate for domain: ~p~n", [DomainName]),
@@ -243,7 +121,7 @@ get_certificate(CAUrl, DomainName, PrivateKey) ->
-spec create_save_new_account(url()) -> {'ok', string(), jose_jwk:key()} | no_return().
create_save_new_account(CAUrl) ->
%% Get contact from configuration file
- {ok, Contact} = get_config_contact(),
+ Contact = get_config_contact(),
%% Generate a Key
PrivateKey = generate_key(),
@@ -309,6 +187,8 @@ create_new_authorization(CAUrl, DomainName, PrivateKey) ->
throw({error, DomainName, authorization})
end.
+-spec create_new_certificate(url(), bitstring(), jose_jwk:key()) ->
+ {ok, bitstring(), pem()}.
create_new_certificate(CAUrl, DomainName, PrivateKey) ->
try
{ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl),
@@ -339,6 +219,7 @@ create_new_certificate(CAUrl, DomainName, PrivateKey) ->
throw({error, DomainName, certificate})
end.
+-spec ensure_account_exists() -> {ok, string(), jose_jwk:key()}.
ensure_account_exists() ->
case read_account_persistent() of
none ->
@@ -348,6 +229,152 @@ ensure_account_exists() ->
{ok, AccId, PrivateKey}
end.
+
+%%
+%% List Certificates
+%%
+-spec list_certificates(verbose_opt()) -> [string()] | [any()] | {error, _}.
+list_certificates(Verbose) ->
+ try
+ list_certificates0(Verbose)
+ catch
+ throw:Throw ->
+ Throw;
+ E:R ->
+ ?ERROR_MSG("Unknown ~p:~p, ~p", [E, R, erlang:get_stacktrace()]),
+ {error, list_certificates}
+ end.
+
+-spec list_certificates0(verbose_opt()) -> [string()] | [any()].
+list_certificates0(Verbose) ->
+ Certs = read_certificates_persistent(),
+ case Verbose of
+ "plain" ->
+ [format_certificate(DataCert) || {_Key, DataCert} <- Certs];
+ "verbose" ->
+ Certs
+ end.
+
+%% TODO: Make this cleaner and more robust
+-spec format_certificate(data_cert()) -> string().
+format_certificate(DataCert) ->
+ #data_cert{
+ domain = DomainName,
+ pem = PemCert,
+ path = Path
+ } = DataCert,
+
+ PemList = public_key:pem_decode(PemCert),
+ PemEntryCert = lists:keyfind('Certificate', 1, PemList),
+ Certificate = public_key:pem_entry_decode(PemEntryCert),
+
+ %% Find the commonName
+ _CommonName = get_commonName(Certificate),
+
+ %% Find the notAfter date
+ NotAfter = get_notAfter(Certificate),
+
+ format_certificate1(DomainName, NotAfter, Path).
+
+-spec format_certificate1(bitstring(), string(), string()) -> string().
+format_certificate1(DomainName, NotAfter, Path) ->
+ Result = lists:flatten(io_lib:format(
+ " Domain: ~s~n"
+ " Valid until: ~s UTC~n"
+ " Path: ~s",
+ [DomainName, NotAfter, Path])),
+ Result.
+
+-spec get_commonName(#'Certificate'{}) -> string().
+get_commonName(#'Certificate'{tbsCertificate = TbsCertificate}) ->
+ #'TBSCertificate'{
+ subject = {rdnSequence, SubjectList}
+ } = TbsCertificate,
+
+ %% TODO: Not the best way to find the commonName
+ ShallowSubjectList = [Attribute || [Attribute] <- SubjectList],
+ {_, _, CommonName} = lists:keyfind(attribute_oid(commonName), 2, ShallowSubjectList),
+
+ %% TODO: Remove the length-encoding from the commonName before returning it
+ CommonName.
+
+-spec get_notAfter(#'Certificate'{}) -> string().
+get_notAfter(#'Certificate'{tbsCertificate = TbsCertificate}) ->
+ #'TBSCertificate'{
+ validity = Validity
+ } = TbsCertificate,
+
+ %% TODO: Find a library function to decode utc time
+ #'Validity'{notAfter = {utcTime, UtcTime}} = Validity,
+ [Y1,Y2,MO1,MO2,D1,D2,H1,H2,MI1,MI2,S1,S2,$Z] = UtcTime,
+ YEAR = case list_to_integer([Y1,Y2]) >= 50 of
+ true -> "19" ++ [Y1,Y2];
+ _ -> "20" ++ [Y1,Y2]
+ end,
+ NotAfter = lists:flatten(io_lib:format("~s-~s-~s ~s:~s:~s",
+ [YEAR, [MO1,MO2], [D1,D2],
+ [H1,H2], [MI1,MI2], [S1,S2]])),
+
+ NotAfter.
+
+
+%%
+%% Revoke Certificate
+%%
+
+%% Add a try-catch to this stub
+-spec revoke_certificate(url(), string()) -> {ok, deleted} | {error, _}.
+revoke_certificate(CAUrl, Domain) ->
+ try
+ revoke_certificate0(CAUrl, Domain)
+ catch
+ throw:Throw ->
+ Throw;
+ E:R ->
+ ?ERROR_MSG("Unknown ~p:~p, ~p", [E, R, erlang:get_stacktrace()]),
+ {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} ->
+ ?INFO_MSG("Certificate: ~p found!!", [Certificate]),
+ ok = revoke_certificate1(CAUrl, Certificate),
+ {ok, deleted};
+ false ->
+ {error, not_found}
+ end.
+
+-spec revoke_certificate1(url(), data_cert()) -> ok.
+revoke_certificate1(CAUrl, Cert = #data_cert{pem=PemEncodedCert}) ->
+ {ok, _AccId, PrivateKey} = ensure_account_exists(),
+
+ Certificate = prepare_certificate_revoke(PemEncodedCert),
+
+ {ok, Dirs, Nonce} = ejabberd_acme_comm:directory(CAUrl),
+
+ Req = [{<<"certificate">>, Certificate}],
+ {ok, [], Nonce1} = ejabberd_acme_comm:revoke_cert(Dirs, PrivateKey, Req, Nonce),
+ ok = remove_certificate_persistent(Cert),
+ ok.
+
+-spec prepare_certificate_revoke(pem()) -> bitstring().
+prepare_certificate_revoke(PemEncodedCert) ->
+ PemList = public_key:pem_decode(PemEncodedCert),
+ PemCertEnc = lists:keyfind('Certificate', 1, PemList),
+ PemCert = public_key:pem_entry_decode(PemCertEnc),
+ DerCert = public_key:der_encode('Certificate', PemCert),
+ Base64Cert = base64url:encode(DerCert),
+ Base64Cert.
+
+-spec domain_certificate_exists(bitstring()) -> {bitstring(), data_cert()} | false.
+domain_certificate_exists(Domain) ->
+ Certs = read_certificates_persistent(),
+ lists:keyfind(Domain, 1, Certs).
+
+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
%% Certificate Request Functions
@@ -508,13 +535,16 @@ get_challenges(Body) ->
{<<"challenges">>, Challenges} = proplists:lookup(<<"challenges">>, Body),
Challenges.
+-spec not_before_not_after() -> {binary(), binary()}.
not_before_not_after() ->
- %% TODO: Make notBefore and notAfter like they do it in other clients
+ %% TODO: Make notBefore and notAfter configurable somewhere
{MegS, Sec, MicS} = erlang:timestamp(),
- NotBefore = xmpp_util:encode_timestamp({MegS-1, Sec, MicS}),
- NotAfter = xmpp_util:encode_timestamp({MegS+1, Sec, MicS}),
+ NotBefore = xmpp_util:encode_timestamp({MegS, Sec, MicS}),
+ %% The certificate will be valid for 60 Days after today
+ NotAfter = xmpp_util:encode_timestamp({MegS+5, Sec+184000, MicS}),
{NotBefore, NotAfter}.
+-spec to_public(jose_jwk:key()) -> jose_jwk:key().
to_public(PrivateKey) ->
jose_jwk:to_public(PrivateKey).
%% case jose_jwk:to_key(PrivateKey) of
@@ -530,7 +560,7 @@ to_public(PrivateKey) ->
%% to_public(PrivateKey) ->
%% jose_jwk:to_public(PrivateKey).
-
+-spec is_error(_) -> boolean().
is_error({error, _}) -> true;
is_error(_) -> false.
@@ -539,7 +569,7 @@ is_error(_) -> false.
%% Handle the persistent data structure
%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-
+-spec data_empty() -> [].
data_empty() ->
[].
@@ -547,6 +577,7 @@ data_empty() ->
%% Account
%%
+-spec data_get_account(acme_data()) -> {ok, list(), jose_jwk:key()} | none.
data_get_account(Data) ->
case lists:keyfind(account, 1, Data) of
{account, #data_acc{id = AccId, key = PrivateKey}} ->
@@ -555,6 +586,7 @@ data_get_account(Data) ->
none
end.
+-spec data_set_account(acme_data(), {list(), jose_jwk:key()}) -> acme_data().
data_set_account(Data, {AccId, PrivateKey}) ->
NewAcc = {account, #data_acc{id = AccId, key = PrivateKey}},
lists:keystore(account, 1, Data, NewAcc).
@@ -563,23 +595,32 @@ data_set_account(Data, {AccId, PrivateKey}) ->
%% Certificates
%%
+-spec data_get_certificates(acme_data()) -> data_certs().
data_get_certificates(Data) ->
case lists:keyfind(certs, 1, Data) of
{certs, Certs} ->
- {ok, Certs};
+ Certs;
false ->
- {ok, []}
+ []
end.
+-spec data_set_certificates(acme_data(), data_certs()) -> acme_data().
data_set_certificates(Data, NewCerts) ->
lists:keystore(certs, 1, Data, {certs, NewCerts}).
%% ATM we preserve one certificate for each domain
+-spec data_add_certificate(acme_data(), data_cert()) -> acme_data().
data_add_certificate(Data, DataCert = #data_cert{domain=Domain}) ->
- {ok, Certs} = data_get_certificates(Data),
+ Certs = data_get_certificates(Data),
NewCerts = lists:keystore(Domain, 1, Certs, {Domain, DataCert}),
data_set_certificates(Data, NewCerts).
+-spec data_remove_certificate(acme_data(), data_cert()) -> acme_data().
+data_remove_certificate(Data, DataCert = #data_cert{domain=Domain}) ->
+ Certs = data_get_certificates(Data),
+ NewCerts = lists:keydelete(Domain, 1, Certs),
+ data_set_certificates(Data, NewCerts).
+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
@@ -587,14 +628,17 @@ data_add_certificate(Data, DataCert = #data_cert{domain=Domain}) ->
%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec persistent_file() -> file:filename().
persistent_file() ->
MnesiaDir = mnesia:system_info(directory),
filename:join(MnesiaDir, "acme.DAT").
-%% The persistent file should be rread and written only by its owner
+%% The persistent file should be read and written only by its owner
+-spec persistent_file_mode() -> 384.
persistent_file_mode() ->
8#400 + 8#200.
+-spec read_persistent() -> {ok, acme_data()} | no_return().
read_persistent() ->
case file:read_file(persistent_file()) of
{ok, Binary} ->
@@ -607,6 +651,7 @@ read_persistent() ->
throw({error, Reason})
end.
+-spec write_persistent(acme_data()) -> ok | no_return().
write_persistent(Data) ->
Binary = term_to_binary(Data),
case file:write_file(persistent_file(), Binary) of
@@ -616,6 +661,7 @@ write_persistent(Data) ->
throw({error, Reason})
end.
+-spec create_persistent() -> ok | no_return().
create_persistent() ->
Binary = term_to_binary(data_empty()),
case file:write_file(persistent_file(), Binary) of
@@ -631,30 +677,40 @@ create_persistent() ->
throw({error, Reason})
end.
+-spec write_account_persistent({list(), jose_jwk:key()}) -> ok | no_return().
write_account_persistent({AccId, PrivateKey}) ->
{ok, Data} = read_persistent(),
NewData = data_set_account(Data, {AccId, PrivateKey}),
ok = write_persistent(NewData).
+-spec read_account_persistent() -> {ok, list(), jose_jwk:key()} | none.
read_account_persistent() ->
{ok, Data} = read_persistent(),
data_get_account(Data).
+-spec read_certificates_persistent() -> data_certs().
read_certificates_persistent() ->
{ok, Data} = read_persistent(),
data_get_certificates(Data).
+-spec add_certificate_persistent(data_cert()) -> ok.
add_certificate_persistent(DataCert) ->
{ok, Data} = read_persistent(),
NewData = data_add_certificate(Data, DataCert),
ok = write_persistent(NewData).
+-spec remove_certificate_persistent(data_cert()) -> ok.
+remove_certificate_persistent(DataCert) ->
+ {ok, Data} = read_persistent(),
+ NewData = data_remove_certificate(Data, DataCert),
+ ok = write_persistent(NewData).
+-spec save_certificate({ok, bitstring(), binary()} | {error, _, _}) -> {ok, bitstring(), saved}.
save_certificate({error, _, _} = Error) ->
Error;
save_certificate({ok, DomainName, Cert}) ->
try
- {ok, CertDir} = get_config_cert_dir(),
+ CertDir = get_config_cert_dir(),
DomainString = bitstring_to_list(DomainName),
CertificateFile = filename:join([CertDir, DomainString ++ "_cert.pem"]),
%% TODO: At some point do the following using a Transaction so
@@ -676,6 +732,7 @@ save_certificate({ok, DomainName, Cert}) ->
{error, DomainName, saving}
end.
+-spec write_cert(file:filename(), binary(), bitstring()) -> {ok, bitstring(), saved}.
write_cert(CertificateFile, Cert, DomainName) ->
case file:write_file(CertificateFile, Cert) of
ok ->
@@ -686,41 +743,45 @@ write_cert(CertificateFile, Cert, DomainName) ->
throw({error, DomainName, saving})
end.
+-spec get_config_acme() -> [{atom(), bitstring()}].
get_config_acme() ->
case ejabberd_config:get_option(acme, undefined) of
undefined ->
?ERROR_MSG("No acme configuration has been specified", []),
throw({error, configuration});
Acme ->
- {ok, Acme}
+ Acme
end.
+-spec get_config_contact() -> bitstring().
get_config_contact() ->
- {ok, Acme} = get_config_acme(),
+ Acme = get_config_acme(),
case lists:keyfind(contact, 1, Acme) of
{contact, Contact} ->
- {ok, Contact};
+ Contact;
false ->
?ERROR_MSG("No contact has been specified", []),
throw({error, configuration_contact})
end.
+-spec get_config_hosts() -> [bitstring()].
get_config_hosts() ->
case ejabberd_config:get_option(hosts, undefined) of
undefined ->
?ERROR_MSG("No hosts have been specified", []),
throw({error, configuration_hosts});
Hosts ->
- {ok, Hosts}
+ Hosts
end.
+-spec get_config_cert_dir() -> file:filename().
get_config_cert_dir() ->
case ejabberd_config:get_option(cert_dir, undefined) of
undefined ->
?ERROR_MSG("No cert_dir configuration has been specified", []),
throw({error, configuration});
CertDir ->
- {ok, CertDir}
+ CertDir
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%