both sender and receiver request remote SSL certificate, but only if it's a new one.

Correctly verify remote SSL certificate.
This commit is contained in:
Yann Leboulanger 2013-12-04 18:43:28 +01:00
parent 6e410b463b
commit df11617ddb
8 changed files with 141 additions and 58 deletions

View file

@ -2053,8 +2053,11 @@ class FileRequestReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
self.fjid = self.conn._ft_get_from(self.stanza) self.fjid = self.conn._ft_get_from(self.stanza)
self.jid = gajim.get_jid_without_resource(self.fjid) self.jid = gajim.get_jid_without_resource(self.fjid)
if self.jingle_content: if self.jingle_content:
self.FT_content.use_security = bool(self.jingle_content.getTag( secu = self.jingle_content.getTag('security')
'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: if not self.FT_content.transport:
self.FT_content.transport = JingleTransportSocks5() self.FT_content.transport = JingleTransportSocks5()
self.FT_content.transport.set_our_jid( self.FT_content.transport.set_our_jid(

View file

@ -17,9 +17,12 @@
Handles Jingle contents (XEP 0166) Handles Jingle contents (XEP 0166)
""" """
import os
from common import gajim from common import gajim
import nbxmpp import nbxmpp
from common.jingle_transport import JingleTransportIBB from common.jingle_transport import JingleTransportIBB
from jingle_xtls import SELF_SIGNED_CERTIFICATE
from jingle_xtls import load_cert_file
contents = {} contents = {}
@ -213,12 +216,18 @@ class JingleContent(object):
if self.use_security: if self.use_security:
security = nbxmpp.simplexml.Node( security = nbxmpp.simplexml.Node(
tag=nbxmpp.NS_JINGLE_XTLS + ' security') tag=nbxmpp.NS_JINGLE_XTLS + ' security')
# TODO: add fingerprint element certpath = os.path.join(gajim.MY_CERT_DIR, SELF_SIGNED_CERTIFICATE)\
for m in ('x509', ): # supported authentication methods + '.cert'
method = nbxmpp.simplexml.Node(tag='method') cert = load_cert_file(certpath)
method.setAttr('name', m) if cert:
security.addChild(node=method) digest_algo = cert.get_signature_algorithm().split('With')[0]
content.addChild(node=security) 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) content.addChild(node=description_node)
def destroy(self): def destroy(self):

View file

@ -22,6 +22,7 @@ Handles Jingle File Transfer (XEP 0234)
import hashlib import hashlib
from common import gajim from common import gajim
import nbxmpp import nbxmpp
import jingle_xtls
from common.jingle_content import contents, JingleContent from common.jingle_content import contents, JingleContent
from common.jingle_transport import * from common.jingle_transport import *
from common import helpers from common import helpers
@ -68,6 +69,7 @@ class JingleFileTransfer(JingleContent):
self.callbacks['transport-info'] += [self.__on_transport_info] self.callbacks['transport-info'] += [self.__on_transport_info]
self.callbacks['iq-result'] += [self.__on_iq_result] self.callbacks['iq-result'] += [self.__on_iq_result]
self.use_security = use_security self.use_security = use_security
self.x509_fingerprint = None
self.file_props = file_props self.file_props = file_props
self.weinitiate = self.session.weinitiate self.weinitiate = self.session.weinitiate
self.werequest = self.session.werequest self.werequest = self.session.werequest
@ -165,17 +167,37 @@ class JingleFileTransfer(JingleContent):
h.addHash(hash_, self.file_props.algo) h.addHash(hash_, self.file_props.algo)
return h 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): def __on_session_accept(self, stanza, content, error, action):
log.info("__on_session_accept") log.info("__on_session_accept")
con = self.session.connection 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') security = content.getTag('security')
if not security: # responder can not verify our fingerprint if not security: # responder can not verify our fingerprint
self.use_security = False 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: 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 we are requesting we don't have the file
if self.session.werequest: if self.session.werequest:
raise nbxmpp.NodeProcessed raise nbxmpp.NodeProcessed
@ -186,16 +208,13 @@ class JingleFileTransfer(JingleContent):
# Calculate file hash in a new thread # Calculate file hash in a new thread
# if we haven't sent the hash already. # if we haven't sent the hash already.
if self.file_props.hash_ is None and self.file_props.algo and \ 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 = threading.Thread(target=self.__send_hash)
self.hashThread.start() self.hashThread.start()
for host in self.file_props.streamhosts: for host in self.file_props.streamhosts:
host['initiator'] = self.session.initiator host['initiator'] = self.session.initiator
host['target'] = self.session.responder host['target'] = self.session.responder
host['sid'] = self.file_props.sid host['sid'] = self.file_props.sid
response = stanza.buildReply('result')
response.delChild(response.getQuery())
con.connection.send(response)
fingerprint = None fingerprint = None
if self.use_security: if self.use_security:
fingerprint = 'client' fingerprint = 'client'
@ -204,7 +223,7 @@ class JingleFileTransfer(JingleContent):
self.file_props.sid, self.on_connect, self.file_props.sid, self.on_connect,
self._on_connect_error, fingerprint=fingerprint, self._on_connect_error, fingerprint=fingerprint,
receiving=False) receiving=False)
return raise nbxmpp.NodeProcessed
self.__state_changed(STATE_TRANSFERING) self.__state_changed(STATE_TRANSFERING)
raise nbxmpp.NodeProcessed raise nbxmpp.NodeProcessed

