core (xmpp): server certificate validation:

XMPP server certificate is now checked, and connection is refused (by default) if it's not valid.
Certificate check can be disabled in the new parameter "Configuration/check_certificate".
If certificate checking is disabled, a warning note is sent on every new connection.
Twisted and Wokkel are temporarly monkey patched in sat.core.tls_patches module, until modifications are merged upstream.
This commit is contained in:
Goffi 2018-11-10 10:16:35 +01:00
commit 1b8345f4af
6 changed files with 131 additions and 28 deletions

View file

@ -1,7 +1,8 @@
All theses changelogs are not exhaustive, please check the Mercurial repository for more details.
v 0.7.0 (NOT RELEASED YET):
This version is a huge gap with previous one, changelog only show a part of novelties
This version is a huge gap with previous one, changelog only show a part of novelties.
This is also the first "general audience" version.
- XEP-0070 implementation (HTTP Auth via XMPP)
- XEP-0184 implementation (Delivery Receipts)
- XEP-0231 implementation (Bits of Binary)
@ -29,6 +30,7 @@ v 0.7.0 (NOT RELEASED YET):
- language detection plugin
- media remote control through MPRIS standard
- generic encryption methods handling in core
- server certificate validation
- jp:
- new debug commands, to monitor stream, call bridge method or send fake signals
- new info/session command, to get data on current session

View file

@ -121,3 +121,8 @@ class ParsingError(Exception):
# Something which need to be done is not available yet
class NotReady(Exception):
pass
class InvalidCertificate(Exception):
"""A TLS certificate is not valid"""
pass

63
sat/core/patches.py Normal file
View file

@ -0,0 +1,63 @@
from twisted.words.protocols.jabber import xmlstream
from twisted.internet import ssl
from wokkel import client
"""This module apply monkey patches to Twisted and Wokkel to handle certificate validation
during XMPP connection"""
class TLSInitiatingInitializer(xmlstream.TLSInitiatingInitializer):
check_certificate = True
def onProceed(self, obj):
self.xmlstream.removeObserver('/failure', self.onFailure)
trustRoot = ssl.platformTrust() if self.check_certificate else None
ctx = ssl.CertificateOptions(trustRoot=trustRoot)
self.xmlstream.transport.startTLS(ctx)
self.xmlstream.reset()
self.xmlstream.sendHeader()
self._deferred.callback(xmlstream.Reset)
class XMPPClient(client.XMPPClient):
def __init__(self, jid, password, host=None, port=5222, check_certificate=True):
self.jid = jid
self.domain = jid.host.encode('idna')
self.host = host
self.port = port
factory = HybridClientFactory(jid, password, check_certificate)
client.StreamManager.__init__(self, factory)
def HybridClientFactory(jid, password, check_certificate=True):
a = HybridAuthenticator(jid, password, check_certificate)
return xmlstream.XmlStreamFactory(a)
class HybridAuthenticator(client.HybridAuthenticator):
def __init__(self, jid, password, check_certificate):
xmlstream.ConnectAuthenticator.__init__(self, jid.host)
self.jid = jid
self.password = password
self.check_certificate = check_certificate
def associateWithStream(self, xs):
xmlstream.ConnectAuthenticator.associateWithStream(self, xs)
tlsInit = xmlstream.TLSInitiatingInitializer(xs)
tlsInit.check_certificate = self.check_certificate
xs.initializers = [client.client.CheckVersionInitializer(xs),
tlsInit,
client.CheckAuthInitializer(xs)]
def apply():
xmlstream.TLSInitiatingInitializer = TLSInitiatingInitializer
client.XMPPClient = XMPPClient
client.HybridClientFactory = HybridClientFactory
client.HybridAuthenticator = HybridAuthenticator

View file

@ -19,6 +19,8 @@
import sat
from sat.core.i18n import _, languageSwitch
from sat.core import patches
patches.apply()
from twisted.application import service
from twisted.internet import defer
from twisted.words.protocols.jabber import jid

View file

