parent
8d0815c176
commit
bcf27cb3c1
|
@ -409,6 +409,7 @@ class Config:
|
||||||
'recent_groupchats': [ opt_str, '' ],
|
'recent_groupchats': [ opt_str, '' ],
|
||||||
'httpupload_verify': [ opt_bool, True, _('HTTP Upload: Enable HTTPS Verification')],
|
'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\'')],
|
'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': ({
|
'statusmsg': ({
|
||||||
'message': [ opt_str, '' ],
|
'message': [ opt_str, '' ],
|
||||||
|
|
|
@ -43,8 +43,11 @@ import hmac
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import base64
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from string import Template
|
from string import Template
|
||||||
|
from urllib.request import urlopen
|
||||||
|
from urllib.error import URLError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
randomsource = random.SystemRandom()
|
randomsource = random.SystemRandom()
|
||||||
|
@ -75,42 +78,6 @@ from gajim.gtkgui_helpers import get_action
|
||||||
|
|
||||||
log = logging.getLogger('gajim.c.connection')
|
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_START_TLS = 'xmpp-client'
|
||||||
SERVICE_DIRECT_TLS = 'xmpps-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.secret_hmac = str(random.random())[2:].encode('utf-8')
|
||||||
self.removing_account = False
|
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
|
self.sm = Smacks(self) # Stream Management
|
||||||
|
|
||||||
app.ged.register_event_handler('privacy-list-received', ged.CORE,
|
app.ged.register_event_handler('privacy-list-received', ged.CORE,
|
||||||
|
@ -1374,25 +1348,85 @@ class Connection(CommonConnection, ConnectionHandlers):
|
||||||
|
|
||||||
cert = self.connection.Connection.ssl_certificate
|
cert = self.connection.Connection.ssl_certificate
|
||||||
errnum = self._ssl_errors.pop()
|
errnum = self._ssl_errors.pop()
|
||||||
hostname = app.config.get_per('accounts', self.name, 'hostname')
|
|
||||||
text = _('The authenticity of the %s '
|
# Check if we can verify the cert with POSH
|
||||||
'certificate could be invalid') % hostname
|
if errnum in self._posh_errors:
|
||||||
if errnum in ssl_error:
|
# Request the POSH json file
|
||||||
text += _('\nSSL Error: <b>%s</b>') % ssl_error[errnum]
|
self._get_posh_file(self._hostname)
|
||||||
else:
|
self._posh_requested = True
|
||||||
text += _('\nUnknown SSL error: %d') % errnum
|
cert_hash256 = self._calculate_cert_sha256(cert)
|
||||||
fingerprint_sha1 = cert.digest('sha1').decode('utf-8')
|
print(cert_hash256)
|
||||||
fingerprint_sha256 = cert.digest('sha256').decode('utf-8')
|
if cert_hash256 in self._posh_hashes:
|
||||||
pem = OpenSSL.crypto.dump_certificate(
|
# Ignore this error if this cert is
|
||||||
OpenSSL.crypto.FILETYPE_PEM, cert).decode('utf-8')
|
# verifyed with POSH
|
||||||
app.nec.push_incoming_event(
|
self.process_ssl_errors()
|
||||||
SSLErrorEvent(None, conn=self,
|
return
|
||||||
error_text=text,
|
|
||||||
error_num=errnum,
|
app.nec.push_incoming_event(SSLErrorEvent(None, conn=self,
|
||||||
cert=pem,
|
error_num=errnum,
|
||||||
fingerprint_sha1=fingerprint_sha1,
|
cert=cert))
|
||||||
fingerprint_sha256=fingerprint_sha256,
|
|
||||||
certificate=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):
|
def ssl_certificate_accepted(self):
|
||||||
if not self.connection:
|
if not self.connection:
|
||||||
|
|
|
@ -42,7 +42,7 @@ from gajim.common import i18n
|
||||||
from gajim.common import dataforms
|
from gajim.common import dataforms
|
||||||
from gajim.common import configpaths
|
from gajim.common import configpaths
|
||||||
from gajim.common.zeroconf.zeroconf import Constant
|
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.pep import SUPPORTED_PERSONAL_USER_EVENTS
|
||||||
from gajim.common.jingle_transport import JingleTransportSocks5
|
from gajim.common.jingle_transport import JingleTransportSocks5
|
||||||
from gajim.common.file_props import FilesProp
|
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.errnum = 0 # we don't have an errnum
|
||||||
self.ssl_msg = ''
|
self.ssl_msg = ''
|
||||||
if self.errnum > 0:
|
if self.errnum > 0:
|
||||||
from gajim.common.connection import ssl_error
|
self.ssl_msg = SSLError.get(self.errnum,
|
||||||
self.ssl_msg = ssl_error.get(self.errnum,
|
|
||||||
_('Unknown SSL error: %d') % self.errnum)
|
_('Unknown SSL error: %d') % self.errnum)
|
||||||
self.ssl_cert = ''
|
self.ssl_cert = ''
|
||||||
self.ssl_fingerprint_sha1 = ''
|
self.ssl_fingerprint_sha1 = ''
|
||||||
|
|
|
@ -107,6 +107,42 @@ class JIDConstant(IntEnum):
|
||||||
ROOM_TYPE = 1
|
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"""\
|
THANKS = u"""\
|
||||||
Alexander Futász
|
Alexander Futász
|
||||||
Alexander V. Butenko
|
Alexander V. Butenko
|
||||||
|
|
|
@ -98,7 +98,7 @@ from gajim.common.connection import Connection
|
||||||
from gajim.common.file_props import FilesProp
|
from gajim.common.file_props import FilesProp
|
||||||
from gajim.common import pep
|
from gajim.common import pep
|
||||||
from gajim import emoticons
|
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 roster_window
|
||||||
from gajim import profile_window
|
from gajim import profile_window
|
||||||
|
@ -1343,7 +1343,6 @@ class Interface:
|
||||||
obj.exchange_items_list, obj.fjid)
|
obj.exchange_items_list, obj.fjid)
|
||||||
|
|
||||||
def handle_event_ssl_error(self, obj):
|
def handle_event_ssl_error(self, obj):
|
||||||
# ('SSL_ERROR', account, (text, errnum, cert, sha1_fingerprint, sha256_fingerprint))
|
|
||||||
account = obj.conn.name
|
account = obj.conn.name
|
||||||
server = app.config.get_per('accounts', account, 'hostname')
|
server = app.config.get_per('accounts', account, 'hostname')
|
||||||
|
|
||||||
|
@ -1379,22 +1378,32 @@ class Interface:
|
||||||
app.nec.push_incoming_event(OurShowEvent(None, conn=obj.conn,
|
app.nec.push_incoming_event(OurShowEvent(None, conn=obj.conn,
|
||||||
show='offline'))
|
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: <b>%s</b>') % 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')
|
pritext = _('Error verifying SSL certificate')
|
||||||
sectext = _('There was an error verifying the SSL certificate of your '
|
sectext = _('There was an error verifying the SSL certificate of your '
|
||||||
'XMPP server: %(error)s\nDo you still want to connect to this '
|
'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):
|
if obj.error_num in (18, 27):
|
||||||
checktext1 = _('Add this certificate to the list of trusted '
|
checktext1 = _('Add this certificate to the list of trusted '
|
||||||
'certificates.\nSHA-1 fingerprint of the certificate:\n%(sha1)s'
|
'certificates.\nSHA-1 fingerprint of the certificate:\n%(sha1)s'
|
||||||
'\nSHA-256 fingerprint of the certificate:\n%(sha256)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:
|
else:
|
||||||
checktext1 = ''
|
checktext1 = ''
|
||||||
checktext2 = _('Ignore this error for this certificate.')
|
checktext2 = _('Ignore this error for this certificate.')
|
||||||
if 'ssl_error' in self.instances[account]['online_dialog']:
|
if 'ssl_error' in self.instances[account]['online_dialog']:
|
||||||
self.instances[account]['online_dialog']['ssl_error'].destroy()
|
self.instances[account]['online_dialog']['ssl_error'].destroy()
|
||||||
self.instances[account]['online_dialog']['ssl_error'] = \
|
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,
|
sectext, checktext1, checktext2, on_response_ok=on_ok,
|
||||||
on_response_cancel=on_cancel)
|
on_response_cancel=on_cancel)
|
||||||
self.instances[account]['online_dialog']['ssl_error'].set_title(
|
self.instances[account]['online_dialog']['ssl_error'].set_title(
|
||||||
|
|
Loading…
Reference in New Issue