From e1dbfc454180a77ebb38ecae6244ac4abe6d0ac5 Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 18 Oct 2012 09:30:53 +0900 Subject: catch cert verification errors and ask user for trust with a little helper function using gnutls --- src/leap/base/checks.py | 17 +++--- src/leap/base/exceptions.py | 5 ++ src/leap/crypto/certs.py | 31 ++++++++++ src/leap/eip/checks.py | 27 +++++++-- src/leap/eip/exceptions.py | 11 ++++ src/leap/gui/firstrunwizard.py | 128 +++++++++++++++++++++++++++++++++-------- 6 files changed, 182 insertions(+), 37 deletions(-) create mode 100644 src/leap/crypto/certs.py diff --git a/src/leap/base/checks.py b/src/leap/base/checks.py index 7285e74f..23446f4a 100644 --- a/src/leap/base/checks.py +++ b/src/leap/base/checks.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import logging import platform +import socket import netifaces import ping @@ -23,7 +24,7 @@ class LeapNetworkChecker(object): def run_all(self, checker=None): if not checker: checker = self - self.error = None # ? + #self.error = None # ? # for MVS checker.check_tunnel_default_interface() @@ -118,11 +119,9 @@ class LeapNetworkChecker(object): if packet_loss > constants.MAX_ICMP_PACKET_LOSS: raise exceptions.NoConnectionToGateway - # XXX check for name resolution servers - # dunno what's the best way to do this... - # check for etc/resolv entries or similar? - # just try to resolve? - # is there something in psutil? - - # def check_name_resolution(self): - # pass + def check_name_resolution(self, domain_name): + try: + socket.gethostbyname(domain_name) + return True + except socket.gaierror: + raise exceptions.CannotResolveDomainError diff --git a/src/leap/base/exceptions.py b/src/leap/base/exceptions.py index f12a49d5..227da953 100644 --- a/src/leap/base/exceptions.py +++ b/src/leap/base/exceptions.py @@ -67,6 +67,11 @@ class NoInternetConnection(CriticalError): # and now we try to connect to our web to troubleshoot LOL :P +class CannotResolveDomainError(LeapException): + message = "Cannot resolve domain" + usermessage = "Domain cannot be found" + + class TunnelNotDefaultRouteError(CriticalError): message = "Tunnel connection dissapeared. VPN down?" usermessage = "The Encrypted Connection was lost. Shutting down..." diff --git a/src/leap/crypto/certs.py b/src/leap/crypto/certs.py new file mode 100644 index 00000000..aa1fc9e9 --- /dev/null +++ b/src/leap/crypto/certs.py @@ -0,0 +1,31 @@ +import ctypes +import socket + +import gnutls.connection +import gnutls.library + + +def get_https_cert_fingerprint(domain): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + cred = gnutls.connection.X509Credentials() + + session = gnutls.connection.ClientSession(sock, cred) + session.connect((domain, 443)) + session.handshake() + cert = session.peer_certificate + + _buffer = ctypes.create_string_buffer(20) + buffer_length = ctypes.c_size_t(20) + + gnutls.library.functions.gnutls_x509_crt_get_fingerprint( + cert._c_object, gnutls.library.constants.GNUTLS_DIG_SHA1, # 3 + ctypes.byref(_buffer), ctypes.byref(buffer_length)) + + # deinit + #server_cert._X509Certificate__deinit(server_cert._c_object) + # needed? is segfaulting + + fpr = ctypes.string_at(_buffer, buffer_length.value) + hex_fpr = u":".join(u"%02X" % ord(char) for char in fpr) + + return hex_fpr diff --git a/src/leap/eip/checks.py b/src/leap/eip/checks.py index f739c3e8..c704aef3 100644 --- a/src/leap/eip/checks.py +++ b/src/leap/eip/checks.py @@ -94,6 +94,7 @@ class ProviderCertChecker(object): raise NotImplementedError def is_there_provider_ca(self): + # XXX remove for generic build from leap import certs logger.debug('do we have provider_ca?') cacert_path = BRANDING.get('provider_ca_file', None) @@ -104,30 +105,46 @@ class ProviderCertChecker(object): logger.debug('True') return True - def is_https_working(self, uri=None, verify=True): + def is_https_working( + self, uri=None, verify=True, + autocacert=False): if uri is None: uri = self._get_root_uri() # XXX raise InsecureURI or something better - assert uri.startswith('https') - if verify is True and self.cacert is not None: + try: + assert uri.startswith('https') + except AssertionError: + raise AssertionError( + "uri passed should start with https") + if autocacert and verify is True and self.cacert is not None: logger.debug('verify cert: %s', self.cacert) verify = self.cacert logger.debug('is https working?') logger.debug('uri: %s (verify:%s)', uri, verify) try: self.fetcher.get(uri, verify=verify) + + except requests.exceptions.SSLError as exc: + logger.error("SSLError") + raise eipexceptions.HttpsBadCertError + + except requests.exceptions.ConnectionError: + logger.error('ConnectionError') + raise eipexceptions.HttpsNotSupported + except requests.exceptions.SSLError as exc: logger.warning('False! CERT VERIFICATION FAILED! ' '(this should be CRITICAL)') logger.warning('SSLError: %s', exc.message) # XXX RAISE! See #638 #raise eipexceptions.EIPBadCertError - # XXX get requests.exceptions.ConnectionError Errno 110 - # Connection timed out, and raise ours. else: logger.debug('True') return True + def get_certificate_fingerprint(self, domain): + pass + def check_new_cert_needed(self, skip_download=False, verify=True): logger.debug('is new cert needed?') if not self.is_cert_valid(do_raise=False): diff --git a/src/leap/eip/exceptions.py b/src/leap/eip/exceptions.py index 11bfd620..41eed77a 100644 --- a/src/leap/eip/exceptions.py +++ b/src/leap/eip/exceptions.py @@ -32,8 +32,10 @@ TODO: * gettext / i18n for user messages. """ +from leap.base.exceptions import LeapException +# This should inherit from LeapException class EIPClientError(Exception): """ base EIPClient exception @@ -99,6 +101,15 @@ class OpenVPNAlreadyRunning(EIPClientError): "Please close it before starting leap-client") +class HttpsNotSupported(LeapException): + message = "connection refused while accessing via https" + usermessage = "Server does not allow secure connections." + + +class HttpsBadCertError(LeapException): + message = "verification error on cert" + usermessage = "Server certificate could not be verified." + # # errors still needing some love # diff --git a/src/leap/gui/firstrunwizard.py b/src/leap/gui/firstrunwizard.py index bc36a35f..53e551ac 100755 --- a/src/leap/gui/firstrunwizard.py +++ b/src/leap/gui/firstrunwizard.py @@ -11,7 +11,12 @@ from PyQt4 import QtCore from PyQt4 import QtGui from leap.base.auth import LeapSRPRegister +from leap.base import checks as basechecks +from leap.base import exceptions as baseexceptions +from leap.crypto import certs from leap.crypto import leapkeyring +from leap.eip import checks as eipchecks +from leap.eip import exceptions as eipexceptions from leap.gui import mainwindow_rc try: @@ -39,7 +44,10 @@ class FirstRunWizard(QtGui.QWizard): def __init__( self, parent=None, providers=None, success_cb=None, is_provider_setup=False, - is_previously_registered=False): + is_previously_registered=False, + netchecker=basechecks.LeapNetworkChecker, + providercertchecker=eipchecks.ProviderCertChecker, + eipconfigchecker=eipchecks.EIPConfigChecker): super(FirstRunWizard, self).__init__( parent, QtCore.Qt.WindowStaysOnTopHint) @@ -59,25 +67,25 @@ class FirstRunWizard(QtGui.QWizard): # if True, jumps to LogIn page. self.is_previously_registered = is_previously_registered - # FIXME remove kwargs, we can access - # wizard as self.wizard() + # Checkers + self.netchecker = netchecker + self.providercertchecker = providercertchecker + self.eipconfigchecker = eipconfigchecker # FIXME add param for previously_registered # should start at login page. pages_dict = OrderedDict(( - # (name, (WizardPage, **kwargs)) - ('intro', (IntroPage, {})), - ('providerselection', ( - SelectProviderPage, - {'providers': providers})), - ('login', (LogInPage, {})), - ('providerinfo', (ProviderInfoPage, {})), - ('providersetup', (ProviderSetupPage, {})), - ('signup', ( - RegisterUserPage, {})), - ('connecting', (ConnectingPage, {})), - ('lastpage', (LastPage, {})) + # (name, WizardPage) + ('intro', IntroPage), + ('providerselection', + SelectProviderPage), + ('login', LogInPage), + ('providerinfo', ProviderInfoPage), + ('providersetup', ProviderSetupPage), + ('signup', RegisterUserPage), + ('connecting', ConnectingPage), + ('lastpage', LastPage) )) self.add_pages_from_dict(pages_dict) @@ -99,10 +107,10 @@ class FirstRunWizard(QtGui.QWizard): values are a tuple of InstanceofWizardPage, kwargs. @type pages_dict: dict """ - for name, (page, page_args) in pages_dict.items(): + for name, page in pages_dict.items(): # XXX check for is_previously registered # and skip adding the signup branch if so - self.addPage(page(**page_args)) + self.addPage(page()) self.pages_dict = pages_dict def get_page_index(self, page_name): @@ -234,7 +242,7 @@ class SelectProviderPage(QtGui.QWizardPage): def __init__(self, parent=None, providers=None): super(SelectProviderPage, self).__init__(parent) - self.setTitle("Select Provider") + self.setTitle("Enter Provider") self.setSubTitle( "Please enter the domain of the provider you want " "to use for your connection." @@ -243,7 +251,9 @@ class SelectProviderPage(QtGui.QWizardPage): QtGui.QWizard.LogoPixmap, QtGui.QPixmap(APP_LOGO)) - providerNameLabel = QtGui.QLabel("&Provider:") + providerNameLabel = QtGui.QLabel("h&ttps://") + # note that we expect the bare domain name + # we will add the scheme later providerNameEdit = QtGui.QLineEdit() providerNameEdit.cursorPositionChanged.connect( self.reset_validation_status) @@ -269,13 +279,28 @@ class SelectProviderPage(QtGui.QWizardPage): validationMsg = QtGui.QLabel("") validationMsg.setStyleSheet(ErrorLabelStyleSheet) - self.validationMsg = validationMsg + # XXX cert info + self.certInfo = QtGui.QLabel("") + self.certInfo.setWordWrap(True) + self.certWarning = QtGui.QLabel("") + self.trustProviderCertCheckBox = QtGui.QCheckBox( + "&Trust this provider certificate.") + layout = QtGui.QGridLayout() - layout.addWidget(validationMsg, 0, 0) - layout.addWidget(providerNameLabel, 0, 1) - layout.addWidget(providerNameEdit, 0, 2) + layout.addWidget(validationMsg, 0, 2) + layout.addWidget(providerNameLabel, 1, 1) + layout.addWidget(providerNameEdit, 1, 2) + + # XXX get a groupbox or something.... + layout.addWidget(self.certInfo, 4, 1, 4, 2) + layout.addWidget(self.certWarning, 6, 1, 6, 2) + layout.addWidget( + self.trustProviderCertCheckBox, + 8, 1, 8, 2) + self.trustProviderCertCheckBox.hide() + self.setLayout(layout) def reset_validation_status(self): @@ -284,7 +309,64 @@ class SelectProviderPage(QtGui.QWizardPage): """ self.validationMsg.setText('') + def set_validation_status(self, status): + self.validationMsg.setText(status) + + def add_cert_info(self, certinfo): + self.certWarning.setText( + "Do you want to trust this provider certificate?") + self.certInfo.setText( + 'Certificate sha1: %s
' % certinfo) + self.trustProviderCertCheckBox.show() + # XXX when checkbox is marked, remove + # the red warning. + # XXX also, disable the next button! + + def initializePage(self): + self.certWarning.setText('') + self.certInfo.setText('') + self.trustProviderCertCheckBox.hide() + def validatePage(self): + wizard = self.wizard() + netchecker = wizard.netchecker() + providercertchecker = wizard.providercertchecker() + + domain = self.providerNameEdit.text() + + # try name resolution + try: + netchecker.check_name_resolution( + domain) + + except baseexceptions.LeapException as exc: + self.set_validation_status(exc.usermessage) + return False + + # try https connection + try: + providercertchecker.is_https_working( + "https://%s" % domain, + verify=True) + + except eipexceptions.HttpsBadCertError as exc: + if self.trustProviderCertCheckBox.isChecked(): + pass + else: + self.set_validation_status(exc.usermessage) + fingerprint = certs.get_https_cert_fingerprint( + domain) + self.add_cert_info(fingerprint) + return False + + except baseexceptions.LeapException as exc: + self.set_validation_status(exc.usermessage) + return False + + # try download provider info... + # TODO ... + + # all ok, go on... return True def nextId(self): -- cgit v1.2.3