Philipp Hörist pushed to branch master at gajim / gajim
Commits:
-
bcf27cb3
by Philipp Hörist at 2018-05-01T21:08:26+02:00
5 changed files:
- gajim/common/config.py
- gajim/common/connection.py
- gajim/common/connection_handlers_events.py
- gajim/common/const.py
- gajim/gui_interface.py
Changes:
| ... | ... | @@ -409,6 +409,7 @@ class Config: |
| 409 | 409 |
'recent_groupchats': [ opt_str, '' ],
|
| 410 | 410 |
'httpupload_verify': [ opt_bool, True, _('HTTP Upload: Enable HTTPS Verification')],
|
| 411 | 411 |
'filetransfer_preference' : [ opt_str, 'httpupload', _('Preferred file transfer mechanism for file drag&drop on chat window. Can be \'httpupload\' (default) or \'jingle\'')],
|
| 412 |
+ 'allow_posh': [ opt_bool, True, _('Allow cert verification with POSH')],
|
|
| 412 | 413 |
}, {}),
|
| 413 | 414 |
'statusmsg': ({
|
| 414 | 415 |
'message': [ opt_str, '' ],
|
| ... | ... | @@ -43,8 +43,11 @@ import hmac |
| 43 | 43 |
import hashlib
|
| 44 | 44 |
import json
|
| 45 | 45 |
import logging
|
| 46 |
+import base64
|
|
| 46 | 47 |
from functools import partial
|
| 47 | 48 |
from string import Template
|
| 49 |
+from urllib.request import urlopen
|
|
| 50 |
+from urllib.error import URLError
|
|
| 48 | 51 |
|
| 49 | 52 |
try:
|
| 50 | 53 |
randomsource = random.SystemRandom()
|
| ... | ... | @@ -75,42 +78,6 @@ from gajim.gtkgui_helpers import get_action |
| 75 | 78 |
|
| 76 | 79 |
log = logging.getLogger('gajim.c.connection')
|
| 77 | 80 |
|
| 78 |
-ssl_error = {
|
|
| 79 |
- 2: _("Unable to get issuer certificate"),
|
|
| 80 |
- 3: _("Unable to get certificate CRL"),
|
|
| 81 |
- 4: _("Unable to decrypt certificate's signature"),
|
|
| 82 |
- 5: _("Unable to decrypt CRL's signature"),
|
|
| 83 |
- 6: _("Unable to decode issuer public key"),
|
|
| 84 |
- 7: _("Certificate signature failure"),
|
|
| 85 |
- 8: _("CRL signature failure"),
|
|
| 86 |
- 9: _("Certificate is not yet valid"),
|
|
| 87 |
- 10: _("Certificate has expired"),
|
|
| 88 |
- 11: _("CRL is not yet valid"),
|
|
| 89 |
- 12: _("CRL has expired"),
|
|
| 90 |
- 13: _("Format error in certificate's notBefore field"),
|
|
| 91 |
- 14: _("Format error in certificate's notAfter field"),
|
|
| 92 |
- 15: _("Format error in CRL's lastUpdate field"),
|
|
| 93 |
- 16: _("Format error in CRL's nextUpdate field"),
|
|
| 94 |
- 17: _("Out of memory"),
|
|
| 95 |
- 18: _("Self signed certificate"),
|
|
| 96 |
- 19: _("Self signed certificate in certificate chain"),
|
|
| 97 |
- 20: _("Unable to get local issuer certificate"),
|
|
| 98 |
- 21: _("Unable to verify the first certificate"),
|
|
| 99 |
- 22: _("Certificate chain too long"),
|
|
| 100 |
- 23: _("Certificate revoked"),
|
|
| 101 |
- 24: _("Invalid CA certificate"),
|
|
| 102 |
- 25: _("Path length constraint exceeded"),
|
|
| 103 |
- 26: _("Unsupported certificate purpose"),
|
|
| 104 |
- 27: _("Certificate not trusted"),
|
|
| 105 |
- 28: _("Certificate rejected"),
|
|
| 106 |
- 29: _("Subject issuer mismatch"),
|
|
| 107 |
- 30: _("Authority and subject key identifier mismatch"),
|
|
| 108 |
- 31: _("Authority and issuer serial number mismatch"),
|
|
| 109 |
- 32: _("Key usage does not include certificate signing"),
|
|
| 110 |
- 50: _("Application verification failure")
|
|
| 111 |
- #100 is for internal usage: host not correct
|
|
| 112 |
-}
|
|
| 113 |
- |
|
| 114 | 81 |
SERVICE_START_TLS = 'xmpp-client'
|
| 115 | 82 |
SERVICE_DIRECT_TLS = 'xmpps-client'
|
| 116 | 83 |
|
| ... | ... | @@ -693,6 +660,13 @@ class Connection(CommonConnection, ConnectionHandlers): |
| 693 | 660 |
self.secret_hmac = str(random.random())[2:].encode('utf-8')
|
| 694 | 661 |
self.removing_account = False
|
| 695 | 662 |
|
| 663 |
+ # We only request POSH once
|
|
| 664 |
+ self._posh_requested = False
|
|
| 665 |
+ # Fingerprints received via POSH
|
|
| 666 |
+ self._posh_hashes = []
|
|
| 667 |
+ # The SSL Errors that we can override with POSH
|
|
| 668 |
+ self._posh_errors = [18, 19]
|
|
| 669 |
+ |
|
| 696 | 670 |
self.sm = Smacks(self) # Stream Management
|
| 697 | 671 |
|
| 698 | 672 |
app.ged.register_event_handler('privacy-list-received', ged.CORE,
|
| ... | ... | @@ -1374,25 +1348,85 @@ class Connection(CommonConnection, ConnectionHandlers): |
| 1374 | 1348 |
|
| 1375 | 1349 |
cert = self.connection.Connection.ssl_certificate
|
| 1376 | 1350 |
errnum = self._ssl_errors.pop()
|
| 1377 |
- hostname = app.config.get_per('accounts', self.name, 'hostname')
|
|
| 1378 |
- text = _('The authenticity of the %s '
|
|
| 1379 |
- 'certificate could be invalid') % hostname
|
|
| 1380 |
- if errnum in ssl_error:
|
|
| 1381 |
- text += _('\nSSL Error: <b>%s</b>') % ssl_error[errnum]
|
|
| 1382 |
- else:
|
|
| 1383 |
- text += _('\nUnknown SSL error: %d') % errnum
|
|
| 1384 |
- fingerprint_sha1 = cert.digest('sha1').decode('utf-8')
|
|
| 1385 |
- fingerprint_sha256 = cert.digest('sha256').decode('utf-8')
|
|
| 1386 |
- pem = OpenSSL.crypto.dump_certificate(
|
|
| 1387 |
- OpenSSL.crypto.FILETYPE_PEM, cert).decode('utf-8')
|
|
| 1388 |
- app.nec.push_incoming_event(
|
|
| 1389 |
- SSLErrorEvent(None, conn=self,
|
|
| 1390 |
- error_text=text,
|
|
| 1391 |
- error_num=errnum,
|
|
| 1392 |
- cert=pem,
|
|
| 1393 |
- fingerprint_sha1=fingerprint_sha1,
|
|
| 1394 |
- fingerprint_sha256=fingerprint_sha256,
|
|
| 1395 |
- certificate=cert))
|
|
| 1351 |
+ |
|
| 1352 |
+ # Check if we can verify the cert with POSH
|
|
| 1353 |
+ if errnum in self._posh_errors:
|
|
| 1354 |
+ # Request the POSH json file
|
|
| 1355 |
+ self._get_posh_file(self._hostname)
|
|
| 1356 |
+ self._posh_requested = True
|
|
| 1357 |
+ cert_hash256 = self._calculate_cert_sha256(cert)
|
|
| 1358 |
+ print(cert_hash256)
|
|
| 1359 |
+ if cert_hash256 in self._posh_hashes:
|
|
| 1360 |
+ # Ignore this error if this cert is
|
|
| 1361 |
+ # verifyed with POSH
|
|
| 1362 |
+ self.process_ssl_errors()
|
|
| 1363 |
+ return
|
|
| 1364 |
+ |
|
| 1365 |
+ app.nec.push_incoming_event(SSLErrorEvent(None, conn=self,
|
|
| 1366 |
+ error_num=errnum,
|
|
| 1367 |
+ cert=cert))
|
|
| 1368 |
+ |
|
| 1369 |
+ @staticmethod
|
|
| 1370 |
+ def _calculate_cert_sha256(cert):
|
|
| 1371 |
+ der_encoded = OpenSSL.crypto.dump_certificate(
|
|
| 1372 |
+ OpenSSL.crypto.FILETYPE_ASN1, cert)
|
|
| 1373 |
+ hash_obj = hashlib.sha256(der_encoded)
|
|
| 1374 |
+ hash256 = base64.b64encode(hash_obj.digest()).decode('utf8')
|
|
| 1375 |
+ return hash256
|
|
| 1376 |
+ |
|
| 1377 |
+ def _get_posh_file(self, hostname=None, redirect=None):
|
|
| 1378 |
+ if self._posh_requested:
|
|
| 1379 |
+ # We already have requested POSH
|
|
| 1380 |
+ return
|
|
| 1381 |
+ |
|
| 1382 |
+ if not app.config.get_per('accounts', self.name, 'allow_posh'):
|
|
| 1383 |
+ return
|
|
| 1384 |
+ |
|
| 1385 |
+ if hostname is None and redirect is None:
|
|
| 1386 |
+ raise ValueError('There must be either a hostname or a url')
|
|
| 1387 |
+ |
|
| 1388 |
+ url = redirect
|
|
| 1389 |
+ if hostname is not None:
|
|
| 1390 |
+ url = 'https://%s/.well-known/posh/xmpp-client.json' % hostname
|
|
| 1391 |
+ |
|
| 1392 |
+ cafile = None
|
|
| 1393 |
+ if os.name == 'nt':
|
|
| 1394 |
+ cafile = certifi.where()
|
|
| 1395 |
+ |
|
| 1396 |
+ log.info('Request POSH from %s', url)
|
|
| 1397 |
+ try:
|
|
| 1398 |
+ file = urlopen(
|
|
| 1399 |
+ url, cafile=cafile, timeout=2)
|
|
| 1400 |
+ except URLError as exc:
|
|
| 1401 |
+ log.info('Error while requesting POSH: %s' % exc)
|
|
| 1402 |
+ return
|
|
| 1403 |
+ |
|
| 1404 |
+ if file.getcode() != 200:
|
|
| 1405 |
+ log.info('No POSH file found at %s', url)
|
|
| 1406 |
+ return
|
|
| 1407 |
+ |
|
| 1408 |
+ try:
|
|
| 1409 |
+ posh = json.loads(file.read())
|
|
| 1410 |
+ except json.decoder.JSONDecodeError as json_error:
|
|
| 1411 |
+ log.warning(json_error)
|
|
| 1412 |
+ return
|
|
| 1413 |
+ |
|
| 1414 |
+ # Redirect
|
|
| 1415 |
+ if 'url' in posh and redirect is None:
|
|
| 1416 |
+ # We dont allow redirects in redirects
|
|
| 1417 |
+ log.info('POSH redirect found')
|
|
| 1418 |
+ self._get_posh_file(redirect=posh['url'])
|
|
| 1419 |
+ return
|
|
| 1420 |
+ |
|
| 1421 |
+ if 'fingerprints' in posh:
|
|
| 1422 |
+ fingerprints = posh['fingerprints']
|
|
| 1423 |
+ for fingerprint in fingerprints:
|
|
| 1424 |
+ if 'sha-256' not in fingerprint:
|
|
| 1425 |
+ continue
|
|
| 1426 |
+ self._posh_hashes.append(fingerprint['sha-256'])
|
|
| 1427 |
+ |
|
| 1428 |
+ log.info('POSH sha-256 fingerprints found: %s',
|
|
| 1429 |
+ self._posh_hashes)
|
|
| 1396 | 1430 |
|
| 1397 | 1431 |
def ssl_certificate_accepted(self):
|
| 1398 | 1432 |
if not self.connection:
|
| ... | ... | @@ -42,7 +42,7 @@ from gajim.common import i18n |
| 42 | 42 |
from gajim.common import dataforms
|
| 43 | 43 |
from gajim.common import configpaths
|
| 44 | 44 |
from gajim.common.zeroconf.zeroconf import Constant
|
| 45 |
-from gajim.common.const import KindConstant
|
|
| 45 |
+from gajim.common.const import KindConstant, SSLError
|
|
| 46 | 46 |
from gajim.common.pep import SUPPORTED_PERSONAL_USER_EVENTS
|
| 47 | 47 |
from gajim.common.jingle_transport import JingleTransportSocks5
|
| 48 | 48 |
from gajim.common.file_props import FilesProp
|
| ... | ... | @@ -1985,8 +1985,7 @@ class NewAccountConnectedEvent(nec.NetworkIncomingEvent): |
| 1985 | 1985 |
self.errnum = 0 # we don't have an errnum
|
| 1986 | 1986 |
self.ssl_msg = ''
|
| 1987 | 1987 |
if self.errnum > 0:
|
| 1988 |
- from gajim.common.connection import ssl_error
|
|
| 1989 |
- self.ssl_msg = ssl_error.get(self.errnum,
|
|
| 1988 |
+ self.ssl_msg = SSLError.get(self.errnum,
|
|
| 1990 | 1989 |
_('Unknown SSL error: %d') % self.errnum)
|
| 1991 | 1990 |
self.ssl_cert = ''
|
| 1992 | 1991 |
self.ssl_fingerprint_sha1 = ''
|
| ... | ... | @@ -107,6 +107,42 @@ class JIDConstant(IntEnum): |
| 107 | 107 |
ROOM_TYPE = 1
|
| 108 | 108 |
|
| 109 | 109 |
|
| 110 |
+SSLError = {
|
|
| 111 |
+ 2: _("Unable to get issuer certificate"),
|
|
| 112 |
+ 3: _("Unable to get certificate CRL"),
|
|
| 113 |
+ 4: _("Unable to decrypt certificate's signature"),
|
|
| 114 |
+ 5: _("Unable to decrypt CRL's signature"),
|
|
| 115 |
+ 6: _("Unable to decode issuer public key"),
|
|
| 116 |
+ 7: _("Certificate signature failure"),
|
|
| 117 |
+ 8: _("CRL signature failure"),
|
|
| 118 |
+ 9: _("Certificate is not yet valid"),
|
|
| 119 |
+ 10: _("Certificate has expired"),
|
|
| 120 |
+ 11: _("CRL is not yet valid"),
|
|
| 121 |
+ 12: _("CRL has expired"),
|
|
| 122 |
+ 13: _("Format error in certificate's notBefore field"),
|
|
| 123 |
+ 14: _("Format error in certificate's notAfter field"),
|
|
| 124 |
+ 15: _("Format error in CRL's lastUpdate field"),
|
|
| 125 |
+ 16: _("Format error in CRL's nextUpdate field"),
|
|
| 126 |
+ 17: _("Out of memory"),
|
|
| 127 |
+ 18: _("Self signed certificate"),
|
|
| 128 |
+ 19: _("Self signed certificate in certificate chain"),
|
|
| 129 |
+ 20: _("Unable to get local issuer certificate"),
|
|
| 130 |
+ 21: _("Unable to verify the first certificate"),
|
|
| 131 |
+ 22: _("Certificate chain too long"),
|
|
| 132 |
+ 23: _("Certificate revoked"),
|
|
| 133 |
+ 24: _("Invalid CA certificate"),
|
|
| 134 |
+ 25: _("Path length constraint exceeded"),
|
|
| 135 |
+ 26: _("Unsupported certificate purpose"),
|
|
| 136 |
+ 27: _("Certificate not trusted"),
|
|
| 137 |
+ 28: _("Certificate rejected"),
|
|
| 138 |
+ 29: _("Subject issuer mismatch"),
|
|
| 139 |
+ 30: _("Authority and subject key identifier mismatch"),
|
|
| 140 |
+ 31: _("Authority and issuer serial number mismatch"),
|
|
| 141 |
+ 32: _("Key usage does not include certificate signing"),
|
|
| 142 |
+ 50: _("Application verification failure"),
|
|
| 143 |
+ }
|
|
| 144 |
+ |
|
| 145 |
+ |
|
| 110 | 146 |
THANKS = u"""\
|
| 111 | 147 |
Alexander Futász
|
| 112 | 148 |
Alexander V. Butenko
|
| ... | ... | @@ -98,7 +98,7 @@ from gajim.common.connection import Connection |
| 98 | 98 |
from gajim.common.file_props import FilesProp
|
| 99 | 99 |
from gajim.common import pep
|
| 100 | 100 |
from gajim import emoticons
|
| 101 |
-from gajim.common.const import AvatarSize
|
|
| 101 |
+from gajim.common.const import AvatarSize, SSLError
|
|
| 102 | 102 |
|
| 103 | 103 |
from gajim import roster_window
|
| 104 | 104 |
from gajim import profile_window
|
| ... | ... | @@ -1343,7 +1343,6 @@ class Interface: |
| 1343 | 1343 |
obj.exchange_items_list, obj.fjid)
|
| 1344 | 1344 |
|
| 1345 | 1345 |
def handle_event_ssl_error(self, obj):
|
| 1346 |
- # ('SSL_ERROR', account, (text, errnum, cert, sha1_fingerprint, sha256_fingerprint))
|
|
| 1347 | 1346 |
account = obj.conn.name
|
| 1348 | 1347 |
server = app.config.get_per('accounts', account, 'hostname')
|
| 1349 | 1348 |
|
| ... | ... | @@ -1379,22 +1378,32 @@ class Interface: |
| 1379 | 1378 |
app.nec.push_incoming_event(OurShowEvent(None, conn=obj.conn,
|
| 1380 | 1379 |
show='offline'))
|
| 1381 | 1380 |
|
| 1381 |
+ text = _('The authenticity of the %s '
|
|
| 1382 |
+ 'certificate could be invalid') % server
|
|
| 1383 |
+ |
|
| 1384 |
+ default_text = _('\nUnknown SSL error: %d') % obj.error_num
|
|
| 1385 |
+ ssl_error_text = SSLError.get(obj.error_num, default_text)
|
|
| 1386 |
+ text += _('\nSSL Error: <b>%s</b>') % ssl_error_text
|
|
| 1387 |
+ |
|
| 1388 |
+ fingerprint_sha1 = obj.cert.digest('sha1').decode('utf-8')
|
|
| 1389 |
+ fingerprint_sha256 = obj.cert.digest('sha256').decode('utf-8')
|
|
| 1390 |
+ |
|
| 1382 | 1391 |
pritext = _('Error verifying SSL certificate')
|
| 1383 | 1392 |
sectext = _('There was an error verifying the SSL certificate of your '
|
| 1384 | 1393 |
'XMPP server: %(error)s\nDo you still want to connect to this '
|
| 1385 |
- 'server?') % {'error': obj.error_text}
|
|
| 1394 |
+ 'server?') % {'error': text}
|
|
| 1386 | 1395 |
if obj.error_num in (18, 27):
|
| 1387 | 1396 |
checktext1 = _('Add this certificate to the list of trusted '
|
| 1388 | 1397 |
'certificates.\nSHA-1 fingerprint of the certificate:\n%(sha1)s'
|
| 1389 | 1398 |
'\nSHA-256 fingerprint of the certificate:\n%(sha256)s') % \
|
| 1390 |
- {'sha1': obj.fingerprint_sha1, 'sha256': obj.fingerprint_sha256}
|
|
| 1399 |
+ {'sha1': fingerprint_sha1, 'sha256': fingerprint_sha256}
|
|
| 1391 | 1400 |
else:
|
| 1392 | 1401 |
checktext1 = ''
|
| 1393 | 1402 |
checktext2 = _('Ignore this error for this certificate.')
|
| 1394 | 1403 |
if 'ssl_error' in self.instances[account]['online_dialog']:
|
| 1395 | 1404 |
self.instances[account]['online_dialog']['ssl_error'].destroy()
|
| 1396 | 1405 |
self.instances[account]['online_dialog']['ssl_error'] = \
|
| 1397 |
- dialogs.SSLErrorDialog(obj.conn.name, obj.certificate, pritext,
|
|
| 1406 |
+ dialogs.SSLErrorDialog(obj.conn.name, obj.cert, pritext,
|
|
| 1398 | 1407 |
sectext, checktext1, checktext2, on_response_ok=on_ok,
|
| 1399 | 1408 |
on_response_cancel=on_cancel)
|
| 1400 | 1409 |
self.instances[account]['online_dialog']['ssl_error'].set_title(
|