diff options
-rw-r--r-- | ejabberd.yml.example | 4 | ||||
-rw-r--r-- | src/ejabberd_acme.erl | 154 | ||||
-rw-r--r-- | src/ejabberd_acme_comm.erl | 8 | ||||
-rw-r--r-- | src/ejabberd_admin.erl | 18 | ||||
-rw-r--r-- | src/ejabberd_pkix.erl | 27 |
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 |