Support for PKIX over Secure HTTP (POSH)

Fixes #9083
This commit is contained in:
Philipp Hörist 2018-05-01 20:32:36 +02:00
parent 8d0815c176
commit bcf27cb3c1
5 changed files with 142 additions and 63 deletions

View File

@ -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, '' ],

View File

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

View File

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

View File

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

View File

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