View file

@ -156,7 +156,8 @@ class JingleTransportSocks5(JingleTransport):
candidates.append(cand) candidates.append(cand)
# we need this when we construct file_props on session-initiation # we need this when we construct file_props on session-initiation
self.remote_candidates = candidates if candidates:
self.remote_candidates = candidates
return candidates return candidates

View file

@ -25,15 +25,17 @@ log = logging.getLogger('gajim.c.jingle_xtls')
PYOPENSSL_PRESENT = False 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): def key_exchange_pend(id_, cb, args):
pending_contents[id_] = content # args is a list
pending_contents[id_] = [cb, args]
def approve_pending_content(id_): def approve_pending_content(id_):
content = pending_contents[id_] cb = pending_contents[id_][0]
content.session.approve_session() args = pending_contents[id_][1]
content.session.approve_content('file', name=content.name) cb(*args)
try: try:
import OpenSSL.SSL import OpenSSL.SSL
@ -56,18 +58,18 @@ def default_callback(connection, certificate, error_num, depth, return_code):
log.info("certificate: %s" % certificate) log.info("certificate: %s" % certificate)
return return_code 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 This is almost identical to the one in nbxmpp.tls_nb
""" """
if not os.path.isfile(cert_path): if not os.path.isfile(cert_path):
return return None
try: try:
f = open(cert_path) f = open(cert_path)
except IOError as e: except IOError as e:
log.warning('Unable to open certificate file %s: %s' % (cert_path, log.warning('Unable to open certificate file %s: %s' % (cert_path,
str(e))) str(e)))
return return None
lines = f.readlines() lines = f.readlines()
i = 0 i = 0
begin = -1 begin = -1
@ -79,7 +81,9 @@ def load_cert_file(cert_path, cert_store):
try: try:
x509cert = OpenSSL.crypto.load_certificate( x509cert = OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM, cert) 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: except OpenSSL.crypto.Error as exception_obj:
log.warning('Unable to load a certificate from file %s: %s' %\ log.warning('Unable to load a certificate from file %s: %s' %\
(cert_path, exception_obj.args[0][0][2])) (cert_path, exception_obj.args[0][0][2]))
@ -90,7 +94,7 @@ def load_cert_file(cert_path, cert_store):
i += 1 i += 1
f.close() 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 constructs and returns the context objects
""" """
@ -130,22 +134,28 @@ def get_context(fingerprint, verify_cb=None):
% (default_dh_params_name, err)) % (default_dh_params_name, err))
raise raise
store = ctx.get_cert_store() if remote_jid:
for f in os.listdir(os.path.expanduser(gajim.MY_PEER_CERTS_PATH)): store = ctx.get_cert_store()
load_cert_file(os.path.join(os.path.expanduser( path = os.path.join(os.path.expanduser(gajim.MY_PEER_CERTS_PATH),
gajim.MY_PEER_CERTS_PATH), f), store) remote_jid) + '.cert'
log.debug('certificate file ' + f + ' loaded fingerprint ' + \ if os.path.exists(path):
fingerprint) load_cert_file(path, cert_store=store)
log.debug('certificate file ' + path + ' loaded fingerprint ' + \
fingerprint)
return ctx return ctx
def send_cert(con, jid_from, sid): def read_cert(certpath):
certpath = os.path.join(gajim.MY_CERT_DIR, SELF_SIGNED_CERTIFICATE) + \
'.cert'
certificate = '' certificate = ''
with open(certpath, 'r') as certfile: with open(certpath, 'r') as certfile:
for line in certfile.readlines(): for line in certfile.readlines():
if not line.startswith('-'): if not line.startswith('-'):
certificate += line 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 = nbxmpp.Iq('result', to=jid_from);
iq.setAttr('id', sid) iq.setAttr('id', sid)
@ -175,9 +185,21 @@ def handle_new_cert(con, obj, jid_from):
f.write('-----BEGIN CERTIFICATE-----\n') f.write('-----BEGIN CERTIFICATE-----\n')
f.write(cert) f.write(cert)
f.write('-----END CERTIFICATE-----\n') f.write('-----END CERTIFICATE-----\n')
f.close()
approve_pending_content(id_) 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): def send_cert_request(con, to_jid):
iq = nbxmpp.Iq('get', to=to_jid) iq = nbxmpp.Iq('get', to=to_jid)
id_ = con.connection.getAnID() id_ = con.connection.getAnID()
@ -201,12 +223,12 @@ def createKeyPair(type, bits):
pkey.generate_key(type, bits) pkey.generate_key(type, bits)
return pkey return pkey
def createCertRequest(pkey, digest="sha1", **name): def createCertRequest(pkey, digest="sha256", **name):
""" """
Create a certificate request. Create a certificate request.
Arguments: pkey - The key to associate with the 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 **name - The name of the subject of the request, possible
arguments are: arguments are:
C - Country name C - Country name
@ -228,7 +250,7 @@ def createCertRequest(pkey, digest="sha1", **name):
req.sign(pkey, digest) req.sign(pkey, digest)
return req 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. Generate a certificate given a certificate request.
@ -240,7 +262,7 @@ def createCertificate(req, issuerCert, issuerKey, serial, notBefore, notAfter, d
starts being valid starts being valid
notAfter - Timestamp (relative to now) when the certificate notAfter - Timestamp (relative to now) when the certificate
stops being valid 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 Returns: The signed certificate in an X509 object
""" """
cert = crypto.X509() cert = crypto.X509()

View file

@ -153,11 +153,17 @@ class ConnectionBytestream:
if not content: if not content:
return return
if not session.accepted: if not session.accepted:
if session.get_content('file', content.name).use_security: content = session.get_content('file', content.name)
id_ = jingle_xtls.send_cert_request(self, if content.use_security:
file_props.sender) fingerprint = content.x509_fingerprint
jingle_xtls.key_exchange_pend(id_, content) if not jingle_xtls.check_cert(
return 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_session()
session.approve_content('file', content.name) session.approve_content('file', content.name)

View file

@ -119,8 +119,7 @@ class SocksQueue:
streamhosts_to_test = [] streamhosts_to_test = []
# Remove local IPs to not connect to ourself # Remove local IPs to not connect to ourself
for streamhost in file_props.streamhosts: for streamhost in file_props.streamhosts:
if streamhost['host'] == '127.0.0.1' or \ if streamhost['host'] == '127.0.0.1' or streamhost['host'] == '::1':
streamhost['host'] == '::1':
continue continue
streamhosts_to_test.append(streamhost) streamhosts_to_test.append(streamhost)
if not streamhosts_to_test: if not streamhosts_to_test:
@ -327,7 +326,7 @@ class SocksQueue:
if listener.file_props.type_ == 's' and \ if listener.file_props.type_ == 's' and \
not self.isHashInSockObjs(self.senders, sock_hash): not self.isHashInSockObjs(self.senders, sock_hash):
sockobj = Socks5SenderServer(self.idlequeue, sock_hash, self, 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) file_props=listener.file_props)
self._add(sockobj, self.senders, listener.file_props, sock_hash) self._add(sockobj, self.senders, listener.file_props, sock_hash)
# Start waiting for data # Start waiting for data
@ -416,7 +415,7 @@ class SocksQueue:
self.connected -= 1 self.connected -= 1
class Socks5: class Socks5(object):
def __init__(self, idlequeue, host, port, initiator, target, sid): def __init__(self, idlequeue, host, port, initiator, target, sid):
if host is not None: if host is not None:
try: try:
@ -440,12 +439,20 @@ class Socks5:
self.file = None self.file = None
self.connected = False self.connected = False
self.mode = '' self.mode = ''
self.ssl_cert = None
self.ssl_errnum = 0
def _is_connected(self): def _is_connected(self):
if self.state < 5: if self.state < 5:
return False return False
return True 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): def connect(self):
""" """
Create the socket and plug it to the idlequeue Create the socket and plug it to the idlequeue
@ -456,8 +463,16 @@ class Socks5:
try: try:
self._sock = socket.socket(*ai[:3]) self._sock = socket.socket(*ai[:3])
if not self.fingerprint is None: 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( 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 # this will not block the GUI
self._sock.setblocking(False) self._sock.setblocking(False)
self._server = ai[4] self._server = ai[4]
@ -477,9 +492,10 @@ class Socks5:
def do_connect(self): def do_connect(self):
try: try:
self._sock.connect(self._server) self._sock.connect(self._server)
self._sock.setblocking(False)
self._send=self._sock.send self._send=self._sock.send
self._recv=self._sock.recv self._recv=self._sock.recv
except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError), e:
pass
except Exception as ee: except Exception as ee:
errnum = ee.errno errnum = ee.errno
self.connect_timeout += 1 self.connect_timeout += 1
@ -887,7 +903,6 @@ class Socks5Sender(IdleObject):
""" """
Class for sending file to socket over socks5 Class for sending file to socket over socks5
""" """
def __init__(self, idlequeue, sock_hash, parent, _sock, host=None, def __init__(self, idlequeue, sock_hash, parent, _sock, host=None,
port=None, fingerprint = None, connected=True, file_props={}): port=None, fingerprint = None, connected=True, file_props={}):
self.fingerprint = fingerprint self.fingerprint = fingerprint
@ -968,7 +983,6 @@ class Socks5Sender(IdleObject):
class Socks5Receiver(IdleObject): class Socks5Receiver(IdleObject):
def __init__(self, idlequeue, streamhost, sid, file_props = None, def __init__(self, idlequeue, streamhost, sid, file_props = None,
fingerprint=None): fingerprint=None):
""" """
@ -1231,6 +1245,14 @@ class Socks5Client(Socks5):
self.state += 1 self.state += 1
return None 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): def pollin(self):
self.idlequeue.remove_timeout(self.fd) self.idlequeue.remove_timeout(self.fd)
if self.connected: if self.connected:
@ -1312,9 +1334,8 @@ class Socks5SenderServer(Socks5Server, Socks5Sender):
class Socks5ReceiverClient(Socks5Client, Socks5Receiver): class Socks5ReceiverClient(Socks5Client, Socks5Receiver):
def __init__(self, idlequeue, streamhost, sid, file_props = None, def __init__(self, idlequeue, streamhost, sid, file_props = None,
fingerprint=None): fingerprint=None):
Socks5Client.__init__(self, idlequeue, streamhost['host'], Socks5Client.__init__(self, idlequeue, streamhost['host'],
int(streamhost['port']), streamhost['initiator'], int(streamhost['port']), streamhost['initiator'],
streamhost['target'], sid) streamhost['target'], sid)
@ -1436,4 +1457,3 @@ class Socks5Listener(IdleObject):
self.connections.append(_sock[0]) self.connections.append(_sock[0])
return _sock return _sock

View file

@ -1022,6 +1022,9 @@ class Interface:
error_msg=_('Error opening file')) error_msg=_('Error opening file'))
elif file_props.error == -10: elif file_props.error == -10:
ft.show_hash_error(jid, file_props, account) 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 return
msg_type = '' msg_type = ''