mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-22 16:20:52 +01:00
Implement renew_certificate command
This command renews the certificates for all domains that already have a certificate that has expired or is close to expiring. It is meant to be run automatically more often than the renewal process because if the certificates are valid nothing happens
This commit is contained in:
parent
7fa9a387ae
commit
9756b452d6
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
-export([%% Ejabberdctl Commands
|
-export([%% Ejabberdctl Commands
|
||||||
get_certificates/2,
|
get_certificates/2,
|
||||||
|
renew_certificates/1,
|
||||||
list_certificates/1,
|
list_certificates/1,
|
||||||
revoke_certificate/2,
|
revoke_certificate/2,
|
||||||
%% Command Options Validity
|
%% Command Options Validity
|
||||||
@ -52,11 +53,7 @@ is_valid_verbose_opt(_) -> false.
|
|||||||
%%
|
%%
|
||||||
|
|
||||||
%% Needs a hell lot of cleaning
|
%% Needs a hell lot of cleaning
|
||||||
-spec get_certificates(url(), account_opt()) ->
|
-spec get_certificates(url(), account_opt()) -> string() | {'error', _}.
|
||||||
{'ok', [{'ok', bitstring(), 'saved'}]} |
|
|
||||||
{'error',
|
|
||||||
[{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}]} |
|
|
||||||
{'error', _}.
|
|
||||||
get_certificates(CAUrl, NewAccountOpt) ->
|
get_certificates(CAUrl, NewAccountOpt) ->
|
||||||
try
|
try
|
||||||
?INFO_MSG("Persistent: ~p~n", [file:read_file_info(persistent_file())]),
|
?INFO_MSG("Persistent: ~p~n", [file:read_file_info(persistent_file())]),
|
||||||
@ -69,11 +66,7 @@ get_certificates(CAUrl, NewAccountOpt) ->
|
|||||||
{error, get_certificates}
|
{error, get_certificates}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec get_certificates0(url(), account_opt()) ->
|
-spec get_certificates0(url(), account_opt()) -> string().
|
||||||
{'ok', [{'ok', bitstring(), 'saved'}]} |
|
|
||||||
{'error',
|
|
||||||
[{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}]} |
|
|
||||||
no_return().
|
|
||||||
get_certificates0(CAUrl, "old-account") ->
|
get_certificates0(CAUrl, "old-account") ->
|
||||||
%% Get the current account
|
%% Get the current account
|
||||||
{ok, _AccId, PrivateKey} = ensure_account_exists(),
|
{ok, _AccId, PrivateKey} = ensure_account_exists(),
|
||||||
@ -86,11 +79,7 @@ get_certificates0(CAUrl, "new-account") ->
|
|||||||
|
|
||||||
get_certificates1(CAUrl, PrivateKey).
|
get_certificates1(CAUrl, PrivateKey).
|
||||||
|
|
||||||
-spec get_certificates1(url(), jose_jwk:key()) ->
|
-spec get_certificates1(url(), jose_jwk:key()) -> string().
|
||||||
{'ok', [{'ok', bitstring(), 'saved'}]} |
|
|
||||||
{'error',
|
|
||||||
[{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}]} |
|
|
||||||
no_return().
|
|
||||||
get_certificates1(CAUrl, PrivateKey) ->
|
get_certificates1(CAUrl, PrivateKey) ->
|
||||||
%% Read Config
|
%% Read Config
|
||||||
Hosts = get_config_hosts(),
|
Hosts = get_config_hosts(),
|
||||||
@ -105,7 +94,7 @@ get_certificates1(CAUrl, PrivateKey) ->
|
|||||||
%% Result
|
%% Result
|
||||||
format_get_certificates_result(SavedCerts).
|
format_get_certificates_result(SavedCerts).
|
||||||
|
|
||||||
-spec format_get_certificates_result([{'ok', bitstring(), 'saved'} |
|
-spec format_get_certificates_result([{'ok', bitstring(), _} |
|
||||||
{'error', bitstring(), _}]) ->
|
{'error', bitstring(), _}]) ->
|
||||||
string().
|
string().
|
||||||
format_get_certificates_result(Certs) ->
|
format_get_certificates_result(Certs) ->
|
||||||
@ -123,11 +112,15 @@ format_get_certificates_result(Certs) ->
|
|||||||
lists:flatten(Result)
|
lists:flatten(Result)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec format_get_certificate({'ok', bitstring(), 'saved'} |
|
-spec format_get_certificate({'ok', bitstring(), _} |
|
||||||
{'error', bitstring(), _}) ->
|
{'error', bitstring(), _}) ->
|
||||||
string().
|
string().
|
||||||
format_get_certificate({ok, Domain, saved}) ->
|
format_get_certificate({ok, Domain, saved}) ->
|
||||||
io_lib:format(" Certificate for domain: \"~s\" acquired and saved", [Domain]);
|
io_lib:format(" Certificate for domain: \"~s\" acquired and saved", [Domain]);
|
||||||
|
format_get_certificate({ok, Domain, not_found}) ->
|
||||||
|
io_lib:format(" Certificate for domain: \"~s\" not found, so it was not renewed", [Domain]);
|
||||||
|
format_get_certificate({ok, Domain, exists}) ->
|
||||||
|
io_lib:format(" Certificate for domain: \"~s\" is not close to expiring", [Domain]);
|
||||||
format_get_certificate({error, Domain, Reason}) ->
|
format_get_certificate({error, Domain, Reason}) ->
|
||||||
io_lib:format(" Error for domain: \"~s\", with reason: \'~s\'", [Domain, Reason]).
|
io_lib:format(" Error for domain: \"~s\", with reason: \'~s\'", [Domain, Reason]).
|
||||||
|
|
||||||
@ -205,7 +198,7 @@ create_new_authorization(CAUrl, DomainName, PrivateKey) ->
|
|||||||
{ok, ChallengeId} = location_to_id(ChallengeUrl),
|
{ok, ChallengeId} = location_to_id(ChallengeUrl),
|
||||||
Req3 = [{<<"type">>, <<"http-01">>},{<<"keyAuthorization">>, KeyAuthz}],
|
Req3 = [{<<"type">>, <<"http-01">>},{<<"keyAuthorization">>, KeyAuthz}],
|
||||||
{ok, _SolvedChallenge, _Nonce2} = ejabberd_acme_comm:complete_challenge(
|
{ok, _SolvedChallenge, _Nonce2} = ejabberd_acme_comm:complete_challenge(
|
||||||
{CAUrl, AuthzId, ChallengeId}, PrivateKey, Req3, Nonce1),
|
{CAUrl, AuthzId, ChallengeId}, PrivateKey, Req3, Nonce1),
|
||||||
|
|
||||||
{ok, AuthzValid, _Nonce} = ejabberd_acme_comm:get_authz_until_valid({CAUrl, AuthzId}),
|
{ok, AuthzValid, _Nonce} = ejabberd_acme_comm:get_authz_until_valid({CAUrl, AuthzId}),
|
||||||
{ok, AuthzValid}
|
{ok, AuthzValid}
|
||||||
@ -259,6 +252,81 @@ ensure_account_exists() ->
|
|||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
||||||
|
%%
|
||||||
|
%% Renew Certificates
|
||||||
|
%%
|
||||||
|
-spec renew_certificates(url()) -> string() | {'error', _}.
|
||||||
|
renew_certificates(CAUrl) ->
|
||||||
|
try
|
||||||
|
renew_certificates0(CAUrl)
|
||||||
|
catch
|
||||||
|
throw:Throw ->
|
||||||
|
Throw;
|
||||||
|
E:R ->
|
||||||
|
?ERROR_MSG("Unknown ~p:~p, ~p", [E, R, erlang:get_stacktrace()]),
|
||||||
|
{error, get_certificates}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec renew_certificates0(url()) -> string().
|
||||||
|
renew_certificates0(CAUrl) ->
|
||||||
|
%% Get the current account
|
||||||
|
{ok, _AccId, PrivateKey} = ensure_account_exists(),
|
||||||
|
|
||||||
|
%% Read Config
|
||||||
|
Hosts = get_config_hosts(),
|
||||||
|
|
||||||
|
%% Get a certificate for each host
|
||||||
|
PemCertKeys = [renew_certificate(CAUrl, Host, PrivateKey) || Host <- Hosts],
|
||||||
|
|
||||||
|
%% Save Certificates
|
||||||
|
SavedCerts = [save_renewed_certificate(Cert) || Cert <- PemCertKeys],
|
||||||
|
|
||||||
|
%% Format the result to send back to ejabberdctl
|
||||||
|
%% Result
|
||||||
|
format_get_certificates_result(SavedCerts).
|
||||||
|
|
||||||
|
-spec renew_certificate(url(), bitstring(), jose_jwk:key()) ->
|
||||||
|
{'ok', bitstring(), _} |
|
||||||
|
{'error', bitstring(), _}.
|
||||||
|
renew_certificate(CAUrl, DomainName, PrivateKey) ->
|
||||||
|
case cert_to_expire(DomainName) of
|
||||||
|
true ->
|
||||||
|
get_certificate(CAUrl, DomainName, PrivateKey);
|
||||||
|
{false, not_found} ->
|
||||||
|
{ok, DomainName, not_found};
|
||||||
|
{false, PemCert} ->
|
||||||
|
{ok, DomainName, exists}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec cert_to_expire(bitstring()) -> 'true' |
|
||||||
|
{'false', pem()} |
|
||||||
|
{'false', not_found}.
|
||||||
|
cert_to_expire(DomainName) ->
|
||||||
|
Certs = read_certificates_persistent(),
|
||||||
|
case lists:keyfind(DomainName, 1, Certs) of
|
||||||
|
{DomainName, #data_cert{pem = Pem}} ->
|
||||||
|
Certificate = pem_to_certificate(Pem),
|
||||||
|
Validity = get_utc_validity(Certificate),
|
||||||
|
case close_to_expire(Validity) of
|
||||||
|
true ->
|
||||||
|
true;
|
||||||
|
false ->
|
||||||
|
{false, Pem}
|
||||||
|
end;
|
||||||
|
false ->
|
||||||
|
{false, not_found}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec close_to_expire(string()) -> boolean().
|
||||||
|
close_to_expire(Validity) ->
|
||||||
|
{ValidDate, _ValidTime} = utc_string_to_datetime(Validity),
|
||||||
|
ValidDays = calendar:date_to_gregorian_days(ValidDate),
|
||||||
|
|
||||||
|
{CurrentDate, _CurrentTime} = calendar:universal_time(),
|
||||||
|
CurrentDays = calendar:date_to_gregorian_days(CurrentDate),
|
||||||
|
CurrentDays > ValidDays - 30.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
%%
|
%%
|
||||||
%% List Certificates
|
%% List Certificates
|
||||||
@ -290,9 +358,7 @@ format_certificate(DataCert, Verbose) ->
|
|||||||
} = DataCert,
|
} = DataCert,
|
||||||
|
|
||||||
try
|
try
|
||||||
PemList = public_key:pem_decode(PemCert),
|
Certificate = pem_to_certificate(PemCert),
|
||||||
PemEntryCert = lists:keyfind('Certificate', 1, PemList),
|
|
||||||
Certificate = public_key:pem_entry_decode(PemEntryCert),
|
|
||||||
|
|
||||||
%% Find the commonName
|
%% Find the commonName
|
||||||
_CommonName = get_commonName(Certificate),
|
_CommonName = get_commonName(Certificate),
|
||||||
@ -604,19 +670,44 @@ not_before_not_after() ->
|
|||||||
-spec to_public(jose_jwk:key()) -> jose_jwk:key().
|
-spec to_public(jose_jwk:key()) -> jose_jwk:key().
|
||||||
to_public(PrivateKey) ->
|
to_public(PrivateKey) ->
|
||||||
jose_jwk:to_public(PrivateKey).
|
jose_jwk:to_public(PrivateKey).
|
||||||
%% case jose_jwk:to_key(PrivateKey) of
|
%% case jose_jwk:to_key(PrivateKey) of
|
||||||
%% #'RSAPrivateKey'{modulus = Mod, publicExponent = Exp} ->
|
%% #'RSAPrivateKey'{modulus = Mod, publicExponent = Exp} ->
|
||||||
%% Public = #'RSAPublicKey'{modulus = Mod, publicExponent = Exp},
|
%% Public = #'RSAPublicKey'{modulus = Mod, publicExponent = Exp},
|
||||||
%% jose_jwk:from_key(Public);
|
%% jose_jwk:from_key(Public);
|
||||||
%% _ ->
|
%% _ ->
|
||||||
%% jose_jwk:to_public(PrivateKey)
|
%% jose_jwk:to_public(PrivateKey)
|
||||||
%% end.
|
%% end.
|
||||||
|
|
||||||
%% to_public(#'RSAPrivateKey'{modulus = Mod, publicExponent = Exp}) ->
|
%% to_public(#'RSAPrivateKey'{modulus = Mod, publicExponent = Exp}) ->
|
||||||
%% #'RSAPublicKey'{modulus = Mod, publicExponent = Exp};
|
%% #'RSAPublicKey'{modulus = Mod, publicExponent = Exp};
|
||||||
%% to_public(PrivateKey) ->
|
%% to_public(PrivateKey) ->
|
||||||
%% jose_jwk:to_public(PrivateKey).
|
%% jose_jwk:to_public(PrivateKey).
|
||||||
|
|
||||||
|
-spec pem_to_certificate(pem()) -> #'Certificate'{}.
|
||||||
|
pem_to_certificate(Pem) ->
|
||||||
|
PemList = public_key:pem_decode(Pem),
|
||||||
|
PemEntryCert = lists:keyfind('Certificate', 1, PemList),
|
||||||
|
Certificate = public_key:pem_entry_decode(PemEntryCert),
|
||||||
|
Certificate.
|
||||||
|
|
||||||
|
%% TODO: Find a better and more robust way to parse the utc string
|
||||||
|
-spec utc_string_to_datetime(string()) -> calendar:datetime().
|
||||||
|
utc_string_to_datetime(UtcString) ->
|
||||||
|
try
|
||||||
|
[Y1,Y2,MO1,MO2,D1,D2,H1,H2,MI1,MI2,S1,S2,$Z] = UtcString,
|
||||||
|
Year = list_to_integer("20" ++ [Y1,Y2]),
|
||||||
|
Month = list_to_integer([MO1, MO2]),
|
||||||
|
Day = list_to_integer([D1,D2]),
|
||||||
|
Hour = list_to_integer([H1,H2]),
|
||||||
|
Minute = list_to_integer([MI1,MI2]),
|
||||||
|
Second = list_to_integer([S1,S2]),
|
||||||
|
{{Year, Month, Day}, {Hour, Minute, Second}}
|
||||||
|
catch
|
||||||
|
E:R ->
|
||||||
|
?ERROR_MSG("Unable to parse UTC string", []),
|
||||||
|
throw({error, utc_string_to_datetime})
|
||||||
|
end.
|
||||||
|
|
||||||
-spec is_error(_) -> boolean().
|
-spec is_error(_) -> boolean().
|
||||||
is_error({error, _}) -> true;
|
is_error({error, _}) -> true;
|
||||||
is_error({error, _, _}) -> true;
|
is_error({error, _, _}) -> true;
|
||||||
@ -763,7 +854,8 @@ remove_certificate_persistent(DataCert) ->
|
|||||||
NewData = data_remove_certificate(Data, DataCert),
|
NewData = data_remove_certificate(Data, DataCert),
|
||||||
ok = write_persistent(NewData).
|
ok = write_persistent(NewData).
|
||||||
|
|
||||||
-spec save_certificate({ok, bitstring(), binary()} | {error, _, _}) -> {ok, bitstring(), saved}.
|
-spec save_certificate({ok, bitstring(), binary()} | {error, _, _}) ->
|
||||||
|
{ok, bitstring(), saved} | {error, bitstring(), _}.
|
||||||
save_certificate({error, _, _} = Error) ->
|
save_certificate({error, _, _} = Error) ->
|
||||||
Error;
|
Error;
|
||||||
save_certificate({ok, DomainName, Cert}) ->
|
save_certificate({ok, DomainName, Cert}) ->
|
||||||
@ -790,6 +882,17 @@ save_certificate({ok, DomainName, Cert}) ->
|
|||||||
{error, DomainName, saving}
|
{error, DomainName, saving}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
-spec save_renewed_certificate({ok, bitstring(), _} | {error, _, _}) ->
|
||||||
|
{ok, bitstring(), _} | {error, bitstring(), _}.
|
||||||
|
save_renewed_certificate({error, _, _} = Error) ->
|
||||||
|
Error;
|
||||||
|
save_renewed_certificate({ok, _, not_found} = Cert) ->
|
||||||
|
Cert;
|
||||||
|
save_renewed_certificate({ok, _, exists} = Cert) ->
|
||||||
|
Cert;
|
||||||
|
save_renewed_certificate({ok, DomainName, Cert}) ->
|
||||||
|
save_certificate({ok, DomainName, Cert}).
|
||||||
|
|
||||||
-spec write_cert(file:filename(), binary(), bitstring()) -> {ok, bitstring(), saved}.
|
-spec write_cert(file:filename(), binary(), bitstring()) -> {ok, bitstring(), saved}.
|
||||||
write_cert(CertificateFile, Cert, DomainName) ->
|
write_cert(CertificateFile, Cert, DomainName) ->
|
||||||
case file:write_file(CertificateFile, Cert) of
|
case file:write_file(CertificateFile, Cert) of
|
||||||
@ -853,11 +956,11 @@ transaction([{Fun, Rollback} | Rest]) ->
|
|||||||
{ok, Result} = Fun(),
|
{ok, Result} = Fun(),
|
||||||
[Result | transaction(Rest)]
|
[Result | transaction(Rest)]
|
||||||
catch Type:Reason ->
|
catch Type:Reason ->
|
||||||
Rollback(),
|
Rollback(),
|
||||||
erlang:raise(Type, Reason, erlang:get_stacktrace())
|
erlang:raise(Type, Reason, erlang:get_stacktrace())
|
||||||
end;
|
end;
|
||||||
transaction([Fun | Rest]) ->
|
transaction([Fun | Rest]) ->
|
||||||
% not every action require cleanup on error
|
% not every action require cleanup on error
|
||||||
transaction([{Fun, fun () -> ok end} | Rest]);
|
transaction([{Fun, fun () -> ok end} | Rest]);
|
||||||
transaction([]) -> [].
|
transaction([]) -> [].
|
||||||
|
|
||||||
@ -995,7 +1098,7 @@ generate_key() ->
|
|||||||
Key = public_key:generate_key({rsa, 2048, 65537}),
|
Key = public_key:generate_key({rsa, 2048, 65537}),
|
||||||
Key1 = Key#'RSAPrivateKey'{version = 'two-prime'},
|
Key1 = Key#'RSAPrivateKey'{version = 'two-prime'},
|
||||||
jose_jwk:from_key(Key1).
|
jose_jwk:from_key(Key1).
|
||||||
%% jose_jwk:generate_key({rsa, 2048}).
|
%% jose_jwk:generate_key({rsa, 2048}).
|
||||||
-else.
|
-else.
|
||||||
generate_key() ->
|
generate_key() ->
|
||||||
?INFO_MSG("Generate EC key pair~n", []),
|
?INFO_MSG("Generate EC key pair~n", []),
|
||||||
|
@ -46,6 +46,7 @@
|
|||||||
import_file/1, import_dir/1,
|
import_file/1, import_dir/1,
|
||||||
%% Acme
|
%% Acme
|
||||||
get_certificate/1,
|
get_certificate/1,
|
||||||
|
renew_certificate/0,
|
||||||
list_certificates/1,
|
list_certificates/1,
|
||||||
revoke_certificate/1,
|
revoke_certificate/1,
|
||||||
%% Purge DB
|
%% Purge DB
|
||||||
@ -246,7 +247,6 @@ get_commands_spec() ->
|
|||||||
args_example = ["/var/lib/ejabberd/jabberd14/"],
|
args_example = ["/var/lib/ejabberd/jabberd14/"],
|
||||||
args = [{file, string}],
|
args = [{file, string}],
|
||||||
result = {res, restuple}},
|
result = {res, restuple}},
|
||||||
|
|
||||||
#ejabberd_commands{name = get_certificate, tags = [acme],
|
#ejabberd_commands{name = get_certificate, tags = [acme],
|
||||||
desc = "Gets a certificate for the specified domain. Can be used with {old-account|new-account}.",
|
desc = "Gets a certificate for the specified domain. Can be used with {old-account|new-account}.",
|
||||||
module = ?MODULE, function = get_certificate,
|
module = ?MODULE, function = get_certificate,
|
||||||
@ -254,6 +254,11 @@ get_commands_spec() ->
|
|||||||
args_example = ["old-account | new-account"],
|
args_example = ["old-account | new-account"],
|
||||||
args = [{option, string}],
|
args = [{option, string}],
|
||||||
result = {certificates, string}},
|
result = {certificates, string}},
|
||||||
|
#ejabberd_commands{name = renew_certificate, tags = [acme],
|
||||||
|
desc = "Renews all certificates that are close to expiring",
|
||||||
|
module = ?MODULE, function = renew_certificate,
|
||||||
|
args = [],
|
||||||
|
result = {certificates, string}},
|
||||||
#ejabberd_commands{name = list_certificates, tags = [acme],
|
#ejabberd_commands{name = list_certificates, tags = [acme],
|
||||||
desc = "Lists all curently handled certificates and their respective domains in {plain|verbose} format",
|
desc = "Lists all curently handled certificates and their respective domains in {plain|verbose} format",
|
||||||
module = ?MODULE, function = list_certificates,
|
module = ?MODULE, function = list_certificates,
|
||||||
@ -579,6 +584,9 @@ get_certificate(UseNewAccount) ->
|
|||||||
{invalid_option, String}
|
{invalid_option, String}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
renew_certificate() ->
|
||||||
|
ejabberd_acme:renew_certificates("http://localhost:4000").
|
||||||
|
|
||||||
list_certificates(Verbose) ->
|
list_certificates(Verbose) ->
|
||||||
case ejabberd_acme:is_valid_verbose_opt(Verbose) of
|
case ejabberd_acme:is_valid_verbose_opt(Verbose) of
|
||||||
true ->
|
true ->
|
||||||
|
Loading…
Reference in New Issue
Block a user