From e77e78e79eab8fb285debd4b8b9114ebf03d4ec7 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 21 Feb 2016 14:18:14 +0100 Subject: [PATCH] Add support for using server certificate fingerprint instead of CA signature. --- src/conf.py | 7 ++++++ src/drivers/Socket.py | 10 +++++---- src/utils/net.py | 50 +++++++++++++++++++++++++++++++------------ 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/conf.py b/src/conf.py index 79e14fc69..877a5a0dc 100644 --- a/src/conf.py +++ b/src/conf.py @@ -320,13 +320,20 @@ def registerNetwork(name, password='', ssl=True, sasl_username='', registerGlobalValue(network, 'channels', SpaceSeparatedSetOfChannels([], _("""Space-separated list of channels the bot will join only on %s.""") % name, private=True)) + registerGlobalValue(network, 'ssl', registry.Boolean(ssl, _("""Determines whether the bot will attempt to connect with SSL sockets to %s.""") % name)) + registerGlobalValue(network.ssl, 'serverFingerprints', + registry.SpaceSeparatedSetOfStrings([], _("""Space-separated list + of fingerprints of trusted certificates for this network. + If non-empty, Certification Authority signatures will not be used to + verify certificates."""))) registerGlobalValue(network, 'requireStarttls', registry.Boolean(False, _("""Determines whether the bot will connect in plain text to %s but require STARTTLS before authentication. This is ignored if the connection already uses SSL.""") % name)) + registerGlobalValue(network, 'certfile', registry.String('', _("""Determines what certificate file (if any) the bot will use to connect with SSL sockets to %s.""") % name)) diff --git a/src/drivers/Socket.py b/src/drivers/Socket.py index 2da19ee99..75bc7bd8d 100644 --- a/src/drivers/Socket.py +++ b/src/drivers/Socket.py @@ -351,8 +351,8 @@ class SocketDriver(drivers.IrcDriver, drivers.ServersMixin): def starttls(self): assert 'ssl' in globals() - certfile = getattr(conf.supybot.networks, self.irc.network) \ - .certfile() + network_config = getattr(conf.supybot.networks, self.irc.network) + certfile = network_config.certfile() if not certfile: certfile = conf.supybot.protocols.irc.certfile() if not certfile: @@ -365,12 +365,14 @@ class SocketDriver(drivers.IrcDriver, drivers.ServersMixin): self.conn = utils.net.ssl_wrap_socket(self.conn, logger=drivers.log, hostname=self.server[0], certfile=certfile, - verify_mode=conf.supybot.protocols.ssl.verifyMode()) + trusted_fingerprints=network_config.ssl.serverFingerprints(), + ) except ssl.CertificateError as e: drivers.log.critical(('Certificate validation failed when ' 'connecting to %s: %s\n' 'This means someone is doing a man-in-the-middle attack ' - 'on your connection.') + 'on your connection, or the server\'s certificate is ' + 'not in your trusted fingerprints list.') % (self.irc.network, e.args[0])) raise ssl.SSLError('Aborting because of failed certificate ' 'verification.') diff --git a/src/utils/net.py b/src/utils/net.py index d2880b198..0f7dac22a 100644 --- a/src/utils/net.py +++ b/src/utils/net.py @@ -35,6 +35,7 @@ Simple utility modules. import re import ssl import socket +import hashlib from .web import _ipAddr, _domain @@ -129,26 +130,47 @@ def isIPV6(s): return bruteIsIPV6(s) return False + +FINGERPRINT_ALGORITHMS = ('md5', 'sha1', 'sha224', 'sha256', 'sha384', + 'sha512') +def check_certificate_fingerprint(conn, trusted_fingerprints): + cert = conn.getpeercert(binary_form=True) + for algorithm in FINGERPRINT_ALGORITHMS: + h = hashlib.new(algorithm) + h.update(cert) + if h.hexdigest() in trusted_fingerprints: + return + raise ssl.CertificateError('No matching fingerprint.') + if hasattr(ssl, 'create_default_context'): def ssl_wrap_socket(conn, hostname, logger, certfile=None, - verify_mode=ssl.CERT_REQUIRED, **kwargs): + trusted_fingerprints=None, + **kwargs): context = ssl.create_default_context(**kwargs) - context.verify_mode = verify_mode + if trusted_fingerprints: + # Do not use Certification Authorities + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE if certfile: context.load_cert_chain(certfile) - return context.wrap_socket(conn, server_hostname=hostname) + conn = context.wrap_socket(conn, server_hostname=hostname) + if trusted_fingerprints: + check_certificate_fingerprint(conn, trusted_fingerprints) + return conn else: - def ssl_wrap_socket(conn, hostname, logger, certfile=None, ca_certs=None, - verify_mode=ssl.CERT_REQUIRED): - logger.critical('This Python version does not support SSL contexts, ' - 'which makes your connection vulnerable to man-in-the-middle ' - 'attacks. You should consider upgrading to Python 3 ' - '(or at least 2.7.9).') + def ssl_wrap_socket(conn, hostname, logger, certfile=None, + ca_certs=None, trusted_fingerprints=None): # TLSv1.0 is the only TLS version Python < 2.7.9 supports # (besides SSLv2 and v3, which are known to be insecure) - return ssl.wrap_socket(conn, certfile=certfile, ca_certs=ca_certs, - ssl_version=ssl.ssl.PROTOCOL_TLSv1, verify_mode=verify_mode) - - - + conn = ssl.wrap_socket(conn, certfile=certfile, ca_certs=ca_certs, + ssl_version=ssl.ssl.PROTOCOL_TLSv1, verify_mode=ssl.CERT_NONE) + if trusted_fingerprints: + check_certificate_fingerprint(conn, trusted_fingerprints) + else: + logger.critical('This Python version does not support SSL/TLS ' + 'certification authority verification, which makes your ' + 'connection vulnerable to man-in-the-middle attacks.' + 'You should consider upgrading to Python 3 ' + '(or at least 2.7.9).') + return conn # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: