24
1
mirror of https://github.com/processone/ejabberd.git synced 2024-06-12 21:52:07 +02:00
xmpp.chapril.org-ejabberd/src/ejabberd_acme.erl
Konstantinos Kallas 2b1fea01cd Renew certificate now renews all saved certificates that are close to expire
Before this commit renew_certificate only checked the hosts in the config file and renewd the certificates for those. However the user can request certificates apart from the hosts in the config file so he should be able to also renew them.
2017-08-10 18:54:26 +03:00

992 lines
30 KiB
Erlang

-module (ejabberd_acme).
-export([%% Ejabberdctl Commands
get_certificates/3,
renew_certificates/1,
list_certificates/1,
revoke_certificate/2,
%% Command Options Validity
is_valid_account_opt/1,
is_valid_verbose_opt/1,
is_valid_domain_opt/1,
%% Key Related
generate_key/0,
to_public/1
]).
-include("ejabberd.hrl").
-include("logger.hrl").
-include("xmpp.hrl").
-include("ejabberd_acme.hrl").
-include_lib("public_key/include/public_key.hrl").
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
%% Command Functions
%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
%% Check Validity of command options
%%
-spec is_valid_account_opt(string()) -> boolean().
is_valid_account_opt("old-account") -> true;
is_valid_account_opt("new-account") -> true;
is_valid_account_opt(_) -> false.
-spec is_valid_verbose_opt(string()) -> boolean().
is_valid_verbose_opt("plain") -> true;
is_valid_verbose_opt("verbose") -> true;
is_valid_verbose_opt(_) -> false.
%% TODO: Make this check more complicated
-spec is_valid_domain_opt(string()) -> boolean().
is_valid_domain_opt("all") -> true;
is_valid_domain_opt(DomainString) ->
case parse_domain_string(DomainString) of
[] ->
false;
SeparatedDomains ->
true
end.
%%
%% Get Certificate
%%
-spec get_certificates(url(), domains_opt(), account_opt()) -> string() | {'error', _}.
get_certificates(CAUrl, Domains, NewAccountOpt) ->
try
get_certificates0(CAUrl, Domains, NewAccountOpt)
catch
throw:Throw ->
Throw;
E:R ->
?ERROR_MSG("Unknown ~p:~p, ~p", [E, R, erlang:get_stacktrace()]),
{error, get_certificates}
end.
-spec get_certificates0(url(), domains_opt(), account_opt()) -> string().
get_certificates0(CAUrl, Domains, "old-account") ->
%% Get the current account
{ok, _AccId, PrivateKey} = ensure_account_exists(),
get_certificates1(CAUrl, Domains, PrivateKey);
get_certificates0(CAUrl, Domains, "new-account") ->
%% Create a new account and save it to disk
{ok, _Id, PrivateKey} = create_save_new_account(CAUrl),
get_certificates1(CAUrl, Domains, PrivateKey).
-spec get_certificates1(url(), domains_opt(), jose_jwk:key()) -> string().
get_certificates1(CAUrl, "all", PrivateKey) ->
Hosts = get_config_hosts(),
get_certificates2(CAUrl, PrivateKey, Hosts);
get_certificates1(CAUrl, DomainString, PrivateKey) ->
Domains = parse_domain_string(DomainString),
Hosts = [list_to_bitstring(D) || D <- Domains],
get_certificates2(CAUrl, PrivateKey, Hosts).
-spec get_certificates2(url(), jose_jwk:key(), [bitstring()]) -> string().
get_certificates2(CAUrl, PrivateKey, Hosts) ->
%% Get a certificate for each host
PemCertKeys = [get_certificate(CAUrl, Host, PrivateKey) || Host <- Hosts],
%% Save Certificates
SavedCerts = [save_certificate(Cert) || Cert <- PemCertKeys],
%% Format the result to send back to ejabberdctl
format_get_certificates_result(SavedCerts).
-spec format_get_certificates_result([{'ok', bitstring(), _} |
{'error', bitstring(), _}]) ->
string().
format_get_certificates_result(Certs) ->
Cond = lists:all(fun(Cert) ->
not is_error(Cert)
end, Certs),
FormattedCerts = lists:join($\n,
[format_get_certificate(C) || C <- Certs]),
case Cond of
true ->
Result = io_lib:format("Success:~n~s", [FormattedCerts]),
lists:flatten(Result);
_ ->
Result = io_lib:format("Error with one or more certificates~n~s", [lists:flatten(FormattedCerts)]),
lists:flatten(Result)
end.
-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]).
-spec get_certificate(url(), bitstring(), jose_jwk:key()) ->
{'ok', bitstring(), pem()} |
{'error', bitstring(), _}.
get_certificate(CAUrl, DomainName, PrivateKey) ->
?INFO_MSG("Getting a Certificate for domain: ~p~n", [DomainName]),
try
{ok, _Authz} = create_new_authorization(CAUrl, DomainName, PrivateKey),
create_new_certificate(CAUrl, DomainName, PrivateKey)
catch
throw:Throw ->
Throw;
E:R ->
?ERROR_MSG("Unknown ~p:~p, ~p", [E, R, erlang:get_stacktrace()]),
{error, DomainName, get_certificate}
end.
-spec create_save_new_account(url()) -> {'ok', string(), jose_jwk:key()} | no_return().
create_save_new_account(CAUrl) ->
%% Get contact from configuration file
Contact = get_config_contact(),
%% Generate a Key
PrivateKey = generate_key(),
%% Create a new account
{ok, Id} = create_new_account(CAUrl, Contact, PrivateKey),
%% Write Persistent Data
ok = write_account_persistent({Id, PrivateKey}),
{ok, Id, PrivateKey}.
%% TODO:
%% Find a way to ask the user if he accepts the TOS
-spec create_new_account(url(), bitstring(), jose_jwk:key()) -> {'ok', string()} |
no_return().
create_new_account(CAUrl, Contact, PrivateKey) ->
try
{ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl),
Req0 = [{ <<"contact">>, [Contact]}],
{ok, {TOS, Account}, Nonce1} =
ejabberd_acme_comm:new_account(Dirs, PrivateKey, Req0, Nonce0),
{<<"id">>, AccIdInt} = lists:keyfind(<<"id">>, 1, Account),
AccId = integer_to_list(AccIdInt),
Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}],
{ok, _Account2, _Nonce2} =
ejabberd_acme_comm:update_account({CAUrl, AccId}, PrivateKey, Req1, Nonce1),
{ok, AccId}
catch
E:R ->
?ERROR_MSG("Error: ~p creating an account for contact: ~p",
[{E,R}, Contact]),
throw({error,create_new_account})
end.
-spec create_new_authorization(url(), bitstring(), jose_jwk:key()) ->
{'ok', proplist()} | no_return().
create_new_authorization(CAUrl, DomainName, PrivateKey) ->
try
{ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl),
Req0 = [{<<"identifier">>,
{[{<<"type">>, <<"dns">>},
{<<"value">>, DomainName}]}},
{<<"existing">>, <<"accept">>}],
{ok, {AuthzUrl, Authz}, Nonce1} =
ejabberd_acme_comm:new_authz(Dirs, PrivateKey, Req0, Nonce0),
{ok, AuthzId} = location_to_id(AuthzUrl),
Challenges = get_challenges(Authz),
{ok, ChallengeUrl, KeyAuthz} =
acme_challenge:solve_challenge(<<"http-01">>, Challenges, 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),
{ok, AuthzValid, _Nonce} = ejabberd_acme_comm:get_authz_until_valid({CAUrl, AuthzId}),
{ok, AuthzValid}
catch
E:R ->
?ERROR_MSG("Error: ~p getting an authorization for domain: ~p~n",
[{E,R}, DomainName]),
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),
CSRSubject = [{commonName, bitstring_to_list(DomainName)}],
{CSR, CSRKey} = make_csr(CSRSubject),
{NotBefore, NotAfter} = not_before_not_after(),
Req =
[{<<"csr">>, CSR},
{<<"notBefore">>, NotBefore},
{<<"NotAfter">>, NotAfter}
],
{ok, {_CertUrl, Certificate}, _Nonce1} =
ejabberd_acme_comm:new_cert(Dirs, PrivateKey, Req, Nonce0),
DecodedCert = public_key:pkix_decode_cert(list_to_binary(Certificate), plain),
PemEntryCert = public_key:pem_entry_encode('Certificate', DecodedCert),
{_, CSRKeyKey} = jose_jwk:to_key(CSRKey),
PemEntryKey = public_key:pem_entry_encode('ECPrivateKey', CSRKeyKey),
PemCertKey = public_key:pem_encode([PemEntryKey, PemEntryCert]),
{ok, DomainName, PemCertKey}
catch
E:R ->
?ERROR_MSG("Error: ~p getting an authorization for domain: ~p~n",
[{E,R}, DomainName]),
throw({error, DomainName, certificate})
end.
-spec ensure_account_exists() -> {ok, string(), jose_jwk:key()}.
ensure_account_exists() ->
case read_account_persistent() of
none ->
?ERROR_MSG("No existing account", []),
throw({error, no_old_account});
{ok, AccId, PrivateKey} ->
{ok, AccId, PrivateKey}
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(),
%% Find all hosts that we have certificates for
Certs = read_certificates_persistent(),
%% Get a certificate for each host
PemCertKeys = [renew_certificate(CAUrl, Cert, PrivateKey) || Cert <- Certs],
%% Save Certificates
SavedCerts = [save_renewed_certificate(Cert) || Cert <- PemCertKeys],
%% Format the result to send back to ejabberdctl
format_get_certificates_result(SavedCerts).
-spec renew_certificate(url(), data_cert(), jose_jwk:key()) ->
{'ok', bitstring(), _} |
{'error', bitstring(), _}.
renew_certificate(CAUrl, {DomainName, _} = Cert, PrivateKey) ->
case cert_to_expire(Cert) of
true ->
get_certificate(CAUrl, DomainName, PrivateKey);
false ->
{ok, DomainName, no_expire}
end.
-spec cert_to_expire(data_cert()) -> boolean().
cert_to_expire({DomainName, #data_cert{pem = Pem}}) ->
Certificate = pem_to_certificate(Pem),
Validity = get_utc_validity(Certificate),
close_to_expire(Validity).
-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
%%
-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(),
[format_certificate(DataCert, Verbose) || {_Key, DataCert} <- Certs].
%% TODO: Make this cleaner and more robust
-spec format_certificate(data_cert(), verbose_opt()) -> string().
format_certificate(DataCert, Verbose) ->
#data_cert{
domain = DomainName,
pem = PemCert,
path = Path
} = DataCert,
try
Certificate = pem_to_certificate(PemCert),
%% Find the commonName
_CommonName = get_commonName(Certificate),
%% Find the notAfter date
NotAfter = get_notAfter(Certificate),
case Verbose of
"plain" ->
format_certificate_plain(DomainName, NotAfter, Path);
"verbose" ->
format_certificate_verbose(DomainName, NotAfter, PemCert)
end
catch
E:R ->
?ERROR_MSG("Unknown ~p:~p, ~p", [E, R, erlang:get_stacktrace()]),
fail_format_certificate(DomainName)
end.
-spec format_certificate_plain(bitstring(), string(), string()) -> string().
format_certificate_plain(DomainName, NotAfter, Path) ->
Result = lists:flatten(io_lib:format(
" Domain: ~s~n"
" Valid until: ~s UTC~n"
" Path: ~s",
[DomainName, NotAfter, Path])),
Result.
-spec format_certificate_verbose(bitstring(), string(), bitstring()) -> string().
format_certificate_verbose(DomainName, NotAfter, PemCert) ->
Result = lists:flatten(io_lib:format(
" Domain: ~s~n"
" Valid until: ~s UTC~n"
" Certificate In PEM format: ~n~s",
[DomainName, NotAfter, PemCert])),
Result.
-spec fail_format_certificate(bitstring()) -> string().
fail_format_certificate(DomainName) ->
Result = lists:flatten(io_lib:format(
" Domain: ~s~n"
" Failed to format Certificate",
[DomainName])),
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) ->
UtcTime = get_utc_validity(Certificate),
%% TODO: Find a library function to decode utc time
[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.
-spec get_utc_validity(#'Certificate'{}) -> string().
get_utc_validity(#'Certificate'{tbsCertificate = TbsCertificate}) ->
#'TBSCertificate'{
validity = Validity
} = TbsCertificate,
#'Validity'{notAfter = {utcTime, UtcTime}} = Validity,
UtcTime.
%%
%% 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} ->
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}) ->
{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().
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),
Key = find_private_key_in_pem(PemEncodedCert),
{Base64Cert, Key}.
-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
%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% 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) ->
Key = generate_key(),
{_, KeyKey} = jose_jwk:to_key(Key),
KeyPub = to_public(Key),
try
SubPKInfoAlgo = subject_pk_info_algo(KeyPub),
{ok, RawBinPubKey} = raw_binary_public_key(KeyPub),
SubPKInfo = subject_pk_info(SubPKInfoAlgo, RawBinPubKey),
{ok, Subject} = attributes_from_list(Attributes),
CRI = certificate_request_info(SubPKInfo, Subject),
{ok, EncodedCRI} = der_encode(
'CertificationRequestInfo',
CRI),
SignedCRI = public_key:sign(EncodedCRI, 'sha256', KeyKey),
SignatureAlgo = signature_algo(Key, 'sha256'),
CSR = certification_request(CRI, SignatureAlgo, SignedCRI),
{ok, DerCSR} = der_encode(
'CertificationRequest',
CSR),
Result = base64url:encode(DerCSR),
{Result, Key}
catch
_:{badmatch, {error, bad_public_key}} ->
{error, bad_public_key};
_:{badmatch, {error, bad_attributes}} ->
{error, bad_public_key};
_:{badmatch, {error, der_encode}} ->
{error, der_encode}
end.
subject_pk_info_algo(_KeyPub) ->
#'SubjectPublicKeyInfoAlgorithm'{
algorithm = ?'id-ecPublicKey',
parameters = {asn1_OPENTYPE,<<6,8,42,134,72,206,61,3,1,7>>}
}.
subject_pk_info(Algo, RawBinPubKey) ->
#'SubjectPublicKeyInfo-PKCS-10'{
algorithm = Algo,
subjectPublicKey = RawBinPubKey
}.
certificate_request_info(SubPKInfo, Subject) ->
#'CertificationRequestInfo'{
version = 0,
subject = Subject,
subjectPKInfo = SubPKInfo,
attributes = []
}.
signature_algo(_Key, _Hash) ->
#'CertificationRequest_signatureAlgorithm'{
algorithm = ?'ecdsa-with-SHA256',
parameters = asn1_NOVALUE
}.
certification_request(CRI, SignatureAlgo, SignedCRI) ->
#'CertificationRequest'{
certificationRequestInfo = CRI,
signatureAlgorithm = SignatureAlgo,
signature = SignedCRI
}.
raw_binary_public_key(KeyPub) ->
try
{_, RawPubKey} = jose_jwk:to_key(KeyPub),
{{_, RawBinPubKey}, _} = RawPubKey,
{ok, RawBinPubKey}
catch
_:_ ->
?ERROR_MSG("Bad public key: ~p~n", [KeyPub]),
{error, bad_public_key}
end.
der_encode(Type, Term) ->
try
{ok, public_key:der_encode(Type, Term)}
catch
_:_ ->
?ERROR_MSG("Cannot DER encode: ~p, with asn1type: ~p", [Term, Type]),
{error, der_encode}
end.
%%
%% Attributes Parser
%%
attributes_from_list(Attrs) ->
ParsedAttrs = [attribute_parser_fun(Attr) || Attr <- Attrs],
case lists:any(fun is_error/1, ParsedAttrs) of
true ->
{error, bad_attributes};
false ->
{ok, {rdnSequence, [[PAttr] || PAttr <- ParsedAttrs]}}
end.
attribute_parser_fun({AttrName, AttrVal}) ->
try
#'AttributeTypeAndValue'{
type = attribute_oid(AttrName),
%% TODO: Check if every attribute should be encoded as
%% common name. Actually it doesn't matter in
%% practice. Only in theory in order to have cleaner code.
value = public_key:der_encode('X520CommonName', {printableString, AttrVal})
%% value = length_bitstring(list_to_bitstring(AttrVal))
}
catch
_:_ ->
?ERROR_MSG("Bad attribute: ~p~n", [{AttrName, AttrVal}]),
{error, bad_attributes}
end.
-spec attribute_oid(atom()) -> tuple() | no_return().
attribute_oid(commonName) -> ?'id-at-commonName';
attribute_oid(countryName) -> ?'id-at-countryName';
attribute_oid(stateOrProvinceName) -> ?'id-at-stateOrProvinceName';
attribute_oid(localityName) -> ?'id-at-localityName';
attribute_oid(organizationName) -> ?'id-at-organizationName';
attribute_oid(_) -> error(bad_attributes).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
%% Useful funs
%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec location_to_id(url()) -> {ok, string()} | {error, not_found}.
location_to_id(Url0) ->
Url = string:strip(Url0, right, $/),
case string:rchr(Url, $/) of
0 ->
?ERROR_MSG("Couldn't find id in url: ~p~n", [Url]),
{error, not_found};
Ind ->
{ok, string:sub_string(Url, Ind+1)}
end.
-spec get_challenges(proplist()) -> [{proplist()}].
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 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
NotAfter = xmpp_util:encode_timestamp({MegS+7, Sec+776000, 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
%% #'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 find_private_key_in_pem(pem()) -> {ok, jose_jwk:key()} | false.
find_private_key_in_pem(Pem) ->
PemList = public_key:pem_decode(Pem),
case find_private_key_in_pem1(private_key_types(), PemList) of
false ->
false;
PemKey ->
Key = public_key:pem_entry_decode(PemKey),
JoseKey = jose_jwk:from_key(Key),
JoseKey
end.
-spec find_private_key_in_pem1([public_key:pki_asn1_type()],
[public_key:pem_entry()]) ->
public_key:pem_entry() | false.
find_private_key_in_pem1([], _PemList) ->
false;
find_private_key_in_pem1([Type|Types], PemList) ->
case lists:keyfind(Type, 1, PemList) of
false ->
find_private_key_in_pem1(Types, PemList);
Key ->
Key
end.
-spec parse_domain_string(string()) -> [string()].
parse_domain_string(DomainString) ->
string:tokens(DomainString, ";").
-spec private_key_types() -> [public_key:pki_asn1_type()].
private_key_types() ->
['RSAPrivateKey',
'DSAPrivateKey',
'ECPrivateKey'].
-spec is_error(_) -> boolean().
is_error({error, _}) -> true;
is_error({error, _, _}) -> true;
is_error(_) -> false.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
%% Handle the persistent data structure
%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec data_empty() -> [].
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}} ->
{ok, AccId, PrivateKey};
false ->
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).
%%
%% Certificates
%%
-spec data_get_certificates(acme_data()) -> data_certs().
data_get_certificates(Data) ->
case lists:keyfind(certs, 1, Data) of
{certs, Certs} ->
Certs;
false ->
[]
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}) ->
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).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
%% Handle Config and Persistence Files
%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec persistent_file() -> file:filename().
persistent_file() ->
MnesiaDir = mnesia:system_info(directory),
filename:join(MnesiaDir, "acme.DAT").
%% 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} ->
{ok, binary_to_term(Binary)};
{error, enoent} ->
create_persistent(),
{ok, data_empty()};
{error, Reason} ->
?ERROR_MSG("Error: ~p reading acme data file", [Reason]),
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
ok -> ok;
{error, Reason} ->
?ERROR_MSG("Error: ~p writing acme data file", [Reason]),
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
ok ->
case file:change_mode(persistent_file(), persistent_file_mode()) of
ok -> ok;
{error, Reason} ->
?ERROR_MSG("Error: ~p changing acme data file mode", [Reason]),
throw({error, Reason})
end;
{error, Reason} ->
?ERROR_MSG("Error: ~p creating acme data file", [Reason]),
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} | {error, bitstring(), _}.
save_certificate({error, _, _} = Error) ->
Error;
save_certificate({ok, DomainName, Cert}) ->
try
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
%% that there is no certificate saved if it cannot be added in
%% certificate persistent storage
write_cert(CertificateFile, Cert, DomainName),
DataCert = #data_cert{
domain = DomainName,
pem = Cert,
path = CertificateFile
},
add_certificate_persistent(DataCert),
{ok, DomainName, saved}
catch
throw:Throw ->
Throw;
E:R ->
?ERROR_MSG("Unknown ~p:~p, ~p", [E, R, erlang:get_stacktrace()]),
{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, _, no_expire} = 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
ok ->
{ok, DomainName, saved};
{error, Reason} ->
?ERROR_MSG("Error: ~p saving certificate at file: ~p",
[Reason, CertificateFile]),
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 ->
Acme
end.
-spec get_config_contact() -> bitstring().
get_config_contact() ->
Acme = get_config_acme(),
case lists:keyfind(contact, 1, Acme) of
{contact, 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 ->
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 ->
CertDir
end.
-ifdef(GENERATE_RSA_KEY).
generate_key() ->
?INFO_MSG("Generate RSA key pair~n", []),
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}).
-else.
generate_key() ->
?INFO_MSG("Generate EC key pair~n", []),
jose_jwk:generate_key({ec, secp256r1}).
-endif.