@ -35,6 +35,7 @@ from sat.core.log import getLogger
log = getLogger(__name__)
from sat.core import exceptions
from sat.memory import encryption
from sat.tools import xml_tools
from zope.interface import implements
import time
import calendar
@ -44,6 +45,7 @@ import sys
class SatXMPPEntity(object):
"""Common code for Client and Component"""
_reason = None # reason of disconnection
def __init__(self, host_app, profile, max_retries):
@ -214,6 +216,11 @@ class SatXMPPEntity(object):
return
super(SatXMPPEntity, self)._authd(xmlstream)
if self._reason is not None:
# if we have had trouble to connect we can reset
# the exception as the connection is now working.
del self._reason
# the following Deferred is used to know when we are connected
# so we need to be set it to None when connection is lost
self._connected = defer.Deferred()
@ -267,6 +274,11 @@ class SatXMPPEntity(object):
## connection ##
def _disconnected(self, reason):
# we have to save the reason of disconnection, otherwise it would be lost
self._reason = reason
super(SatXMPPEntity, self)._disconnected(reason)
def connectionLost(self, connector, reason):
try:
self.keep_alife.stop()
@ -288,9 +300,21 @@ class SatXMPPEntity(object):
if not self.conn_deferred.called:
# FIXME: real error is not gotten here (e.g. if jid is not know by Prosody,
# we should have the real error)
self.conn_deferred.errback(
error.StreamError(u"Server unexpectedly closed the connection")
)
if self._reason is None:
err = error.StreamError(u"Server unexpectedly closed the connection")
else:
err = self._reason
try:
if err.value.args[0][0][2] == "certificate verify failed":
err = exceptions.InvalidCertificate(
_(u"Your server certificate is not valid "
u"(its identity can't be checked).\n\n"
u"This should never happen and may indicate that "
u"somebody is trying to spy on you.\n"
u"Please contact your server administrator."))
except (IndexError, TypeError):
pass
self.conn_deferred.errback(err)
@defer.inlineCallbacks
def _cleanConnection(self, __):
@ -544,16 +568,8 @@ class SatXMPPClient(SatXMPPEntity, wokkel_client.XMPPClient):
trigger_suffix = ""
is_component = False
def __init__(
self,
host_app,
profile,
user_jid,
password,
host=None,
port=C.XMPP_C2S_PORT,
max_retries=C.XMPP_MAX_RETRIES,
):
def __init__(self, host_app, profile, user_jid, password, host=None,
port=C.XMPP_C2S_PORT, max_retries=C.XMPP_MAX_RETRIES):
# XXX: DNS SRV records are checked when the host is not specified.
# If no SRV record is found, the host is directly extracted from the JID.
self.started = time.time()
@ -594,11 +610,24 @@ class SatXMPPClient(SatXMPPEntity, wokkel_client.XMPPClient):
.format(host_ori=user_jid.host, host=host, port=port)
)
self.check_certificate = host_app.memory.getParamA(
"check_certificate", "Connection", profile_key=profile)
wokkel_client.XMPPClient.__init__(
self, user_jid, password, host or None, port or C.XMPP_C2S_PORT
self, user_jid, password, host or None, port or C.XMPP_C2S_PORT,
check_certificate = self.check_certificate
)
SatXMPPEntity.__init__(self, host_app, profile, max_retries)
if not self.check_certificate:
msg = (_(u"Certificate validation is deactivated, this is unsecure and "
u"somebody may be spying on you. If you have no good reason to disable "
u"certificate validation, please activate \"Check certificate\" in your "
u"settings in \"Connection\" tab."))
xml_tools.quickNote(host_app, self, msg, _(u"Security notice"),
level = C.XMLUI_DATA_LVL_WARNING)
def _getPluginsList(self):
for p in self.host_app.plugins.itervalues():
if C.PLUG_MODE_CLIENT in p._info[u"modes"]:

View file

@ -75,23 +75,25 @@ class Params(object):
<param name="%(force_port_param)s" value="" type="int" constraint="1;65535" security="50" />
<param name="autoconnect" label="%(autoconnect_label)s" value="true" type="bool" security="50" />
<param name="autodisconnect" label="%(autodisconnect_label)s" value="false" type="bool" security="50" />
<param name="check_certificate" label="%(check_certificate_label)s" value="true" type="bool" security="4" />
</category>
</individual>
</params>
""" % {
"category_general": D_("General"),
"category_connection": D_("Connection"),
"history_param": C.HISTORY_LIMIT,
"history_label": D_("Chat history limit"),
"show_offline_contacts": C.SHOW_OFFLINE_CONTACTS,
"show_offline_contacts_label": D_("Show offline contacts"),
"show_empty_groups": C.SHOW_EMPTY_GROUPS,
"show_empty_groups_label": D_("Show empty groups"),
"force_server_param": C.FORCE_SERVER_PARAM,
"force_port_param": C.FORCE_PORT_PARAM,
"new_account_label": D_("Register new account"),
"autoconnect_label": D_("Connect on frontend startup"),
"autodisconnect_label": D_("Disconnect on frontend closure"),
u"category_general": D_(u"General"),
u"category_connection": D_(u"Connection"),
u"history_param": C.HISTORY_LIMIT,
u"history_label": D_(u"Chat history limit"),
u"show_offline_contacts": C.SHOW_OFFLINE_CONTACTS,
u"show_offline_contacts_label": D_(u"Show offline contacts"),
u"show_empty_groups": C.SHOW_EMPTY_GROUPS,
u"show_empty_groups_label": D_(u"Show empty groups"),
u"force_server_param": C.FORCE_SERVER_PARAM,
u"force_port_param": C.FORCE_PORT_PARAM,
u"new_account_label": D_(u"Register new account"),
u"autoconnect_label": D_(u"Connect on frontend startup"),
u"autodisconnect_label": D_(u"Disconnect on frontend closure"),
u"check_certificate_label": D_(u"Check certificate (don't uncheck if unsure)"),
}
def load_default_params(self):