]> granicus.if.org Git - ejabberd/commitdiff
Introduce 'certfiles' global option
authorEvgeniy Khramtsov <ekhramtsov@process-one.net>
Tue, 31 Oct 2017 21:20:27 +0000 (00:20 +0300)
committerEvgeniy Khramtsov <ekhramtsov@process-one.net>
Tue, 31 Oct 2017 21:20:27 +0000 (00:20 +0300)
The option is supposed to replace existing options 'c2s_certfile',
's2s_certfile' and 'domain_certfile'. The option accepts a list
of file paths (optionally with wildcards "*") containing either
PEM certificates or PEM private keys. At startup, ejabberd sorts
the certificates, finds matching private keys and rebuilds full
certificates chains which can be used by fast_tls. Example:

certfiles:
  - "/etc/letsencrypt/live/example.org/*.pem"
  - "/etc/letsencrypt/live/example.com/*.pem"

ejabberd.yml.example
rebar.config
src/ejabberd_c2s.erl
src/ejabberd_config.erl
src/ejabberd_pkix.erl
src/ejabberd_s2s.erl

index fd8b745e69833caf2557a90a48664775a4462f16..ffc6a26c711720024cf16c171308c6d43587534d 100644 (file)
@@ -108,7 +108,6 @@ hosts:
 
 ## Define common macros used by listeners
 ## define_macro:
-##   'CERTFILE': "/path/to/xmpp.pem"
 ##   'CIPHERS': "ECDH:DH:!3DES:!aNULL:!eNULL:!MEDIUM@STRENGTH"
 ##   'TLSOPTS':
 ##     - "no_sslv2"
@@ -130,11 +129,9 @@ listen:
     module: ejabberd_c2s
     ##
     ## If TLS is compiled in and you installed a SSL
-    ## certificate, specify the full path to the
-    ## file and uncomment these lines:
+    ## certificate, uncomment these lines:
     ##
     ## starttls: true
-    ## certfile: 'CERTFILE'
     ## protocol_options: 'TLSOPTS'
     ## dhfile: 'DHFILE'
     ## ciphers: 'CIPHERS'
@@ -219,7 +216,7 @@ listen:
   ##   request_handlers:
   ##     "": mod_http_upload
   ##   tls: true
-  ##   certfile: 'CERTFILE'
+  ##   certfile: "/path/to/xmpp.pem"
   ##   protocol_options: 'TLSOPTS'
   ##   dhfile: 'DHFILE'
   ##   ciphers: 'CIPHERS'
@@ -228,34 +225,31 @@ listen:
 ## password storage (see auth_password_format option).
 ## disable_sasl_mechanisms: "digest-md5"
 
+###.  ============
+###'  Certificates
+
+## List all available PEM files containing certificates for your domains,
+## chains of certificates or certificate keys. Full chains will be built
+## automatically by ejabberd.
+##
+## certfiles:
+##   - "/etc/letsencrypt/live/example.org/*.pem"
+##   - "/etc/letsencrypt/live/example.com/*.pem"
+
 ###.  ==================
 ###'  S2S GLOBAL OPTIONS
 
 ##
 ## s2s_use_starttls: Enable STARTTLS for S2S connections.
 ## Allowed values are: false, optional or required
-## You must specify a certificate file.
+## You must specify 'certfiles' option
 ##
 ## s2s_use_starttls: required
 
-##
-## s2s_certfile: Specify a certificate file.
-##
-## s2s_certfile: 'CERTFILE'
-
 ## Custom OpenSSL options
 ##
 ## s2s_protocol_options: 'TLSOPTS'
 
-##
-## domain_certfile: Specify a different certificate for each served hostname.
-##
-## host_config:
-##   "example.org":
-##     domain_certfile: "/path/to/example_org.pem"
-##   "example.com":
-##     domain_certfile: "/path/to/example_com.pem"
-
 ##
 ## S2S whitelist or blacklist
 ##
index 1932be6cd244230d90ea4855400e366ba972b43f..ce2806e33e11dc5742da75b6f26d114379e554d8 100644 (file)
@@ -30,6 +30,7 @@
         {jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "0.14.8"}}},
         {p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.2"}}},
         {luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "v0.2"}}},
