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.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(

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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 = ''