From b9bbe19d4ce697fd500f5869609752eebd278cb9 Mon Sep 17 00:00:00 2001 From: Badlop Date: Sat, 11 Dec 2010 02:28:50 +0100 Subject: [PATCH] Option to reject S2S connection if untrusted certificate (EJAB-464) --- doc/guide.tex | 10 ++++---- src/ejabberd.cfg.example | 2 +- src/ejabberd_s2s_in.erl | 52 +++++++++++++++++++++++++++------------- src/ejabberd_s2s_out.erl | 2 +- src/tls/tls.erl | 51 ++++++++++++++++++++++++++++++++++++++- src/tls/tls_drv.c | 9 ++++--- 6 files changed, 99 insertions(+), 27 deletions(-) diff --git a/doc/guide.tex b/doc/guide.tex index bccbe586d..0ae3053e5 100644 --- a/doc/guide.tex +++ b/doc/guide.tex @@ -962,9 +962,11 @@ This is a detailed description of each option allowed by the listening modules: There are some additional global options that can be specified in the ejabberd configuration file (outside \term{listen}): \begin{description} - \titem{\{s2s\_use\_starttls, false|optional|required\}} + \titem{\{s2s\_use\_starttls, false|optional|required|required\_trusted\}} \ind{options!s2s\_use\_starttls}\ind{STARTTLS}This option defines if - s2s connections can optionally use STARTTLS encryption, or if it must be required. + s2s connections don't use STARTTLS encryption; if STARTTLS can be used optionally; + if STARTTLS is required to establish the connection; + or if STARTTLS is required and the remote certificate must be valid and trusted. The default value is to not use STARTTLS: \term{false}. \titem{\{s2s\_certfile, Path\}} \ind{options!s2s\_certificate}Full path to a file containing a SSL certificate. @@ -1070,7 +1072,7 @@ In this example, the following configuration defines that: on port 5223 (SSL, IP 192.168.0.1 and fdca:8ab6:a243:75ef::1) and denied for the user called `\term{bad}'. \item s2s connections are listened for on port 5269 (all IPv4 addresses) - with STARTTLS for secured traffic required. + with STARTTLS for secured traffic strictly required, and the certificates are verified. Incoming and outgoing connections of remote XMPP servers are denied, only two servers can connect: "jabber.example.org" and "example.com". \item Port 5280 is serving the Web Admin and the HTTP Polling service @@ -1151,7 +1153,7 @@ In this example, the following configuration defines that: {service_check_from, false}]} ] }. -{s2s_use_starttls, required}. +{s2s_use_starttls, required_trusted}. {s2s_certfile, "/path/to/ssl.pem"}. {s2s_default_policy, deny}. {{s2s_host,"jabber.example.org"}, allow}. diff --git a/src/ejabberd.cfg.example b/src/ejabberd.cfg.example index 45cdfb4d0..a95689b35 100644 --- a/src/ejabberd.cfg.example +++ b/src/ejabberd.cfg.example @@ -170,7 +170,7 @@ %% %% s2s_use_starttls: Enable STARTTLS + Dialback for S2S connections. -%% Allowed values are: false optional required +%% Allowed values are: false optional required required_trusted %% You must specify a certificate file. %% %%{s2s_use_starttls, optional}. diff --git a/src/ejabberd_s2s_in.erl b/src/ejabberd_s2s_in.erl index 7bc183aa7..eb8c05a6e 100644 --- a/src/ejabberd_s2s_in.erl +++ b/src/ejabberd_s2s_in.erl @@ -75,6 +75,7 @@ tls = false, tls_enabled = false, tls_required = false, + tls_certverify = false, tls_options = [], server, authenticated = false, @@ -152,13 +153,15 @@ init([{SockMod, Socket}, Opts]) -> {value, {_, S}} -> S; _ -> none end, - {StartTLS, TLSRequired} = case ejabberd_config:get_local_option(s2s_use_starttls) of + {StartTLS, TLSRequired, TLSCertverify} = case ejabberd_config:get_local_option(s2s_use_starttls) of UseTls when (UseTls==undefined) or (UseTls==false) -> - {false, false}; + {false, false, false}; UseTls when (UseTls==true) or (UseTls==optional) -> - {true, false}; + {true, false, false}; required -> - {true, true} + {true, true, false}; + required_trusted -> + {true, true, true} end, TLSOpts = case ejabberd_config:get_local_option(s2s_certfile) of undefined -> @@ -175,6 +178,7 @@ init([{SockMod, Socket}, Opts]) -> tls = StartTLS, tls_enabled = false, tls_required = TLSRequired, + tls_certverify = TLSCertverify, tls_options = TLSOpts, timer = Timer}}. @@ -198,16 +202,18 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> StateData#state.tls_enabled -> case (StateData#state.sockmod):get_peer_certificate( StateData#state.socket) of - {ok, _Cert} -> - case (StateData#state.sockmod):get_verify_result( - StateData#state.socket) of + {ok, Cert} -> + case (StateData#state.sockmod):get_verify_result(StateData#state.socket) of 0 -> [{xmlelement, "mechanisms", [{"xmlns", ?NS_SASL}], [{xmlelement, "mechanism", [], [{xmlcdata, "EXTERNAL"}]}]}]; - _ -> - [] + CertVerifyRes -> + case StateData#state.tls_certverify of + true -> {error_cert_verif, CertVerifyRes, Cert}; + false -> [] + end end; error -> [] @@ -225,14 +231,26 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> [{xmlelement, "required", [], []}] }] end, - send_element(StateData, - {xmlelement, "stream:features", [], - SASL ++ StartTLS ++ - ejabberd_hooks:run_fold( - s2s_stream_features, - Server, - [], [Server])}), - {next_state, wait_for_feature_request, StateData#state{server = Server}}; + case SASL of + {error_cert_verif, CertVerifyResult, Certificate} -> + CertError = tls:get_cert_verify_string(CertVerifyResult, Certificate), + RemoteServer = xml:get_attr_s("from", Attrs), + ?INFO_MSG("Closing s2s connection: ~s <--> ~s (~s)", [StateData#state.server, RemoteServer, CertError]), + send_text(StateData, xml:element_to_string(?SERRT_POLICY_VIOLATION("en", CertError))), + {atomic, Pid} = ejabberd_s2s:find_connection(jlib:make_jid("", Server, ""), jlib:make_jid("", RemoteServer, "")), + ejabberd_s2s_out:stop_connection(Pid), + + {stop, normal, StateData}; + _ -> + send_element(StateData, + {xmlelement, "stream:features", [], + SASL ++ StartTLS ++ + ejabberd_hooks:run_fold( + s2s_stream_features, + Server, + [], [Server])}), + {next_state, wait_for_feature_request, StateData#state{server = Server}} + end; {"jabber:server", _, Server, true} when StateData#state.authenticated -> send_text(StateData, ?STREAM_HEADER(" version='1.0'")), diff --git a/src/ejabberd_s2s_out.erl b/src/ejabberd_s2s_out.erl index 00e7fa1a9..f59e8ec8e 100644 --- a/src/ejabberd_s2s_out.erl +++ b/src/ejabberd_s2s_out.erl @@ -160,7 +160,7 @@ init([From, Server, Type]) -> {false, false}; UseTls when (UseTls==true) or (UseTls==optional) -> {true, false}; - required -> + UseTls when (UseTls==required) or (UseTls==required_trusted) -> {true, true} end, UseV10 = TLS, diff --git a/src/tls/tls.erl b/src/tls/tls.erl index 46f01b369..7c90569cd 100644 --- a/src/tls/tls.erl +++ b/src/tls/tls.erl @@ -39,6 +39,7 @@ close/1, get_peer_certificate/1, get_verify_result/1, + get_cert_verify_string/2, test/0]). %% Internal exports, call-back functions. @@ -63,8 +64,10 @@ -ifdef(SSL40). -define(CERT_DECODE, {public_key, pkix_decode_cert, plain}). +-define(CERT_SELFSIGNED, {public_key, pkix_is_self_signed}). -else. -define(CERT_DECODE, {ssl_pkix, decode_cert, [pkix]}). +-define(CERT_SELFSIGNED, {erlang, is_atom}). %% Dummy function for old OTPs -endif. @@ -243,7 +246,9 @@ get_peer_certificate(#tlssock{tlsport = Port}) -> <<0, BCert/binary>> -> {CertMod, CertFun, CertSecondArg} = ?CERT_DECODE, case catch apply(CertMod, CertFun, [BCert, CertSecondArg]) of - {ok, Cert} -> + {ok, Cert} -> %% returned by R13 and older + {ok, Cert}; + {'Certificate', _, _, _} = Cert -> {ok, Cert}; _ -> error @@ -311,3 +316,47 @@ loop(Port, Socket) -> end. +get_cert_verify_string(CertVerifyRes, Cert) -> + BCert = public_key:pkix_encode('Certificate', Cert, plain), + {CertMod, CertFun} = ?CERT_SELFSIGNED, + IsSelfsigned = apply(CertMod, CertFun, [BCert]), + case {CertVerifyRes, IsSelfsigned} of + {21, true} -> "self-signed certificate"; + _ -> cert_verify_code(CertVerifyRes) + end. + +%% http://www.openssl.org/docs/apps/verify.html +cert_verify_code(0) -> "ok"; +cert_verify_code(2) -> "unable to get issuer certificate"; +cert_verify_code(3) -> "unable to get certificate CRL"; +cert_verify_code(4) -> "unable to decrypt certificate's signature"; +cert_verify_code(5) -> "unable to decrypt CRL's signature"; +cert_verify_code(6) -> "unable to decode issuer public key"; +cert_verify_code(7) -> "certificate signature failure"; +cert_verify_code(8) -> "CRL signature failure"; +cert_verify_code(9) -> "certificate is not yet valid"; +cert_verify_code(10) -> "certificate has expired"; +cert_verify_code(11) -> "CRL is not yet valid"; +cert_verify_code(12) -> "CRL has expired"; +cert_verify_code(13) -> "format error in certificate's notBefore field"; +cert_verify_code(14) -> "format error in certificate's notAfter field"; +cert_verify_code(15) -> "format error in CRL's lastUpdate field"; +cert_verify_code(16) -> "format error in CRL's nextUpdate field"; +cert_verify_code(17) -> "out of memory"; +cert_verify_code(18) -> "self signed certificate"; +cert_verify_code(19) -> "self signed certificate in certificate chain"; +cert_verify_code(20) -> "unable to get local issuer certificate"; +cert_verify_code(21) -> "unable to verify the first certificate"; +cert_verify_code(22) -> "certificate chain too long"; +cert_verify_code(23) -> "certificate revoked"; +cert_verify_code(24) -> "invalid CA certificate"; +cert_verify_code(25) -> "path length constraint exceeded"; +cert_verify_code(26) -> "unsupported certificate purpose"; +cert_verify_code(27) -> "certificate not trusted"; +cert_verify_code(28) -> "certificate rejected"; +cert_verify_code(29) -> "subject issuer mismatch"; +cert_verify_code(30) -> "authority and subject key identifier mismatch"; +cert_verify_code(31) -> "authority and issuer serial number mismatch"; +cert_verify_code(32) -> "key usage does not include certificate signing"; +cert_verify_code(50) -> "application verification failure"; +cert_verify_code(X) -> "Unknown OpenSSL error code: " ++ integer_to_list(X). diff --git a/src/tls/tls_drv.c b/src/tls/tls_drv.c index fd4e7fff2..ae8707625 100644 --- a/src/tls/tls_drv.c +++ b/src/tls/tls_drv.c @@ -349,13 +349,16 @@ static int tls_drv_control(ErlDrvData handle, #ifdef SSL_MODE_RELEASE_BUFFERS SSL_CTX_set_mode(ctx, SSL_MODE_RELEASE_BUFFERS); #endif + /* SSL_CTX_load_verify_locations(ctx, "/etc/ejabberd/ca_certificates.pem", NULL); */ + /* SSL_CTX_load_verify_locations(ctx, NULL, "/etc/ejabberd/ca_certs/"); */ - if (command == SET_CERTIFICATE_FILE_ACCEPT) - { + /* This IF is commented to allow verification in all cases: */ + /* if (command == SET_CERTIFICATE_FILE_ACCEPT) */ + /* { */ SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER|SSL_VERIFY_CLIENT_ONCE, verify_callback); - } + /* } */ ssl_ctx = ctx; hash_table_insert(buf, mtime, ssl_ctx);