+       {fs, ".*", {git, "https://github.com/synrc/fs.git", {tag, "2.12.0"}}},
        {if_var_true, stun, {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.15"}}}},
        {if_var_true, sip, {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.16"}}}},
         {if_var_true, mysql, {p1_mysql, ".*", {git, "https://github.com/processone/p1_mysql",
index c5af2e031bb3f0e66de1bdeefac015b4585a3176..d8b89f6a23fe17794de3780f89487978418e1e33 100644 (file)
@@ -302,10 +302,7 @@ tls_options(#{lserver := LServer, tls_options := DefaultOpts,
     TLSOpts1 = case {Encrypted, proplists:get_value(certfile, DefaultOpts)} of
                   {true, CertFile} when CertFile /= undefined -> DefaultOpts;
                   {_, _} ->
-                      case ejabberd_config:get_option(
-                             {domain_certfile, LServer},
-                             ejabberd_config:get_option(
-                               {c2s_certfile, LServer})) of
+                      case get_certfile(LServer) of
                           undefined -> DefaultOpts;
                           CertFile -> lists:keystore(certfile, 1, DefaultOpts,
                                                      {certfile, CertFile})
@@ -928,6 +925,17 @@ format_reason(_, {shutdown, _}) ->
 format_reason(_, _) ->
     <<"internal server error">>.
 
+-spec get_certfile(binary()) -> file:filename_all().
+get_certfile(LServer) ->
+    case ejabberd_pkix:get_certfile(LServer) of
+       {ok, CertFile} ->
+           CertFile;
+       error ->
+           ejabberd_config:get_option(
+             {domain_certfile, LServer},
+             ejabberd_config:get_option({c2s_certfile, LServer}))
+    end.
+
 transform_listen_option(Opt, Opts) ->
     [Opt|Opts].
 
@@ -941,7 +949,11 @@ transform_listen_option(Opt, Opts) ->
              (resource_conflict) -> fun((resource_conflict()) -> resource_conflict());
              (disable_sasl_mechanisms) -> fun((binary() | [binary()]) -> [binary()]);
              (atom()) -> [atom()].
-opt_type(c2s_certfile) -> fun misc:try_read_file/1;
+opt_type(c2s_certfile = Opt) ->
+    fun(File) ->
+           ?WARNING_MSG("option '~s' is deprecated, use 'certfiles' instead", [Opt]),
+           misc:try_read_file(File)
+    end;
 opt_type(c2s_ciphers) -> fun iolist_to_binary/1;
 opt_type(c2s_dhfile) -> fun misc:try_read_file/1;
 opt_type(c2s_cafile) -> fun misc:try_read_file/1;
index 5d3bc8680c15a8630bbf95b86d58fba73592ab2d..4b7c15806950e8633560009cd0c6e31bb8302a68 100644 (file)
@@ -1417,8 +1417,11 @@ opt_type(cache_life_time) ->
        (infinity) -> infinity;
        (unlimited) -> infinity
     end;
-opt_type(domain_certfile) ->
-    fun misc:try_read_file/1;
+opt_type(domain_certfile = Opt) ->
+    fun(File) ->
+           ?WARNING_MSG("option '~s' is deprecated, use 'certfiles' instead", [Opt]),
+           misc:try_read_file(File)
+    end;
 opt_type(shared_key) ->
     fun iolist_to_binary/1;
 opt_type(node_start) ->
index f9f0472f6463da63c0861668390fcce1a808d72d..949424f20c61409baf0a1b5a8b39c5c2ea0eee6d 100644 (file)
@@ -27,7 +27,8 @@
 
 %% API
 -export([start_link/0, add_certfile/1, format_error/1, opt_type/1,
-        get_certfile/1, try_certfile/1, route_registered/1]).
+        get_certfile/1, try_certfile/1, route_registered/1,
+        config_reloaded/0]).
 %% gen_server callbacks
 -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
         terminate/2, code_change/3]).
 -include("jid.hrl").
 
 -record(state, {validate = true :: boolean(),
-               certs = #{}}).
--record(cert_state, {domains = [] :: [binary()]}).
+               paths = [] :: [file:filename()],
+               certs = #{} :: map(),
+               keys = [] :: [public_key:private_key()]}).
 
+-type state() :: #state{}.
 -type cert() :: #'OTPCertificate'{}.
 -type priv_key() :: public_key:private_key().
 -type pub_key() :: #'RSAPublicKey'{} | {integer(), #'Dss-Parms'{}} | #'ECPoint'{}.
@@ -62,8 +65,8 @@ add_certfile(Path) ->
 -spec try_certfile(filename:filename()) -> binary().
 try_certfile(Path0) ->
     Path = prep_path(Path0),
-    case mk_cert_state(Path, false) of
-       {ok, _} -> Path;
+    case load_certfile(Path) of
+       {ok, _, _} -> Path;
        {error, _} -> erlang:error(badarg)
     end.
 
@@ -78,14 +81,14 @@ format_error(not_pem) ->
 format_error(not_der) ->
     "failed to decode from DER format";
 format_error(encrypted) ->
-    "encrypted certificate found in the chain";
+    "encrypted certificate";
 format_error({bad_cert, cert_expired}) ->
     "certificate is no longer valid as its expiration date has passed";
 format_error({bad_cert, invalid_issuer}) ->
     "certificate issuer name does not match the name of the "
-       "issuer certificate in the chain";
+       "issuer certificate";
 format_error({bad_cert, invalid_signature}) ->
-    "certificate was not signed by its issuer certificate in the chain";
+    "certificate was not signed by its issuer certificate";
 format_error({bad_cert, name_not_permitted}) ->
     "invalid Subject Alternative Name extension";
 format_error({bad_cert, missing_basic_constraint}) ->
@@ -95,7 +98,7 @@ format_error({bad_cert, invalid_key_usage}) ->
     "certificate key is used in an invalid way according "
        "to the key-usage extension";
 format_error({bad_cert, selfsigned_peer}) ->
-    "self-signed certificate in the chain";
+    "self-signed certificate";
 format_error({bad_cert, unknown_sig_algo}) ->
     "certificate is signed using unknown algorithm";
 format_error({bad_cert, unknown_ca}) ->
@@ -139,18 +142,29 @@ get_certfile(Domain) ->
 start_link() ->
     gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
 
+config_reloaded() ->
+    gen_server:cast(?MODULE, config_reloaded).
+
 opt_type(ca_path) ->
     fun(Path) -> iolist_to_binary(Path) end;
+opt_type(certfiles) ->
+    fun(CertList) ->
+           [binary_to_list(Path) || Path <- CertList]
+    end;
 opt_type(_) ->
-    [ca_path].
+    [ca_path, certfiles].
 
 %%%===================================================================
 %%% gen_server callbacks
 %%%===================================================================
 init([]) ->
+    application:load(fs),
+    application:set_env(fs, backwards_compatible, false),
+    ejabberd:start_app(fs),
     process_flag(trap_exit, true),
     ets:new(?MODULE, [named_table, public, bag]),
     ejabberd_hooks:add(route_registered, ?MODULE, route_registered, 50),
+    ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 30),
     Validate = case os:type() of
                   {win32, _} -> false;
                   _ ->
@@ -162,34 +176,74 @@ init([]) ->
        true -> ok
     end,
     State = #state{validate = Validate},
-    {ok, add_certfiles(State)}.
+    case filelib:ensure_dir(filename:join(certs_dir(), "foo")) of
+       ok ->
+           clean_dir(certs_dir()),
+           case add_certfiles(State) of
+               {ok, State1} ->
+                   {ok, State1};
+               {error, Why} ->
+                   {stop, Why}
+           end;
+       {error, Why} ->
+           ?CRITICAL_MSG("Failed to create directory ~s: ~s",
+                         [certs_dir(), file:format_error(Why)]),
+           {stop, Why}
+    end.
 
 handle_call({add_certfile, Path}, _, State) ->
     {Result, NewState} = add_certfile(Path, State),
     {reply, Result, NewState};
 handle_call({route_registered, Host}, _, State) ->
-    NewState = add_certfiles(Host, State),
-    case get_certfile(Host) of
-       {ok, _} -> ok;
-       error ->
-           ?WARNING_MSG("No certificate found matching '~s': strictly "
-                        "configured clients or servers will reject "
-                        "connections with this host", [Host])
-    end,
-    {reply, ok, NewState};
+    case add_certfiles(Host, State) of
+       {ok, NewState} ->
+           case get_certfile(Host) of
+               {ok, _} -> ok;
+               error ->
+                   ?WARNING_MSG("No certificate found matching '~s': strictly "
+                                "configured clients or servers will reject "
+                                "connections with this host", [Host])
+           end,
+           {reply, ok, NewState};
+       {error, _} ->
+           {reply, ok, State}
+    end;
 handle_call(_Request, _From, State) ->
     Reply = ok,
     {reply, Reply, State}.
 
+handle_cast(config_reloaded, State) ->
+    State1 = State#state{paths = [], certs = #{}, keys = []},
+    case add_certfiles(State1) of
+       {ok, State2} ->
+           {noreply, State2};
+       {error, _} ->
+           {noreply, State}
+    end;
 handle_cast(_Msg, State) ->
     {noreply, State}.
 
+handle_info({_, {fs, file_event}, {File, Events}}, State) ->
+    ?DEBUG("got FS events for ~s: ~p", [File, Events]),
+    Path = iolist_to_binary(File),
+    case lists:member(modified, Events) of
+       true ->
+           case lists:member(Path, State#state.paths) of
+               true ->
+                   handle_cast(config_reloaded, State);
+               false ->
+                   {noreply, State}
+           end;
+       false ->
+           {noreply, State}
+    end;
 handle_info(_Info, State) ->
     ?WARNING_MSG("unexpected info: ~p", [_Info]),
     {noreply, State}.
 
 terminate(_Reason, _State) ->
-    ejabberd_hooks:delete(route_registered, ?MODULE, route_registered, 50).
+    ejabberd_hooks:delete(route_registered, ?MODULE, route_registered, 50),
+    ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 30).
 
 code_change(_OldVsn, State, _Extra) ->
     {ok, State}.
@@ -197,73 +251,150 @@ code_change(_OldVsn, State, _Extra) ->
 %%%===================================================================
 %%% Internal functions
 %%%===================================================================
+-spec certfiles_from_config_options() -> [atom()].
+certfiles_from_config_options() ->
+    [c2s_certfile, s2s_certfile, domain_certfile].
+
+-spec get_certfiles_from_config_options(state()) -> [binary()].
+get_certfiles_from_config_options(State) ->
+    Global = case ejabberd_config:get_option(certfiles) of
+                undefined ->
+                    [];
+                Paths ->
+                    lists:flatmap(fun filelib:wildcard/1, Paths)
+            end,
+    Local = lists:flatmap(
+             fun(OptHost) ->
+                     case ejabberd_config:get_option(OptHost) of
+                         undefined -> [];
+                         Path -> [Path]
+                     end
+             end, [{Opt, Host}
+                   || Opt <- certfiles_from_config_options(),
+                      Host <- ejabberd_config:get_myhosts()]),
+    [iolist_to_binary(P) || P <- lists:usort(Local ++ Global)].
+
+-spec add_certfiles(state()) -> {ok, state()} | {error, bad_cert()}.
 add_certfiles(State) ->
-    lists:foldl(
-      fun(Host, AccState) ->
-             add_certfiles(Host, AccState)
-      end, State, ejabberd_config:get_myhosts()).
+    Paths = get_certfiles_from_config_options(State),
+    State1 = lists:foldl(
+              fun(Path, Acc) ->
+                      {_, NewAcc} = add_certfile(Path, Acc),
+                      NewAcc
+              end, State, Paths),
+    case build_chain_and_check(State1) of
+       ok -> {ok, State1};
+       {error, _} = Err -> Err
+    end.
 
+-spec add_certfiles(binary(), state()) -> {ok, state()} | {error, bad_cert()}.
 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]).
+    State1 = 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, certfiles_from_config_options()),
+    if State /= State1 ->
+           case build_chain_and_check(State1) of
+               ok -> {ok, State1};
+               {error, _} = Err -> Err
+           end;
+       true ->
+           {ok, State}
+    end.
 
