From df11617ddbb1f56548a3e8e75861fbdd6e2c7546 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 4 Dec 2013 18:43:28 +0100 Subject: [PATCH] both sender and receiver request remote SSL certificate, but only if it's a new one. Correctly verify remote SSL certificate. --- src/common/connection_handlers_events.py | 7 ++- src/common/jingle_content.py | 21 +++++-- src/common/jingle_ft.py | 37 ++++++++++--- src/common/jingle_transport.py | 3 +- src/common/jingle_xtls.py | 70 ++++++++++++++++-------- src/common/protocol/bytestream.py | 16 ++++-- src/common/socks5.py | 42 ++++++++++---- src/gui_interface.py | 3 + 8 files changed, 141 insertions(+), 58 deletions(-) diff --git a/src/common/connection_handlers_events.py b/src/common/connection_handlers_events.py index 16c4e1be7..a96918dcd 100644 --- a/src/common/connection_handlers_events.py +++ b/src/common/connection_handlers_events.py @@ -2053,8 +2053,11 @@ class FileRequestReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): self.fjid = self.conn._ft_get_from(self.stanza) self.jid = gajim.get_jid_without_resource(self.fjid) if self.jingle_content: - self.FT_content.use_security = bool(self.jingle_content.getTag( - 'security')) + secu = self.jingle_content.getTag('security') + self.FT_content.use_security = bool(secu) + fingerprint = secu.getTag('fingerprint') + if fingerprint: + self.FT_content.x509_fingerprint = fingerprint.getData() if not self.FT_content.transport: self.FT_content.transport = JingleTransportSocks5() self.FT_content.transport.set_our_jid( diff --git a/src/common/jingle_content.py b/src/common/jingle_content.py index b09598eaa..c417f704d 100644 --- a/src/common/jingle_content.py +++ b/src/common/jingle_content.py @@ -17,9 +17,12 @@ Handles Jingle contents (XEP 0166) """ +import os from common import gajim import nbxmpp from common.jingle_transport import JingleTransportIBB +from jingle_xtls import SELF_SIGNED_CERTIFICATE +from jingle_xtls import load_cert_file contents = {} @@ -213,12 +216,18 @@ class JingleContent(object): if self.use_security: security = nbxmpp.simplexml.Node( tag=nbxmpp.NS_JINGLE_XTLS + ' security') - # TODO: add fingerprint element - for m in ('x509', ): # supported authentication methods - method = nbxmpp.simplexml.Node(tag='method') - method.setAttr('name', m) - security.addChild(node=method) - content.addChild(node=security) + certpath = os.path.join(gajim.MY_CERT_DIR, SELF_SIGNED_CERTIFICATE)\ + + '.cert' + cert = load_cert_file(certpath) + if cert: + digest_algo = cert.get_signature_algorithm().split('With')[0] + security.addChild('fingerprint').addData(cert.digest( + digest_algo)) + for m in ('x509', ): # supported authentication methods + method = nbxmpp.simplexml.Node(tag='method') + method.setAttr('name', m) + security.addChild(node=method) + content.addChild(node=security) content.addChild(node=description_node) def destroy(self): diff --git a/src/common/jingle_ft.py b/src/common/jingle_ft.py index 3070c4978..54f5eb919 100644 --- a/src/common/jingle_ft.py +++ b/src/common/jingle_ft.py @@ -22,6 +22,7 @@ Handles Jingle File Transfer (XEP 0234) import hashlib from common import gajim import nbxmpp +import jingle_xtls from common.jingle_content import contents, JingleContent from common.jingle_transport import * from common import helpers @@ -68,6 +69,7 @@ class JingleFileTransfer(JingleContent): self.callbacks['transport-info'] += [self.__on_transport_info] self.callbacks['iq-result'] += [self.__on_iq_result] self.use_security = use_security + self.x509_fingerprint = None self.file_props = file_props self.weinitiate = self.session.weinitiate self.werequest = self.session.werequest @@ -165,17 +167,37 @@ class JingleFileTransfer(JingleContent): h.addHash(hash_, self.file_props.algo) return h + def on_cert_received(self): + self.session.approve_session() + self.session.approve_content('file', name=self.name) + def __on_session_accept(self, stanza, content, error, action): log.info("__on_session_accept") con = self.session.connection + # We ack the session accept + response = stanza.buildReply('result') + response.delChild(response.getQuery()) + con.connection.send(response) security = content.getTag('security') if not security: # responder can not verify our fingerprint self.use_security = False + else: + fingerprint = security.getTag('fingerprint') + if fingerprint: + fingerprint = fingerprint.getData() + self.x509_fingerprint = fingerprint + if not jingle_xtls.check_cert(gajim.get_jid_without_resource( + self.session.responder), fingerprint): + id_ = jingle_xtls.send_cert_request(con, + self.session.responder) + jingle_xtls.key_exchange_pend(id_, + self.continue_session_accept, [stanza]) + raise nbxmpp.NodeProcessed + self.continue_session_accept(stanza) + + def continue_session_accept(self, stanza): + con = self.session.connection if self.state == STATE_TRANSPORT_REPLACE: - # We ack the session accept - response = stanza.buildReply('result') - response.delChild(response.getQuery()) - con.connection.send(response) # If we are requesting we don't have the file if self.session.werequest: raise nbxmpp.NodeProcessed @@ -186,16 +208,13 @@ class JingleFileTransfer(JingleContent): # Calculate file hash in a new thread # if we haven't sent the hash already. if self.file_props.hash_ is None and self.file_props.algo and \ - not self.werequest: + not self.werequest: self.hashThread = threading.Thread(target=self.__send_hash) self.hashThread.start() for host in self.file_props.streamhosts: host['initiator'] = self.session.initiator host['target'] = self.session.responder host['sid'] = self.file_props.sid - response = stanza.buildReply('result') - response.delChild(response.getQuery()) - con.connection.send(response) fingerprint = None if self.use_security: fingerprint = 'client' @@ -204,7 +223,7 @@ class JingleFileTransfer(JingleContent): self.file_props.sid, self.on_connect, self._on_connect_error, fingerprint=fingerprint, receiving=False) - return + raise nbxmpp.NodeProcessed self.__state_changed(STATE_TRANSFERING) raise nbxmpp.NodeProcessed diff --git a/src/common/jingle_transport.py b/src/common/jingle_transport.py index 9a3f14f7a..0e5580575 100644 --- a/src/common/jingle_transport.py +++ b/src/common/jingle_transport.py @@ -156,7 +156,8 @@ class JingleTransportSocks5(JingleTransport): candidates.append(cand) # we need this when we construct file_props on session-initiation - self.remote_candidates = candidates + if candidates: + self.remote_candidates = candidates return candidates diff --git a/src/common/jingle_xtls.py b/src/common/jingle_xtls.py index 297609148..e8763a082 100644 --- a/src/common/jingle_xtls.py +++ b/src/common/jingle_xtls.py @@ -25,15 +25,17 @@ log = logging.getLogger('gajim.c.jingle_xtls') PYOPENSSL_PRESENT = False -pending_contents = {} # key-exchange id -> session, accept that session once key-exchange completes +# key-exchange id -> [callback, args], accept that session once key-exchange completes +pending_contents = {} -def key_exchange_pend(id_, content): - pending_contents[id_] = content +def key_exchange_pend(id_, cb, args): + # args is a list + pending_contents[id_] = [cb, args] def approve_pending_content(id_): - content = pending_contents[id_] - content.session.approve_session() - content.session.approve_content('file', name=content.name) + cb = pending_contents[id_][0] + args = pending_contents[id_][1] + cb(*args) try: import OpenSSL.SSL @@ -56,18 +58,18 @@ def default_callback(connection, certificate, error_num, depth, return_code): log.info("certificate: %s" % certificate) return return_code -def load_cert_file(cert_path, cert_store): +def load_cert_file(cert_path, cert_store=None): """ This is almost identical to the one in nbxmpp.tls_nb """ if not os.path.isfile(cert_path): - return + return None try: f = open(cert_path) except IOError as e: log.warning('Unable to open certificate file %s: %s' % (cert_path, str(e))) - return + return None lines = f.readlines() i = 0 begin = -1 @@ -79,7 +81,9 @@ def load_cert_file(cert_path, cert_store): try: x509cert = OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, cert) - cert_store.add_cert(x509cert) + if cert_store: + cert_store.add_cert(x509cert) + return x509cert except OpenSSL.crypto.Error as exception_obj: log.warning('Unable to load a certificate from file %s: %s' %\ (cert_path, exception_obj.args[0][0][2])) @@ -90,7 +94,7 @@ def load_cert_file(cert_path, cert_store): i += 1 f.close() -def get_context(fingerprint, verify_cb=None): +def get_context(fingerprint, verify_cb=None, remote_jid=None): """ constructs and returns the context objects """ @@ -130,22 +134,28 @@ def get_context(fingerprint, verify_cb=None): % (default_dh_params_name, err)) raise - store = ctx.get_cert_store() - for f in os.listdir(os.path.expanduser(gajim.MY_PEER_CERTS_PATH)): - load_cert_file(os.path.join(os.path.expanduser( - gajim.MY_PEER_CERTS_PATH), f), store) - log.debug('certificate file ' + f + ' loaded fingerprint ' + \ - fingerprint) + if remote_jid: + store = ctx.get_cert_store() + path = os.path.join(os.path.expanduser(gajim.MY_PEER_CERTS_PATH), + remote_jid) + '.cert' + if os.path.exists(path): + load_cert_file(path, cert_store=store) + log.debug('certificate file ' + path + ' loaded fingerprint ' + \ + fingerprint) return ctx -def send_cert(con, jid_from, sid): - certpath = os.path.join(gajim.MY_CERT_DIR, SELF_SIGNED_CERTIFICATE) + \ - '.cert' +def read_cert(certpath): certificate = '' with open(certpath, 'r') as certfile: for line in certfile.readlines(): if not line.startswith('-'): certificate += line + return certificate + +def send_cert(con, jid_from, sid): + certpath = os.path.join(gajim.MY_CERT_DIR, SELF_SIGNED_CERTIFICATE) + \ + '.cert' + certificate = read_cert(certpath) iq = nbxmpp.Iq('result', to=jid_from); iq.setAttr('id', sid) @@ -175,9 +185,21 @@ def handle_new_cert(con, obj, jid_from): f.write('-----BEGIN CERTIFICATE-----\n') f.write(cert) f.write('-----END CERTIFICATE-----\n') + f.close() approve_pending_content(id_) +def check_cert(jid, fingerprint): + certpath = os.path.join(os.path.expanduser(gajim.MY_PEER_CERTS_PATH), jid) + certpath += '.cert' + if os.path.exists(certpath): + cert = load_cert_file(certpath) + if cert: + digest_algo = cert.get_signature_algorithm().split('With')[0] + if cert.digest(digest_algo) == fingerprint: + return True + return False + def send_cert_request(con, to_jid): iq = nbxmpp.Iq('get', to=to_jid) id_ = con.connection.getAnID() @@ -201,12 +223,12 @@ def createKeyPair(type, bits): pkey.generate_key(type, bits) return pkey -def createCertRequest(pkey, digest="sha1", **name): +def createCertRequest(pkey, digest="sha256", **name): """ Create a certificate request. Arguments: pkey - The key to associate with the request - digest - Digestion method to use for signing, default is sha1 + digest - Digestion method to use for signing, default is sha256 **name - The name of the subject of the request, possible arguments are: C - Country name @@ -228,7 +250,7 @@ def createCertRequest(pkey, digest="sha1", **name): req.sign(pkey, digest) return req -def createCertificate(req, issuerCert, issuerKey, serial, notBefore, notAfter, digest="sha1"): +def createCertificate(req, issuerCert, issuerKey, serial, notBefore, notAfter, digest="shai256"): """ Generate a certificate given a certificate request. @@ -240,7 +262,7 @@ def createCertificate(req, issuerCert, issuerKey, serial, notBefore, notAfter, d starts being valid notAfter - Timestamp (relative to now) when the certificate stops being valid - digest - Digest method to use for signing, default is sha1 + digest - Digest method to use for signing, default is sha256 Returns: The signed certificate in an X509 object """ cert = crypto.X509() diff --git a/src/common/protocol/bytestream.py b/src/common/protocol/bytestream.py index ed6133693..79e97f1bf 100644 --- a/src/common/protocol/bytestream.py +++ b/src/common/protocol/bytestream.py @@ -153,11 +153,17 @@ class ConnectionBytestream: if not content: return if not session.accepted: - if session.get_content('file', content.name).use_security: - id_ = jingle_xtls.send_cert_request(self, - file_props.sender) - jingle_xtls.key_exchange_pend(id_, content) - return + content = session.get_content('file', content.name) + if content.use_security: + fingerprint = content.x509_fingerprint + if not jingle_xtls.check_cert( + gajim.get_jid_without_resource(file_props.sender), + fingerprint): + id_ = jingle_xtls.send_cert_request(self, + file_props.sender) + jingle_xtls.key_exchange_pend(id_, + content.on_cert_received, []) + return session.approve_session() session.approve_content('file', content.name) diff --git a/src/common/socks5.py b/src/common/socks5.py index ea84fefed..9ccfa92c2 100644 --- a/src/common/socks5.py +++ b/src/common/socks5.py @@ -119,8 +119,7 @@ class SocksQueue: streamhosts_to_test = [] # Remove local IPs to not connect to ourself for streamhost in file_props.streamhosts: - if streamhost['host'] == '127.0.0.1' or \ - streamhost['host'] == '::1': + if streamhost['host'] == '127.0.0.1' or streamhost['host'] == '::1': continue streamhosts_to_test.append(streamhost) if not streamhosts_to_test: @@ -327,7 +326,7 @@ class SocksQueue: if listener.file_props.type_ == 's' and \ not self.isHashInSockObjs(self.senders, sock_hash): sockobj = Socks5SenderServer(self.idlequeue, sock_hash, self, - sock[0], sock[1][0], sock[1][1], fingerprint='server', + sock[0], sock[1][0], sock[1][1], fingerprint='server', file_props=listener.file_props) self._add(sockobj, self.senders, listener.file_props, sock_hash) # Start waiting for data @@ -416,7 +415,7 @@ class SocksQueue: self.connected -= 1 -class Socks5: +class Socks5(object): def __init__(self, idlequeue, host, port, initiator, target, sid): if host is not None: try: @@ -440,12 +439,20 @@ class Socks5: self.file = None self.connected = False self.mode = '' + self.ssl_cert = None + self.ssl_errnum = 0 def _is_connected(self): if self.state < 5: return False return True + def ssl_verify_cb(self, ssl_conn, cert, error_num, depth, return_code): + if depth == 0: + self.ssl_cert = cert + self.ssl_errnum = error_num + return True + def connect(self): """ Create the socket and plug it to the idlequeue @@ -456,8 +463,16 @@ class Socks5: try: self._sock = socket.socket(*ai[:3]) if not self.fingerprint is None: + if self.file_props.type_ == 's': + remote_jid = gajim.get_jid_without_resource( + self.file_props.receiver) + else: + remote_jid = gajim.get_jid_without_resource( + self.file_props.sender) self._sock = OpenSSL.SSL.Connection( - jingle_xtls.get_context('client'), self._sock) + jingle_xtls.get_context('client', + verify_cb=self.ssl_verify_cb, remote_jid=remote_jid), + self._sock) # this will not block the GUI self._sock.setblocking(False) self._server = ai[4] @@ -477,9 +492,10 @@ class Socks5: def do_connect(self): try: self._sock.connect(self._server) - self._sock.setblocking(False) self._send=self._sock.send self._recv=self._sock.recv + except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError), e: + pass except Exception as ee: errnum = ee.errno self.connect_timeout += 1 @@ -887,7 +903,6 @@ class Socks5Sender(IdleObject): """ Class for sending file to socket over socks5 """ - def __init__(self, idlequeue, sock_hash, parent, _sock, host=None, port=None, fingerprint = None, connected=True, file_props={}): self.fingerprint = fingerprint @@ -968,7 +983,6 @@ class Socks5Sender(IdleObject): class Socks5Receiver(IdleObject): - def __init__(self, idlequeue, streamhost, sid, file_props = None, fingerprint=None): """ @@ -1231,6 +1245,14 @@ class Socks5Client(Socks5): self.state += 1 return None + def send_file(self): + if self.ssl_errnum > 0: + log.error('remote certificate does not match the announced one.' + \ + '\nSSL Error: %d\nCancelling file transfer' % self.ssl_errnum) + self.file_props.error = -12 + return -1 + return super(Socks5Client, self).send_file() + def pollin(self): self.idlequeue.remove_timeout(self.fd) if self.connected: @@ -1312,9 +1334,8 @@ class Socks5SenderServer(Socks5Server, Socks5Sender): class Socks5ReceiverClient(Socks5Client, Socks5Receiver): - def __init__(self, idlequeue, streamhost, sid, file_props = None, - fingerprint=None): + fingerprint=None): Socks5Client.__init__(self, idlequeue, streamhost['host'], int(streamhost['port']), streamhost['initiator'], streamhost['target'], sid) @@ -1436,4 +1457,3 @@ class Socks5Listener(IdleObject): self.connections.append(_sock[0]) return _sock - diff --git a/src/gui_interface.py b/src/gui_interface.py index 6d4fc5688..9a314be61 100644 --- a/src/gui_interface.py +++ b/src/gui_interface.py @@ -1022,6 +1022,9 @@ class Interface: error_msg=_('Error opening file')) elif file_props.error == -10: ft.show_hash_error(jid, file_props, account) + elif file_props.error == -12: + ft.show_stopped(jid, file_props, + error_msg=_('SSL certificate error')) return msg_type = ''