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
This commit is contained in:
@@ -1268,6 +1268,8 @@ argparse._SubParsersAction.__call__ = subparser_call
|
|||||||
|
|
||||||
|
|
||||||
def add_network_options(parser):
|
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("-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("-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")
|
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")
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||||||
self.checking_accounts = False
|
self.checking_accounts = False
|
||||||
self.qr_window = None
|
self.qr_window = None
|
||||||
self.pluginsdialog = None
|
self.pluginsdialog = None
|
||||||
|
self.showing_cert_mismatch_error = False
|
||||||
self.tl_windows = []
|
self.tl_windows = []
|
||||||
Logger.__init__(self)
|
Logger.__init__(self)
|
||||||
|
|
||||||
@@ -267,7 +268,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||||||
'banner', 'verified', 'fee', 'fee_histogram', 'on_quotes',
|
'banner', 'verified', 'fee', 'fee_histogram', 'on_quotes',
|
||||||
'on_history', 'channel', 'channels_updated',
|
'on_history', 'channel', 'channels_updated',
|
||||||
'payment_failed', 'payment_succeeded',
|
'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
|
# To avoid leaking references to "self" that prevent the
|
||||||
# window from being GC-ed when closed, callbacks should be
|
# window from being GC-ed when closed, callbacks should be
|
||||||
# methods of this class only, and specifically not be
|
# methods of this class only, and specifically not be
|
||||||
@@ -442,6 +444,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||||||
self.history_model.on_fee_histogram()
|
self.history_model.on_fee_histogram()
|
||||||
elif event == 'ln_gossip_sync_progress':
|
elif event == 'ln_gossip_sync_progress':
|
||||||
self.update_lightning_icon()
|
self.update_lightning_icon()
|
||||||
|
elif event == 'cert_mismatch':
|
||||||
|
self.show_cert_mismatch_error()
|
||||||
else:
|
else:
|
||||||
self.logger.info(f"unexpected network event: {event} {args}")
|
self.logger.info(f"unexpected network event: {event} {args}")
|
||||||
|
|
||||||
@@ -3119,3 +3123,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||||||
"to see it, you need to broadcast it."))
|
"to see it, you need to broadcast it."))
|
||||||
win.msg_box(QPixmap(icon_path("offline_tx.png")), None, _('Success'), msg)
|
win.msg_box(QPixmap(icon_path("offline_tx.png")), None, _('Success'), msg)
|
||||||
return True
|
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()
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ from collections import defaultdict
|
|||||||
from ipaddress import IPv4Network, IPv6Network, ip_address, IPv6Address
|
from ipaddress import IPv4Network, IPv6Network, ip_address, IPv6Address
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
|
import hashlib
|
||||||
|
|
||||||
import aiorpcx
|
import aiorpcx
|
||||||
from aiorpcx import TaskGroup
|
from aiorpcx import TaskGroup
|
||||||
@@ -190,6 +191,8 @@ class RequestCorrupted(GracefulDisconnect): pass
|
|||||||
|
|
||||||
class ErrorParsingSSLCert(Exception): pass
|
class ErrorParsingSSLCert(Exception): pass
|
||||||
class ErrorGettingSSLCertFromServer(Exception): pass
|
class ErrorGettingSSLCertFromServer(Exception): pass
|
||||||
|
class ErrorSSLCertFingerprintMismatch(Exception): pass
|
||||||
|
class InvalidOptionCombination(Exception): pass
|
||||||
class ConnectError(NetworkException): pass
|
class ConnectError(NetworkException): pass
|
||||||
|
|
||||||
|
|
||||||
@@ -350,6 +353,8 @@ class Interface(Logger):
|
|||||||
async def _try_saving_ssl_cert_for_first_time(self, ca_ssl_context):
|
async def _try_saving_ssl_cert_for_first_time(self, ca_ssl_context):
|
||||||
ca_signed = await self.is_server_ca_signed(ca_ssl_context)
|
ca_signed = await self.is_server_ca_signed(ca_ssl_context)
|
||||||
if ca_signed:
|
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:
|
with open(self.cert_path, 'w') as f:
|
||||||
# empty file means this is CA signed, not self-signed
|
# empty file means this is CA signed, not self-signed
|
||||||
f.write('')
|
f.write('')
|
||||||
@@ -362,6 +367,8 @@ class Interface(Logger):
|
|||||||
with open(self.cert_path, 'r') as f:
|
with open(self.cert_path, 'r') as f:
|
||||||
contents = f.read()
|
contents = f.read()
|
||||||
if contents == '': # CA signed
|
if contents == '': # CA signed
|
||||||
|
if self.get_expected_fingerprint():
|
||||||
|
raise InvalidOptionCombination("cannot use --serverfingerprint with CA signed servers")
|
||||||
return True
|
return True
|
||||||
# pinned self-signed cert
|
# pinned self-signed cert
|
||||||
try:
|
try:
|
||||||
@@ -376,11 +383,12 @@ class Interface(Logger):
|
|||||||
raise ErrorParsingSSLCert(e) from e
|
raise ErrorParsingSSLCert(e) from e
|
||||||
try:
|
try:
|
||||||
x.check_date()
|
x.check_date()
|
||||||
return True
|
|
||||||
except x509.CertificateError as e:
|
except x509.CertificateError as e:
|
||||||
self.logger.info(f"certificate has expired: {e}")
|
self.logger.info(f"certificate has expired: {e}")
|
||||||
os.unlink(self.cert_path) # delete pinned cert only in this case
|
os.unlink(self.cert_path) # delete pinned cert only in this case
|
||||||
return False
|
return False
|
||||||
|
self.verify_certificate_fingerprint(bytearray(b))
|
||||||
|
return True
|
||||||
|
|
||||||
async def _get_ssl_context(self):
|
async def _get_ssl_context(self):
|
||||||
if self.protocol != 's':
|
if self.protocol != 's':
|
||||||
@@ -468,6 +476,7 @@ class Interface(Logger):
|
|||||||
dercert = await self.get_certificate()
|
dercert = await self.get_certificate()
|
||||||
if dercert:
|
if dercert:
|
||||||
self.logger.info("succeeded in getting cert")
|
self.logger.info("succeeded in getting cert")
|
||||||
|
self.verify_certificate_fingerprint(dercert)
|
||||||
with open(self.cert_path, 'w') as f:
|
with open(self.cert_path, 'w') as f:
|
||||||
cert = ssl.DER_cert_to_PEM_cert(dercert)
|
cert = ssl.DER_cert_to_PEM_cert(dercert)
|
||||||
# workaround android bug
|
# workaround android bug
|
||||||
@@ -492,6 +501,21 @@ class Interface(Logger):
|
|||||||
ssl_object = asyncio_transport.get_extra_info("ssl_object") # type: ssl.SSLObject
|
ssl_object = asyncio_transport.get_extra_info("ssl_object") # type: ssl.SSLObject
|
||||||
return ssl_object.getpeercert(binary_form=True)
|
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):
|
async def get_block_header(self, height, assert_mode):
|
||||||
self.logger.info(f'requesting block header {height} in mode {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
|
# use lower timeout as we usually have network.bhi_lock here
|
||||||
|
|||||||
Reference in New Issue
Block a user