diff --git a/gajim/common/config.py b/gajim/common/config.py
index cb81531ec..5e3fbbf51 100644
--- a/gajim/common/config.py
+++ b/gajim/common/config.py
@@ -409,6 +409,7 @@ class Config:
'recent_groupchats': [ opt_str, '' ],
'httpupload_verify': [ opt_bool, True, _('HTTP Upload: Enable HTTPS Verification')],
'filetransfer_preference' : [ opt_str, 'httpupload', _('Preferred file transfer mechanism for file drag&drop on chat window. Can be \'httpupload\' (default) or \'jingle\'')],
+ 'allow_posh': [ opt_bool, True, _('Allow cert verification with POSH')],
}, {}),
'statusmsg': ({
'message': [ opt_str, '' ],
diff --git a/gajim/common/connection.py b/gajim/common/connection.py
index c6d88a7ad..ef3d14339 100644
--- a/gajim/common/connection.py
+++ b/gajim/common/connection.py
@@ -43,8 +43,11 @@ import hmac
import hashlib
import json
import logging
+import base64
from functools import partial
from string import Template
+from urllib.request import urlopen
+from urllib.error import URLError
try:
randomsource = random.SystemRandom()
@@ -75,42 +78,6 @@ from gajim.gtkgui_helpers import get_action
log = logging.getLogger('gajim.c.connection')
-ssl_error = {
- 2: _("Unable to get issuer certificate"),
- 3: _("Unable to get certificate CRL"),
- 4: _("Unable to decrypt certificate's signature"),
- 5: _("Unable to decrypt CRL's signature"),
- 6: _("Unable to decode issuer public key"),
- 7: _("Certificate signature failure"),
- 8: _("CRL signature failure"),
- 9: _("Certificate is not yet valid"),
- 10: _("Certificate has expired"),
- 11: _("CRL is not yet valid"),
- 12: _("CRL has expired"),
- 13: _("Format error in certificate's notBefore field"),
- 14: _("Format error in certificate's notAfter field"),
- 15: _("Format error in CRL's lastUpdate field"),
- 16: _("Format error in CRL's nextUpdate field"),
- 17: _("Out of memory"),
- 18: _("Self signed certificate"),
- 19: _("Self signed certificate in certificate chain"),
- 20: _("Unable to get local issuer certificate"),
- 21: _("Unable to verify the first certificate"),
- 22: _("Certificate chain too long"),
- 23: _("Certificate revoked"),
- 24: _("Invalid CA certificate"),
- 25: _("Path length constraint exceeded"),
- 26: _("Unsupported certificate purpose"),
- 27: _("Certificate not trusted"),
- 28: _("Certificate rejected"),
- 29: _("Subject issuer mismatch"),
- 30: _("Authority and subject key identifier mismatch"),
- 31: _("Authority and issuer serial number mismatch"),
- 32: _("Key usage does not include certificate signing"),
- 50: _("Application verification failure")
- #100 is for internal usage: host not correct
-}
-
SERVICE_START_TLS = 'xmpp-client'
SERVICE_DIRECT_TLS = 'xmpps-client'
@@ -693,6 +660,13 @@ class Connection(CommonConnection, ConnectionHandlers):
self.secret_hmac = str(random.random())[2:].encode('utf-8')
self.removing_account = False
+ # We only request POSH once
+ self._posh_requested = False
+ # Fingerprints received via POSH
+ self._posh_hashes = []
+ # The SSL Errors that we can override with POSH
+ self._posh_errors = [18, 19]
+
self.sm = Smacks(self) # Stream Management
app.ged.register_event_handler('privacy-list-received', ged.CORE,
@@ -1374,25 +1348,85 @@ class Connection(CommonConnection, ConnectionHandlers):
cert = self.connection.Connection.ssl_certificate
errnum = self._ssl_errors.pop()
- hostname = app.config.get_per('accounts', self.name, 'hostname')
- text = _('The authenticity of the %s '
- 'certificate could be invalid') % hostname
- if errnum in ssl_error:
- text += _('\nSSL Error: %s') % ssl_error[errnum]
- else:
- text += _('\nUnknown SSL error: %d') % errnum
- fingerprint_sha1 = cert.digest('sha1').decode('utf-8')
- fingerprint_sha256 = cert.digest('sha256').decode('utf-8')
- pem = OpenSSL.crypto.dump_certificate(
- OpenSSL.crypto.FILETYPE_PEM, cert).decode('utf-8')
- app.nec.push_incoming_event(
- SSLErrorEvent(None, conn=self,
- error_text=text,
- error_num=errnum,
- cert=pem,
- fingerprint_sha1=fingerprint_sha1,
- fingerprint_sha256=fingerprint_sha256,
- certificate=cert))
+
+ # Check if we can verify the cert with POSH
+ if errnum in self._posh_errors:
+ # Request the POSH json file
+ self._get_posh_file(self._hostname)
+ self._posh_requested = True
+ cert_hash256 = self._calculate_cert_sha256(cert)
+ print(cert_hash256)
+ if cert_hash256 in self._posh_hashes:
+ # Ignore this error if this cert is
+ # verifyed with POSH
+ self.process_ssl_errors()
+ return
+
+ app.nec.push_incoming_event(SSLErrorEvent(None, conn=self,
+ error_num=errnum,
+ cert=cert))
+
+ @staticmethod
+ def _calculate_cert_sha256(cert):
+ der_encoded = OpenSSL.crypto.dump_certificate(
+ OpenSSL.crypto.FILETYPE_ASN1, cert)
+ hash_obj = hashlib.sha256(der_encoded)
+ hash256 = base64.b64encode(hash_obj.digest()).decode('utf8')
+ return hash256
+
+ def _get_posh_file(self, hostname=None, redirect=None):
+ if self._posh_requested:
+ # We already have requested POSH
+ return
+
+ if not app.config.get_per('accounts', self.name, 'allow_posh'):
+ return
+
+ if hostname is None and redirect is None:
+ raise ValueError('There must be either a hostname or a url')
+
+ url = redirect
+ if hostname is not None:
+ url = 'https://%s/.well-known/posh/xmpp-client.json' % hostname
+
+ cafile = None
+ if os.name == 'nt':
+ cafile = certifi.where()
+
+ log.info('Request POSH from %s', url)
+ try:
+ file = urlopen(
+ url, cafile=cafile, timeout=2)
+ except URLError as exc:
+ log.info('Error while requesting POSH: %s' % exc)
+ return
+
+ if file.getcode() != 200:
+ log.info('No POSH file found at %s', url)
+ return
+
+ try:
+ posh = json.loads(file.read())
+ except json.decoder.JSONDecodeError as json_error:
+ log.warning(json_error)
+ return
+
+ # Redirect
+ if 'url' in posh and redirect is None:
+ # We dont allow redirects in redirects
+ log.info('POSH redirect found')
+ self._get_posh_file(redirect=posh['url'])
+ return
+
+ if 'fingerprints' in posh:
+ fingerprints = posh['fingerprints']
+ for fingerprint in fingerprints:
+ if 'sha-256' not in fingerprint:
+ continue
+ self._posh_hashes.append(fingerprint['sha-256'])
+
+ log.info('POSH sha-256 fingerprints found: %s',
+ self._posh_hashes)
def ssl_certificate_accepted(self):
if not self.connection:
diff --git a/gajim/common/connection_handlers_events.py b/gajim/common/connection_handlers_events.py
index 9d29357d1..c5f06f7ba 100644
--- a/gajim/common/connection_handlers_events.py
+++ b/gajim/common/connection_handlers_events.py
@@ -42,7 +42,7 @@ from gajim.common import i18n
from gajim.common import dataforms
from gajim.common import configpaths
from gajim.common.zeroconf.zeroconf import Constant
-from gajim.common.const import KindConstant
+from gajim.common.const import KindConstant, SSLError
from gajim.common.pep import SUPPORTED_PERSONAL_USER_EVENTS
from gajim.common.jingle_transport import JingleTransportSocks5
from gajim.common.file_props import FilesProp
@@ -1985,8 +1985,7 @@ class NewAccountConnectedEvent(nec.NetworkIncomingEvent):
self.errnum = 0 # we don't have an errnum
self.ssl_msg = ''
if self.errnum > 0:
- from gajim.common.connection import ssl_error
- self.ssl_msg = ssl_error.get(self.errnum,
+ self.ssl_msg = SSLError.get(self.errnum,
_('Unknown SSL error: %d') % self.errnum)
self.ssl_cert = ''
self.ssl_fingerprint_sha1 = ''
diff --git a/gajim/common/const.py b/gajim/common/const.py
index 80aba8da4..4125ea1c8 100644
--- a/gajim/common/const.py
+++ b/gajim/common/const.py
@@ -107,6 +107,42 @@ class JIDConstant(IntEnum):
ROOM_TYPE = 1
+SSLError = {
+ 2: _("Unable to get issuer certificate"),
+ 3: _("Unable to get certificate CRL"),
+ 4: _("Unable to decrypt certificate's signature"),
+ 5: _("Unable to decrypt CRL's signature"),
+ 6: _("Unable to decode issuer public key"),
+ 7: _("Certificate signature failure"),
+ 8: _("CRL signature failure"),
+ 9: _("Certificate is not yet valid"),
+ 10: _("Certificate has expired"),
+ 11: _("CRL is not yet valid"),
+ 12: _("CRL has expired"),
+ 13: _("Format error in certificate's notBefore field"),
+ 14: _("Format error in certificate's notAfter field"),
+ 15: _("Format error in CRL's lastUpdate field"),
+ 16: _("Format error in CRL's nextUpdate field"),
+ 17: _("Out of memory"),
+ 18: _("Self signed certificate"),
+ 19: _("Self signed certificate in certificate chain"),
+ 20: _("Unable to get local issuer certificate"),
+ 21: _("Unable to verify the first certificate"),
+ 22: _("Certificate chain too long"),
+ 23: _("Certificate revoked"),
+ 24: _("Invalid CA certificate"),
+ 25: _("Path length constraint exceeded"),
+ 26: _("Unsupported certificate purpose"),
+ 27: _("Certificate not trusted"),
+ 28: _("Certificate rejected"),
+ 29: _("Subject issuer mismatch"),
+ 30: _("Authority and subject key identifier mismatch"),
+ 31: _("Authority and issuer serial number mismatch"),
+ 32: _("Key usage does not include certificate signing"),
+ 50: _("Application verification failure"),
+ }
+
+
THANKS = u"""\
Alexander Futász
Alexander V. Butenko
diff --git a/gajim/gui_interface.py b/gajim/gui_interface.py
index 0929ba714..6afc52988 100644
--- a/gajim/gui_interface.py
+++ b/gajim/gui_interface.py
@@ -98,7 +98,7 @@ from gajim.common.connection import Connection
from gajim.common.file_props import FilesProp
from gajim.common import pep
from gajim import emoticons
-from gajim.common.const import AvatarSize
+from gajim.common.const import AvatarSize, SSLError
from gajim import roster_window
from gajim import profile_window
@@ -1343,7 +1343,6 @@ class Interface:
obj.exchange_items_list, obj.fjid)
def handle_event_ssl_error(self, obj):
- # ('SSL_ERROR', account, (text, errnum, cert, sha1_fingerprint, sha256_fingerprint))
account = obj.conn.name
server = app.config.get_per('accounts', account, 'hostname')
@@ -1379,22 +1378,32 @@ class Interface:
app.nec.push_incoming_event(OurShowEvent(None, conn=obj.conn,
show='offline'))
+ text = _('The authenticity of the %s '
+ 'certificate could be invalid') % server
+
+ default_text = _('\nUnknown SSL error: %d') % obj.error_num
+ ssl_error_text = SSLError.get(obj.error_num, default_text)
+ text += _('\nSSL Error: %s') % ssl_error_text
+
+ fingerprint_sha1 = obj.cert.digest('sha1').decode('utf-8')
+ fingerprint_sha256 = obj.cert.digest('sha256').decode('utf-8')
+
pritext = _('Error verifying SSL certificate')
sectext = _('There was an error verifying the SSL certificate of your '
'XMPP server: %(error)s\nDo you still want to connect to this '
- 'server?') % {'error': obj.error_text}
+ 'server?') % {'error': text}
if obj.error_num in (18, 27):
checktext1 = _('Add this certificate to the list of trusted '
'certificates.\nSHA-1 fingerprint of the certificate:\n%(sha1)s'
'\nSHA-256 fingerprint of the certificate:\n%(sha256)s') % \
- {'sha1': obj.fingerprint_sha1, 'sha256': obj.fingerprint_sha256}
+ {'sha1': fingerprint_sha1, 'sha256': fingerprint_sha256}
else:
checktext1 = ''
checktext2 = _('Ignore this error for this certificate.')
if 'ssl_error' in self.instances[account]['online_dialog']:
self.instances[account]['online_dialog']['ssl_error'].destroy()
self.instances[account]['online_dialog']['ssl_error'] = \
- dialogs.SSLErrorDialog(obj.conn.name, obj.certificate, pritext,
+ dialogs.SSLErrorDialog(obj.conn.name, obj.cert, pritext,
sectext, checktext1, checktext2, on_response_ok=on_ok,
on_response_cancel=on_cancel)
self.instances[account]['online_dialog']['ssl_error'].set_title(