URI: 
       tAdd --serverfingerprint option (#6094) - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit ca1046bce2c8d57c03c7803fab0e14b7c119b2c6
   DIR parent e2ae44beb99c8be98a8a978e7e69612534fe4621
  HTML Author: Luke Childs <lukechilds123@gmail.com>
       Date:   Fri, 24 Apr 2020 21:11:40 +0700
       
       Add --serverfingerprint option (#6094)
       
       * Add --fingerprint option
       
       * Simplify conditional checks
       
       * Improve warning wording
       
       * Throw error instead of logging and returning
       
       * --fingerprint => --serverfingerprint
       
       * Only run fingerprint checks against main server
       
       * Throw error if --serverfingerprint is set for a non SSL main server
       
       * Fix linting errors
       
       * Don't check certificate fingerprint in a seperate connection
       
       * Disallow CA signed certs when a fingerprint is provided
       
       * Show clear error and then exit for Qt GUI users
       
       * Remove leading newlines from error dialog
       
       * Always check is_main_server() when getting fingerprint
       
       * Document how to generate SSL cert fingerprint
       Diffstat:
         M electrum/commands.py                |       2 ++
         M electrum/gui/qt/main_window.py      |      16 +++++++++++++++-
         M electrum/interface.py               |      26 +++++++++++++++++++++++++-
       
       3 files changed, 42 insertions(+), 2 deletions(-)
       ---
   DIR diff --git a/electrum/commands.py b/electrum/commands.py
       t@@ -1268,6 +1268,8 @@ argparse._SubParsersAction.__call__ = subparser_call
        
        
        def add_network_options(parser):
       +    parser.add_argument("-f", "--serverfingerprint", dest="serverfingerprint", default=None, help="only allow connecting to servers with a matching SSL certificate SHA256 fingerprint." + " " +
       +                                                                                                  "To calculate this yourself: '$ openssl x509 -noout -fingerprint -sha256 -inform pem -in mycertfile.crt'. Enter as 64 hex chars.")
            parser.add_argument("-1", "--oneserver", action="store_true", dest="oneserver", default=None, help="connect to one server only")
            parser.add_argument("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)")
            parser.add_argument("-p", "--proxy", dest="proxy", default=None, help="set proxy [type:]host[:port] (or 'none' to disable proxy), where type is socks4,socks5 or http")
   DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -183,6 +183,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                self.checking_accounts = False
                self.qr_window = None
                self.pluginsdialog = None
       +        self.showing_cert_mismatch_error = False
                self.tl_windows = []
                Logger.__init__(self)
        
       t@@ -267,7 +268,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                                 'banner', 'verified', 'fee', 'fee_histogram', 'on_quotes',
                                 'on_history', 'channel', 'channels_updated',
                                 'payment_failed', 'payment_succeeded',
       -                         'invoice_status', 'request_status', 'ln_gossip_sync_progress']
       +                         'invoice_status', 'request_status', 'ln_gossip_sync_progress',
       +                         'cert_mismatch']
                    # To avoid leaking references to "self" that prevent the
                    # window from being GC-ed when closed, callbacks should be
                    # methods of this class only, and specifically not be
       t@@ -442,6 +444,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    self.history_model.on_fee_histogram()
                elif event == 'ln_gossip_sync_progress':
                    self.update_lightning_icon()
       +        elif event == 'cert_mismatch':
       +            self.show_cert_mismatch_error()
                else:
                    self.logger.info(f"unexpected network event: {event} {args}")
        
       t@@ -3119,3 +3123,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                             "to see it, you need to broadcast it."))
                    win.msg_box(QPixmap(icon_path("offline_tx.png")), None, _('Success'), msg)
                    return True
       +
       +    def show_cert_mismatch_error(self):
       +        if self.showing_cert_mismatch_error:
       +            return
       +        self.showing_cert_mismatch_error = True
       +        self.show_critical(title=_("Certificate mismatch"),
       +                           msg=_("The SSL certificate provided by the main server did not match the fingerprint passed in with the --serverfingerprint option.") + "\n\n" +
       +                               _("Electrum will now exit."))
       +        self.showing_cert_mismatch_error = False
       +        self.close()
   DIR diff --git a/electrum/interface.py b/electrum/interface.py
       t@@ -34,6 +34,7 @@ from collections import defaultdict
        from ipaddress import IPv4Network, IPv6Network, ip_address, IPv6Address
        import itertools
        import logging
       +import hashlib
        
        import aiorpcx
        from aiorpcx import TaskGroup
       t@@ -190,6 +191,8 @@ class RequestCorrupted(GracefulDisconnect): pass
        
        class ErrorParsingSSLCert(Exception): pass
        class ErrorGettingSSLCertFromServer(Exception): pass
       +class ErrorSSLCertFingerprintMismatch(Exception): pass
       +class InvalidOptionCombination(Exception): pass
        class ConnectError(NetworkException): pass
        
        
       t@@ -350,6 +353,8 @@ class Interface(Logger):
            async def _try_saving_ssl_cert_for_first_time(self, ca_ssl_context):
                ca_signed = await self.is_server_ca_signed(ca_ssl_context)
                if ca_signed:
       +            if self.get_expected_fingerprint():
       +                raise InvalidOptionCombination("cannot use --serverfingerprint with CA signed servers")
                    with open(self.cert_path, 'w') as f:
                        # empty file means this is CA signed, not self-signed
                        f.write('')
       t@@ -362,6 +367,8 @@ class Interface(Logger):
                with open(self.cert_path, 'r') as f:
                    contents = f.read()
                if contents == '':  # CA signed
       +            if self.get_expected_fingerprint():
       +                raise InvalidOptionCombination("cannot use --serverfingerprint with CA signed servers")
                    return True
                # pinned self-signed cert
                try:
       t@@ -376,11 +383,12 @@ class Interface(Logger):
                    raise ErrorParsingSSLCert(e) from e
                try:
                    x.check_date()
       -            return True
                except x509.CertificateError as e:
                    self.logger.info(f"certificate has expired: {e}")
                    os.unlink(self.cert_path)  # delete pinned cert only in this case
                    return False
       +        self.verify_certificate_fingerprint(bytearray(b))
       +        return True
        
            async def _get_ssl_context(self):
                if self.protocol != 's':
       t@@ -468,6 +476,7 @@ class Interface(Logger):
                        dercert = await self.get_certificate()
                        if dercert:
                            self.logger.info("succeeded in getting cert")
       +                    self.verify_certificate_fingerprint(dercert)
                            with open(self.cert_path, 'w') as f:
                                cert = ssl.DER_cert_to_PEM_cert(dercert)
                                # workaround android bug
       t@@ -492,6 +501,21 @@ class Interface(Logger):
                    ssl_object = asyncio_transport.get_extra_info("ssl_object")  # type: ssl.SSLObject
                    return ssl_object.getpeercert(binary_form=True)
        
       +    def get_expected_fingerprint(self):
       +        if self.is_main_server():
       +            return self.network.config.get("serverfingerprint")
       +
       +    def verify_certificate_fingerprint(self, certificate):
       +        expected_fingerprint = self.get_expected_fingerprint()
       +        if not expected_fingerprint:
       +            return
       +        fingerprint = hashlib.sha256(certificate).hexdigest()
       +        fingerprints_match = fingerprint.lower() == expected_fingerprint.lower()
       +        if not fingerprints_match:
       +            util.trigger_callback('cert_mismatch')
       +            raise ErrorSSLCertFingerprintMismatch('Refusing to connect to server due to cert fingerprint mismatch')
       +        self.logger.info("cert fingerprint verification passed")
       +
            async def get_block_header(self, height, assert_mode):
                self.logger.info(f'requesting block header {height} in mode {assert_mode}')
                # use lower timeout as we usually have network.bhi_lock here