From: W. Trevor King Date: Fri, 7 Mar 2014 04:21:23 +0000 (-0800) Subject: irkerd: Initial SSL/TLS implementation X-Git-Tag: 2.7~9 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=a82724f;p=irker.git irkerd: Initial SSL/TLS implementation This is pretty basic, just using as much of Python's ssl module as the host Python implementation supports. I also added error-level logging of IRCServerConnectionError instances, to get helpful messages like: Invalid SSL/TLS certificate: hostname 'localhost' doesn't match 'irc.example.net' and: Couldn't connect to socket: _ssl.c:334: No root certificates specified for verification of other-side certificates. Important milestones in the standard library's ssl module: * Python 2.5 [1,2]: No ssl module at all * Python 2.6 [1,2]: ssl module added * Python 3.2 [3,4]: ssl.SSLContext class added, with SSLContext.set_default_verify_paths [4]. ssl.match_hostname is also added [5], which can be used with the existing getpeercert [6] to ensure the server certificate belongs to the target host. So for full verification, we need Python 3.2. We can scrape by with 2.6 and later, by manually supplying a ca_certs path and ignoring hostname mismatches. That's more succeptible to man-in-the-middle attacks, but still better than sending server, nick, and channel passwords in plaintext. [1]: http://docs.python.org/2/library/ssl.html [2]: http://docs.python.org/2/whatsnew/2.6.html#improved-ssl-support [3]: http://docs.python.org/3/whatsnew/3.2.html#ssl [4]: http://docs.python.org/3/library/ssl.html#ssl.SSLContext.set_default_verify_paths [5]: http://docs.python.org/3/library/ssl.html#ssl.match_hostname [6]: http://docs.python.org/2/library/ssl.html#ssl.SSLSocket.getpeercert --- diff --git a/NEWS b/NEWS index ceacfb1..18c6859 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,8 @@ irker history +2.7 @ unreleased + Add support for ircs:// and SSL/TLS connections to IRC servers. + 2.6 @ 2014-02-04 Fix for an infinite loop on failing to connect to IRC diff --git a/irkerd b/irkerd index 7f12c58..adcde8b 100755 --- a/irkerd +++ b/irkerd @@ -58,6 +58,7 @@ try: # Python 3 import socketserver except ImportError: # Python 2 import SocketServer as socketserver +import ssl import sys import threading import time @@ -230,8 +231,40 @@ class IRCServerConnection(): self.master = master self.socket = None + def _wrap_socket(self, socket, target, cafile=None, + protocol=ssl.PROTOCOL_TLSv1): + try: # Python 3.2 and greater + ssl_context = ssl.SSLContext(protocol) + except AttributeError: # Python < 3.2 + self.socket = ssl.wrap_socket( + self.socket, cert_reqs=ssl.CERT_REQUIRED, + ssl_version=protocol, ca_certs=cafile) + else: + ssl_context.verify_mode = ssl.CERT_REQUIRED + if cafile: + ssl_context.load_verify_locations(cafile=cafile) + else: + ssl_context.set_default_verify_paths() + kwargs = {} + if ssl.HAS_SNI: + kwargs['server_hostname'] = target.servername + self.socket = ssl_context.wrap_socket(self.socket, **kwargs) + return self.socket + + def _check_hostname(self, target): + if hasattr(ssl, 'match_hostname'): # Python >= 3.2 + cert = self.socket.getpeercert() + try: + ssl.match_hostname(cert, target.servername) + except ssl.CertificateError as e: + raise IRCServerConnectionError( + 'Invalid SSL/TLS certificate: %s' % e) + else: # Python < 3.2 + LOG.warning( + 'cannot check SSL/TLS hostname with Python %s' % sys.version) + def connect(self, target, nickname, - password=None, username=None, ircname=None): + password=None, username=None, ircname=None, **kwargs): LOG.debug("connect(server=%r, port=%r, nickname=%r, ...)" % ( target.servername, target.port, nickname)) if self.socket is not None: @@ -244,11 +277,16 @@ class IRCServerConnection(): self.nickname = nickname try: self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + if target.ssl: + self.socket = self._wrap_socket( + socket=self.socket, target=target, **kwargs) self.socket.bind(('', 0)) self.socket.connect((target.servername, target.port)) except socket.error as err: raise IRCServerConnectionError("Couldn't connect to socket: %s" % err) + if target.ssl: + self._check_hostname(target=target) if password: self.ship("PASS " + password) self.nick(self.nickname) @@ -537,7 +575,8 @@ class Connection: self.target, time.asctime())) self.last_xmit = time.time() self.last_ping = time.time() - except IRCServerConnectionError: + except IRCServerConnectionError as e: + LOG.error(e) self.status = "expired" break elif self.status == "handshaking": @@ -631,9 +670,14 @@ class Target(): def __init__(self, url): self.url = url parsed = urllib_parse.urlparse(url) + self.ssl = parsed.scheme == 'ircs' + if self.ssl: + default_ircport = 6697 + else: + default_ircport = 6667 irchost, _, ircport = parsed.netloc.partition(':') if not ircport: - ircport = 6667 + ircport = default_ircport self.servername = irchost # IRC channel names are case-insensitive. If we don't smash # case here we may run into problems later. There was a bug @@ -894,6 +938,9 @@ class IrkerUDPHandler(socketserver.BaseRequestHandler): if __name__ == '__main__': parser = argparse.ArgumentParser( description=__doc__.strip().splitlines()[0]) + parser.add_argument( + '-c', '--ca-file', metavar='PATH', + help='file of trusted certificates for SSL/TLS') parser.add_argument( '-d', '--log-level', metavar='LEVEL', choices=LOG_LEVELS, help='file of trusted certificates for SSL/TLS') @@ -925,6 +972,7 @@ if __name__ == '__main__': nick_template=args.nick, nick_needs_number=re.search('%.*d', args.nick), password=args.password, + cafile=args.ca_file, ) LOG.info("irkerd version %s" % version) if args.immediate: diff --git a/irkerd.xml b/irkerd.xml index c2c37fb..729c4f4 100644 --- a/irkerd.xml +++ b/irkerd.xml @@ -18,6 +18,7 @@ irkerd + -c ca-file -d debuglevel -l logfile -n nick @@ -52,7 +53,7 @@ Examples: {"to":"irc://chat.freenode.net/git-ciabot", "privmsg":"Hello, world!"} {"to":["irc://chat.freenode.net/#git-ciabot","irc://chat.freenode.net/#gpsd"],"privmsg":"Multichannel test"} {"to":"irc://chat.hypothetical.net:6668/git-ciabot", "privmsg":"Hello, world!"} -{"to":"irc://chat.hypothetical.net:6668/git-private?key=topsecret", "privmsg":"Keyed channel test"} +{"to":"ircs://chat.hypothetical.net/git-private?key=topsecret", "privmsg":"Keyed channel test"} If the channel part of the URL does not have one of the prefix @@ -63,8 +64,24 @@ shipping - unlessthe channel part has the suffix The host part of the URL may have a port-number suffix separated by a colon, as shown in the third example; otherwise -irkerd sends messages to the the default 6667 IRC -port of each server. +irkerd sends plaintext messages to the default +6667 IRC port of each server, and SSL/TLS messages to 6697. + +When the to URL uses the ircs +scheme (as shown in the fourth and fifth examples), the connection to +the server is made via SSL/TLS (vs. a plaintext connection with the +irc scheme). To connect via SSL/TLS with Python 2.x, +you need to explicitly declare the certificate authority file used to +verify server certificates. For example, -c +/etc/ssl/certs/ca-certificates.crt. In Python 3.2 and later, +you can still set this option to declare a custom CA file, but +irkerd; if you don't set it +irkerd will use OpenSSL's default file +(using Python's +ssl.SSLContext.set_default_verify_paths). In Python +3.2 and later, ssl.match_hostname is used to ensure the +server certificate belongs to the intended host, as well as being +signed by a trusted CA. To join password-protected (mode +k) channels, the channel part of the URL may be followed with a query-string indicating the channel key, of the