mirror of
https://github.com/processone/ejabberd.git
synced 2024-12-22 17:28:25 +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
|
||||
get_certificates/2,
|
||||
renew_certificates/1,
|
||||
list_certificates/1,
|
||||
revoke_certificate/2,
|
||||
%% Command Options Validity
|
||||
@ -52,11 +53,7 @@ is_valid_verbose_opt(_) -> false.
|
||||
%%
|
||||
|
||||
%% Needs a hell lot of cleaning
|
||||
-spec get_certificates(url(), account_opt()) ->
|
||||
{'ok', [{'ok', bitstring(), 'saved'}]} |
|
||||
{'error',
|
||||
[{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}]} |
|
||||
{'error', _}.
|
||||
-spec get_certificates(url(), account_opt()) -> string() | {'error', _}.
|
||||
get_certificates(CAUrl, NewAccountOpt) ->
|
||||
try
|
||||
?INFO_MSG("Persistent: ~p~n", [file:read_file_info(persistent_file())]),
|
||||
@ -69,11 +66,7 @@ get_certificates(CAUrl, NewAccountOpt) ->
|
||||
{error, get_certificates}
|
||||
end.
|
||||
|
||||
-spec get_certificates0(url(), account_opt()) ->
|
||||
{'ok', [{'ok', bitstring(), 'saved'}]} |
|
||||
{'error',
|
||||
[{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}]} |
|
||||
no_return().
|
||||
-spec get_certificates0(url(), account_opt()) -> string().
|
||||
get_certificates0(CAUrl, "old-account") ->
|
||||
%% Get the current account
|
||||
{ok, _AccId, PrivateKey} = ensure_account_exists(),
|
||||
@ -83,14 +76,10 @@ get_certificates0(CAUrl, "old-account") ->
|
||||
get_certificates0(CAUrl, "new-account") ->
|
||||
%% Create a new account and save it to disk
|
||||
{ok, _Id, PrivateKey} = create_save_new_account(CAUrl),
|
||||
|
||||
|
||||
get_certificates1(CAUrl, PrivateKey).
|
||||
|
||||
-spec get_certificates1(url(), jose_jwk:key()) ->
|
||||
{'ok', [{'ok', bitstring(), 'saved'}]} |
|
||||
{'error',
|
||||
[{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}]} |
|
||||
no_return().
|
||||
-spec get_certificates1(url(), jose_jwk:key()) -> string().
|
||||
get_certificates1(CAUrl, PrivateKey) ->
|
||||
%% Read Config
|
||||
Hosts = get_config_hosts(),
|
||||
@ -105,7 +94,7 @@ get_certificates1(CAUrl, PrivateKey) ->
|
||||
%% Result
|
||||
format_get_certificates_result(SavedCerts).
|
||||
|
||||
-spec format_get_certificates_result([{'ok', bitstring(), 'saved'} |
|
||||
-spec format_get_certificates_result([{'ok', bitstring(), _} |
|
||||
{'error', bitstring(), _}]) ->
|
||||
string().
|
||||
format_get_certificates_result(Certs) ->
|
||||
@ -123,11 +112,15 @@ format_get_certificates_result(Certs) ->
|
||||
lists:flatten(Result)
|
||||
end.
|
||||
|
||||
-spec format_get_certificate({'ok', bitstring(), 'saved'} |
|
||||
-spec format_get_certificate({'ok', bitstring(), _} |
|
||||
{'error', bitstring(), _}) ->
|
||||
string().
|
||||
format_get_certificate({ok, Domain, saved}) ->
|
||||
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}) ->
|
||||
io_lib:format(" Error for domain: \"~s\", with reason: \'~s\'", [Domain, Reason]).
|
||||
|
||||
@ -160,7 +153,7 @@ create_save_new_account(CAUrl) ->
|
||||
|
||||
%% Write Persistent Data
|
||||
ok = write_account_persistent({Id, PrivateKey}),
|
||||
|
||||
|
||||
{ok, Id, PrivateKey}.
|
||||
|
||||
%% TODO:
|
||||
@ -205,7 +198,7 @@ create_new_authorization(CAUrl, DomainName, PrivateKey) ->
|
||||
{ok, ChallengeId} = location_to_id(ChallengeUrl),
|
||||
Req3 = [{<<"type">>, <<"http-01">>},{<<"keyAuthorization">>, KeyAuthz}],
|
||||
{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}
|
||||
@ -259,6 +252,81 @@ ensure_account_exists() ->
|
||||
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
|
||||
@ -288,11 +356,9 @@ format_certificate(DataCert, Verbose) ->
|
||||
pem = PemCert,
|
||||
path = Path
|
||||
} = DataCert,
|
||||
|
||||
|
||||
try
|
||||
PemList = public_key:pem_decode(PemCert),
|
||||
PemEntryCert = lists:keyfind('Certificate', 1, PemList),
|
||||
Certificate = public_key:pem_entry_decode(PemEntryCert),
|
||||
Certificate = pem_to_certificate(PemCert),
|
||||
|
||||
%% Find the commonName
|
||||
_CommonName = get_commonName(Certificate),
|
||||
@ -343,11 +409,11 @@ 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.
|
||||
|
||||
@ -409,9 +475,9 @@ 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),
|
||||
@ -604,19 +670,44 @@ not_before_not_after() ->
|
||||
-spec to_public(jose_jwk:key()) -> jose_jwk:key().
|
||||
to_public(PrivateKey) ->
|
||||
jose_jwk:to_public(PrivateKey).
|
||||
%% case jose_jwk:to_key(PrivateKey) of
|
||||
%% #'RSAPrivateKey'{modulus = Mod, publicExponent = Exp} ->
|
||||
%% Public = #'RSAPublicKey'{modulus = Mod, publicExponent = Exp},
|
||||
%% jose_jwk:from_key(Public);
|
||||
%% _ ->
|
||||
%% jose_jwk:to_public(PrivateKey)
|
||||
%% end.
|
||||
|
||||
%% case jose_jwk:to_key(PrivateKey) of
|
||||
%% #'RSAPrivateKey'{modulus = Mod, publicExponent = Exp} ->
|
||||
%% Public = #'RSAPublicKey'{modulus = Mod, publicExponent = Exp},
|
||||
%% jose_jwk:from_key(Public);
|
||||
%% _ ->
|
||||
%% jose_jwk:to_public(PrivateKey)
|
||||
%% end.
|
||||
|
||||
%% to_public(#'RSAPrivateKey'{modulus = Mod, publicExponent = Exp}) ->
|
||||
%% #'RSAPublicKey'{modulus = Mod, publicExponent = Exp};
|
||||
%% 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().
|
||||
is_error({error, _}) -> true;
|
||||
is_error({error, _, _}) -> true;
|
||||
@ -763,7 +854,8 @@ remove_certificate_persistent(DataCert) ->
|
||||
NewData = data_remove_certificate(Data, DataCert),
|
||||
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) ->
|
||||
Error;
|
||||
save_certificate({ok, DomainName, Cert}) ->
|
||||
@ -790,6 +882,17 @@ save_certificate({ok, DomainName, Cert}) ->
|
||||
{error, DomainName, saving}
|
||||
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}.
|
||||
write_cert(CertificateFile, Cert, DomainName) ->
|
||||
case file:write_file(CertificateFile, Cert) of
|
||||
@ -853,11 +956,11 @@ transaction([{Fun, Rollback} | Rest]) ->
|
||||
{ok, Result} = Fun(),
|
||||
[Result | transaction(Rest)]
|
||||
catch Type:Reason ->
|
||||
Rollback(),
|
||||
erlang:raise(Type, Reason, erlang:get_stacktrace())
|
||||
Rollback(),
|
||||
erlang:raise(Type, Reason, erlang:get_stacktrace())
|
||||
end;
|
||||
transaction([Fun | Rest]) ->
|
||||
% not every action require cleanup on error
|
||||
% not every action require cleanup on error
|
||||
transaction([{Fun, fun () -> ok end} | Rest]);
|
||||
transaction([]) -> [].
|
||||
|
||||
@ -995,7 +1098,7 @@ generate_key() ->
|
||||
Key = public_key:generate_key({rsa, 2048, 65537}),
|
||||
Key1 = Key#'RSAPrivateKey'{version = 'two-prime'},
|
||||
jose_jwk:from_key(Key1).
|
||||
%% jose_jwk:generate_key({rsa, 2048}).
|
||||
%% jose_jwk:generate_key({rsa, 2048}).
|
||||
-else.
|
||||
generate_key() ->
|
||||
?INFO_MSG("Generate EC key pair~n", []),
|
||||
|
@ -46,6 +46,7 @@
|
||||
import_file/1, import_dir/1,
|
||||
%% Acme
|
||||
get_certificate/1,
|
||||
renew_certificate/0,
|
||||
list_certificates/1,
|
||||
revoke_certificate/1,
|
||||
%% Purge DB
|
||||
@ -246,7 +247,6 @@ get_commands_spec() ->
|
||||
args_example = ["/var/lib/ejabberd/jabberd14/"],
|
||||
args = [{file, string}],
|
||||
result = {res, restuple}},
|
||||
|
||||
#ejabberd_commands{name = get_certificate, tags = [acme],
|
||||
desc = "Gets a certificate for the specified domain. Can be used with {old-account|new-account}.",
|
||||
module = ?MODULE, function = get_certificate,
|
||||
@ -254,6 +254,11 @@ get_commands_spec() ->
|
||||
args_example = ["old-account | new-account"],
|
||||
args = [{option, 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],
|
||||
desc = "Lists all curently handled certificates and their respective domains in {plain|verbose} format",
|
||||
module = ?MODULE, function = list_certificates,
|
||||
@ -579,6 +584,9 @@ get_certificate(UseNewAccount) ->
|
||||
{invalid_option, String}
|
||||
end.
|
||||
|
||||
renew_certificate() ->
|
||||
ejabberd_acme:renew_certificates("http://localhost:4000").
|
||||
|
||||
list_certificates(Verbose) ->
|
||||
case ejabberd_acme:is_valid_verbose_opt(Verbose) of
|
||||
true ->
|
||||
|
Loading…
Reference in New Issue
Block a user