+-spec add_certfile(file:filename_all(), state()) -> {ok, state()} |
+                                                   {{error, cert_error()}, state()}.
 add_certfile(Path, State) ->
-    case maps:get(Path, State#state.certs, undefined) of
-       #cert_state{} ->
+    case lists:member(Path, State#state.paths) of
+       true ->
            {ok, State};
-       undefined ->
-           case mk_cert_state(Path, State#state.validate) of
-               {error, Reason} ->
-                   {{error, Reason}, State};
-               {ok, CertState} ->
-                   NewCerts = maps:put(Path, CertState, State#state.certs),
-                   lists:foreach(
-                     fun(Domain) ->
-                             ets:insert(?MODULE, {Domain, Path})
-                     end, CertState#cert_state.domains),
-                   {ok, State#state{certs = NewCerts}}
+       false ->
+           case load_certfile(Path) of
+               {ok, Certs, Keys} ->
+                   NewCerts = lists:foldl(
+                                fun(Cert, Acc) ->
+                                        maps:put(Cert, Path, Acc)
+                                end, State#state.certs, Certs),
+                   {ok, State#state{paths = [Path|State#state.paths],
+                                    certs = NewCerts,
+                                    keys = Keys ++ State#state.keys}};
+               {error, Why} = Err ->
+                   ?ERROR_MSG("failed to read certificate from ~s: ~s",
+                              [Path, format_error(Why)]),
+                   {Err, State}
            end
     end.
 
-mk_cert_state(Path, Validate) ->
-    case check_certfile(Path, Validate) of
-       {ok, Ds} ->
-           {ok, #cert_state{domains = Ds}};
-       {invalid, Ds, {bad_cert, _} = Why} ->
-           ?WARNING_MSG("certificate from ~s is invalid: ~s",
-                        [Path, format_error(Why)]),
-           {ok, #cert_state{domains = Ds}};
-       {error, Why} = Err ->
-           ?ERROR_MSG("failed to read certificate from ~s: ~s",
+-spec build_chain_and_check(state()) -> ok | {error, bad_cert()}.
+build_chain_and_check(State) ->
+    ?DEBUG("Rebuilding certificate chains from ~s",
+          [str:join(State#state.paths, <<", ">>)]),
+    CertPaths = get_cert_paths(maps:keys(State#state.certs)),
+    case match_cert_keys(CertPaths, State#state.keys) of
+       {ok, Chains} ->
+           CertFilesWithDomains = store_certs(Chains, []),
+           ets:delete_all_objects(?MODULE),
+           lists:foreach(
+             fun({Path, Domain}) ->
+                     ets:insert(?MODULE, {Domain, Path})
+             end, CertFilesWithDomains),
+           Errors = validate(CertPaths, State#state.validate),
+           subscribe(State),
+           lists:foreach(
+             fun({Cert, Why}) ->
+                     Path = maps:get(Cert, State#state.certs),
+                     ?ERROR_MSG("Failed to validate certificate from ~s: ~s",
+                                [Path, format_error(Why)])
+             end, Errors);
+       {error, Cert, Why} ->
+           Path = maps:get(Cert, State#state.certs),
+           ?ERROR_MSG("Failed to build certificate chain for ~s: ~s",
                       [Path, format_error(Why)]),
-           Err
+           {error, Why}
     end.
 
--spec check_certfile(filename:filename(), boolean())
-      -> {ok, [binary()]} | {invalid, [binary()], bad_cert()} |
-        {error, cert_error() | file:posix()}.
-check_certfile(Path, Validate) ->
+-spec store_certs([{[cert()], priv_key()}],
+                 [{binary(), binary()}]) -> [{binary(), binary()}].
+store_certs([{Certs, Key}|Chains], Acc) ->
+    CertPEMs = public_key:pem_encode(
+                lists:map(
+                  fun(Cert) ->
+                          Type = element(1, Cert),
+                          DER = public_key:pkix_encode(Type, Cert, otp),
+                          {'Certificate', DER, not_encrypted}
+                  end, Certs)),
+    KeyPEM = public_key:pem_encode(
+              [{element(1, Key),
+                public_key:der_encode(element(1, Key), Key),
+                not_encrypted}]),
+    PEMs = <<CertPEMs/binary, KeyPEM/binary>>,
+    Cert = hd(Certs),
+    Domains = xmpp_stream_pkix:get_cert_domains(Cert),
+    FileName = filename:join(certs_dir(), str:sha(PEMs)),
+    case file:write_file(FileName, PEMs) of
+       ok ->
+           file:change_mode(FileName, 8#600),
+           NewAcc = [{FileName, Domain} || Domain <- Domains] ++ Acc,
+           store_certs(Chains, NewAcc);
+       {error, Why} ->
+           ?ERROR_MSG("Failed to write to ~s: ~s",
+                      [FileName, file:format_error(Why)]),
+           store_certs(Chains, [])
+    end;
+store_certs([], Acc) ->
+    Acc.
+
+-spec load_certfile(file:filename_all()) -> {ok, [cert()], [priv_key()]} |
+                                           {error, cert_error() | file:posix()}.
+load_certfile(Path) ->
     try
        {ok, Data} = file:read_file(Path),
-       {ok, Certs, PrivKeys} = pem_decode(Data),
-       CertPaths = get_cert_paths(Certs),
-       Domains = get_domains(CertPaths),
-       case match_cert_keys(CertPaths, PrivKeys) of
-           {ok, _} ->
-               case validate(CertPaths, Validate) of
-                   ok -> {ok, Domains};
-                   {error, Why} -> {invalid, Domains, Why}
-               end;
-           {error, Why} ->
-               {invalid, Domains, Why}
-       end
+       pem_decode(Data)
     catch _:{badmatch, {error, _} = Err} ->
            Err
     end.
@@ -281,7 +412,7 @@ pem_decode(Data) ->
                           fun(#'OTPCertificate'{}) -> true;
                              (_) -> false
                           end, Objects) of
-                       {[], _} ->
+                       {[], []} ->
                            {error, not_cert};
                        {Certs, PrivKeys} ->
                            {ok, Certs, PrivKeys}
@@ -331,41 +462,44 @@ decode_certs(PemEntries) ->
            {error, not_der}
     end.
 
--spec validate([{path, [cert()]}], boolean()) -> ok | {error, bad_cert()}.
-validate([{path, Path}|Paths], true) ->
-    case validate_path(Path) of
-       ok ->
-           validate(Paths, true);
-       Err ->
-           Err
-    end;
+-spec validate([{path, [cert()]}], boolean()) -> [{cert(), bad_cert()}].
+validate(Paths, true) ->
+    lists:flatmap(
+      fun({path, Path}) ->
+             case validate_path(Path) of
+                 ok ->
+                     [];
+                 {error, Cert, Reason} ->
+                     [{Cert, Reason}]
+             end
+      end, Paths);
 validate(_, _) ->
     ok.
 
--spec validate_path([cert()]) -> ok | {error, bad_cert()}.
+-spec validate_path([cert()]) -> ok | {error, cert(), bad_cert()}.
 validate_path([Cert|_] = Certs) ->
     case find_local_issuer(Cert) of
        {ok, IssuerCert} ->
            try public_key:pkix_path_validation(IssuerCert, Certs, []) of
                {ok, _} ->
                    ok;
-               Err ->
-                   Err
+               {error, Reason} ->
+                   {error, Cert, Reason}
            catch error:function_clause ->
                    case erlang:get_stacktrace() of
                        [{public_key, pkix_sign_types, _, _}|_] ->
-                           {error, {bad_cert, unknown_sig_algo}};
+                           {error, Cert, {bad_cert, unknown_sig_algo}};
                        ST ->
                            %% Bug in public_key application
                            erlang:raise(error, function_clause, ST)
                    end
            end;
-       {error, _} = Err ->
+       {error, Reason} ->
            case public_key:pkix_is_self_signed(Cert) of
                true ->
-                   {error, {bad_cert, selfsigned_peer}};
+                   {error, Cert, {bad_cert, selfsigned_peer}};
                false ->
-                   Err
+                   {error, Cert, Reason}
            end
     end.
 
@@ -373,6 +507,25 @@ validate_path([Cert|_] = Certs) ->
 ca_dir() ->
     ejabberd_config:get_option(ca_path, "/etc/ssl/certs").
 
+-spec certs_dir() -> string().
+certs_dir() ->
+    MnesiaDir = mnesia:system_info(directory),
+    filename:join(MnesiaDir, "certs").
+
+-spec clean_dir(file:filename_all()) -> ok.
+clean_dir(Dir) ->
+    ?DEBUG("Cleaning directory ~s", [Dir]),
+    Files = filelib:wildcard(filename:join(Dir, "*")),
+    lists:foreach(
+      fun(Path) ->
+             case filelib:is_file(Path) of
+                 true ->
+                     file:delete(Path);
+                 false ->
+                     ok
+             end
+      end, Files).
+
 -spec check_ca_dir() -> ok.
 check_ca_dir() ->
     case filelib:wildcard(filename:join(ca_dir(), "*.0")) of
@@ -424,13 +577,13 @@ match_cert_keys(CertPaths, PrivKeys) ->
 
 -spec match_cert_keys([{path, [cert()]}], [{pub_key(), priv_key()}],
                      [{cert(), priv_key()}])
-      -> {ok, [{cert(), priv_key()}]} | {error, {bad_cert, missing_priv_key}}.
+      -> {ok, [{[cert()], priv_key()}]} | {error, cert(), {bad_cert, missing_priv_key}}.
 match_cert_keys([{path, Certs}|CertPaths], KeyPairs, Result) ->
     [Cert|_] = RevCerts = lists:reverse(Certs),
     PubKey = pubkey_from_cert(Cert),
     case lists:keyfind(PubKey, 1, KeyPairs) of
        false ->
-           {error, {bad_cert, missing_priv_key}};
+           {error, Cert, {bad_cert, missing_priv_key}};
        {_, PrivKey} ->
            match_cert_keys(CertPaths, KeyPairs, [{RevCerts, PrivKey}|Result])
     end;
@@ -465,15 +618,6 @@ pubkey_from_privkey(#'DSAPrivateKey'{p = P, q = Q, g = G, y = Y}) ->
 pubkey_from_privkey(#'ECPrivateKey'{publicKey = Key}) ->
     #'ECPoint'{point = Key}.
 
--spec get_domains([{path, [cert()]}]) -> [binary()].
-get_domains(CertPaths) ->
-    lists:usort(
-      lists:flatmap(
-       fun({path, Certs}) ->
-               Cert = lists:last(Certs),
-               xmpp_stream_pkix:get_cert_domains(Cert)
-       end, CertPaths)).
-
 -spec get_cert_paths([cert()]) -> [{path, [cert()]}].
 get_cert_paths(Certs) ->
     G = digraph:new([acyclic]),
@@ -533,3 +677,18 @@ short_name_hash(IssuerID) ->
 short_name_hash(_) ->
     "".
 -endif.
+
+-spec subscribe(state()) -> ok.
+subscribe(State) ->
+    lists:foreach(
+      fun(Path) ->
+             Dir = filename:dirname(Path),
+             Name = list_to_atom(integer_to_list(erlang:phash2(Dir))),
+             case fs:start_link(Name, Dir) of
+                 {ok, _} ->
+                     ?DEBUG("Subscribed to FS events from ~s", [Dir]),
+                     fs:subscribe(Name);
+                 {error, _} ->
+                     ok
+             end
+      end, State#state.paths).
index 7dd82b8047ce7e4bd5c01138d3ab8cf1880e7e55..0626d62fba60cf894c0e4b16b5fe8b5ac740a3c8 100644 (file)
@@ -198,13 +198,11 @@ dirty_get_connections() ->
 
 -spec tls_options(binary(), [proplists:property()]) -> [proplists:property()].
 tls_options(LServer, DefaultOpts) ->
-    TLSOpts1 = case ejabberd_config:get_option(
-                     {domain_certfile, LServer},
-                     ejabberd_config:get_option(
-                       {s2s_certfile, LServer})) of
+    TLSOpts1 = case get_certfile(LServer) of
                   undefined -> DefaultOpts;
-                  CertFile -> lists:keystore(certfile, 1, DefaultOpts,
-                                             {certfile, CertFile})
+                  CertFile ->
+                      lists:keystore(certfile, 1, DefaultOpts,
+                                     {certfile, CertFile})
               end,
     TLSOpts2 = case ejabberd_config:get_option(
                      {s2s_ciphers, LServer}) of
@@ -269,6 +267,17 @@ queue_type(LServer) ->
       {s2s_queue_type, LServer},
       ejabberd_config:default_queue_type(LServer)).
 
+-spec get_certfile(binary()) -> file:filename_all().
+get_certfile(LServer) ->
+    case ejabberd_pkix:get_certfile(LServer) of
+       {ok, CertFile} ->
+           CertFile;
+       error ->
+           ejabberd_config:get_option(
+             {domain_certfile, LServer},
+             ejabberd_config:get_option({s2s_certfile, LServer}))
+    end.
+
 %%====================================================================
 %% gen_server callbacks
 %%====================================================================
@@ -711,7 +720,11 @@ opt_type(route_subdomains) ->
     end;
 opt_type(s2s_access) ->
     fun acl:access_rules_validator/1;
-opt_type(s2s_certfile) -> fun misc:try_read_file/1;
+opt_type(s2s_certfile = Opt) ->
+    fun(File) ->
+           ?WARNING_MSG("option '~s' is deprecated, use 'certfiles' instead", [Opt]),
+           misc:try_read_file(File)
+    end;
 opt_type(s2s_ciphers) -> fun iolist_to_binary/1;
 opt_type(s2s_dhfile) -> fun misc:try_read_file/1;
 opt_type(s2s_cafile) -> fun misc:try_read_file/1;