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:
parent
6e410b463b
commit
df11617ddb
|
@ -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(
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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 = ''
|
||||
|
|
Loading…
Reference in New Issue