[PATCH v2 2/3] Implement SSL certificate checking
Sebastian
Sebastian at SSpaeth.de
Thu Dec 16 12:43:47 GMT 2010
Previously, we did not check at all the authenticy and validity of
the SSL server we connected to. This is bad as it allows
man-in-the-middle attacks etc. This patch remedies the situation
somewhat.
If we specify a sslcacertfile= setting in the Repository section,
validate the server cert (on python>=2.6 or abort with python<=2.5).
As before, no certificate check is performed without that option.
In the future, the hostname check should be made optional and also
a mutt-lick "accept this certificate forever" thing should be
implemented.
Signed-off-By: Sebastian Spaeth <Sebastian at SSpaeth.de>
---
offlineimap.conf | 6 ++
offlineimap/imaplibutil.py | 171 +++++++++++++++++++++++++++-------------
offlineimap/imapserver.py | 11 ++-
offlineimap/repository/IMAP.py | 3 +
4 files changed, 134 insertions(+), 57 deletions(-)
diff --git a/offlineimap.conf b/offlineimap.conf
index 714b3ec..bada76d 100644
--- a/offlineimap.conf
+++ b/offlineimap.conf
@@ -278,6 +278,12 @@ ssl = yes
# SSL Client key (optional)
# sslclientkey = /path/to/file.key
+# SSL CA Cert(s) to verify the server cert against (optional).
+# No SSL verification is done without this option, if it is
+# specified, the CA Cert(s) need to verify the Server cert AND
+# match the hostname (* wildcard allowed on the left hand side)
+# sslcacertcertfile = /path/to/cacertfile.crt
+
# Specify the port. If not specified, use a default port.
# remoteport = 993
diff --git a/offlineimap/imaplibutil.py b/offlineimap/imaplibutil.py
index bb623a1..9e6eece 100644
--- a/offlineimap/imaplibutil.py
+++ b/offlineimap/imaplibutil.py
@@ -1,7 +1,6 @@
# imaplib utilities
-# Copyright (C) 2002-2007 John Goerzen
-# <jgoerzen at complete.org>
-#
+# Copyright (C) 2002-2007 John Goerzen <jgoerzen at complete.org>
+# 2010 Sebastian Spaeth <Sebastian at SSpaeth.de>
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
@@ -25,9 +24,9 @@ from imaplib import IMAP4_PORT, IMAP4_SSL_PORT, InternalDate, Mon2num
try:
import ssl
- ssl_wrap = ssl.wrap_socket
except ImportError:
- ssl_wrap = socket.ssl
+ #fails on python <2.6
+ pass
class IMAP4_Tunnel(IMAP4):
"""IMAP4 client class over a tunnel
@@ -62,45 +61,7 @@ class IMAP4_Tunnel(IMAP4):
self.infd.close()
self.outfd.close()
self.process.wait()
-
-class sslwrapper:
- def __init__(self, sslsock):
- self.sslsock = sslsock
- self.readbuf = ''
-
- def write(self, s):
- return self.sslsock.write(s)
- def _read(self, n):
- return self.sslsock.read(n)
-
- def read(self, n):
- if len(self.readbuf):
- # Return the stuff in readbuf, even if less than n.
- # It might contain the rest of the line, and if we try to
- # read more, might block waiting for data that is not
- # coming to arrive.
- bytesfrombuf = min(n, len(self.readbuf))
- retval = self.readbuf[:bytesfrombuf]
- self.readbuf = self.readbuf[bytesfrombuf:]
- return retval
- retval = self._read(n)
- if len(retval) > n:
- self.readbuf = retval[n:]
- return retval[:n]
- return retval
-
- def readline(self):
- retval = ''
- while 1:
- linebuf = self.read(1024)
- nlindex = linebuf.find("\n")
- if nlindex != -1:
- retval += linebuf[:nlindex + 1]
- self.readbuf = linebuf[nlindex + 1:] + self.readbuf
- return retval
- else:
- retval += linebuf
def new_mesg(self, s, secs=None):
if secs is None:
@@ -109,14 +70,27 @@ def new_mesg(self, s, secs=None):
UIBase.getglobalui().debug('imap', ' %s.%02d %s' % (tm, (secs*100)%100, s))
class WrappedIMAP4_SSL(IMAP4_SSL):
+ """Provides an improved version of the standard IMAP4_SSL
+
+ It provides a better readline() implementation as impaplib's
+ readline() is extremly inefficient. It can also connect to IPv6
+ addresses."""
+ def __init__(self, *args, **kwargs):
+ self._readbuf = ''
+ self._cacertfile = kwargs.get('cacertfile', None)
+ if kwargs.has_key('cacertfile'):
+ del kwargs['cacertfile']
+ IMAP4_SSL.__init__(self, *args, **kwargs)
+
def open(self, host = '', port = IMAP4_SSL_PORT):
- """Setup connection to remote server on "host:port".
- (default: localhost:standard IMAP4 SSL port).
- This connection will be used by the routines:
- read, readline, send, shutdown.
- """
+ """Do whatever IMAP4_SSL would do in open, but call sslwrap
+ with cert verification"""
+ #IMAP4_SSL.open(self, host, port) uses the below 2 lines:
self.host = host
self.port = port
+
+ #rather than just self.sock = socket.create_connection((host, port))
+ #we use the below part to be able to connect to ipv6 addresses too
#This connects to the first ip found ipv4/ipv6
#Added by Adriaan Peeters <apeeters at lashout.net> based on a socket
#example from the python documentation:
@@ -136,11 +110,102 @@ class WrappedIMAP4_SSL(IMAP4_SSL):
if last_error != 0:
# FIXME
raise socket.error(last_error)
- self.sslobj = ssl_wrap(self.sock, self.keyfile, self.certfile)
- self.sslobj = sslwrapper(self.sslobj)
+
+ #connected to socket, now wrap it in SSL
+ try:
+ if self._cacertfile:
+ requirecert = ssl.CERT_REQUIRED
+ else:
+ requirecert = ssl.CERT_NONE
+
+ self.sslobj = ssl.wrap_socket(self.sock, self.keyfile,
+ self.certfile,
+ ca_certs = self._cacertfile,
+ cert_reqs = requirecert)
+ except NameError:
+ #Python 2.4/2.5 don't have the ssl module, we need to
+ #socket.ssl() here but that doesn't allow cert
+ #verification!!!
+ if self._cacertfile:
+ #user configured a CA certificate, but python 2.4/5 doesn't
+ #allow us to easily check it. So bail out here.
+ raise Exception("SSL CA Certificates cannot be checked with python <=2.6. Abort")
+ self.sslobj = socket.ssl(self.sock, self.keyfile,
+ self.certfile)
+
+ else:
+ #ssl.wrap_socket worked and cert is verified, now check
+ #that hostnames also match.
+ error = self._verifycert(self.sslobj.getpeercert(), host)
+ if error:
+ raise ssl.SSLError("SSL Certificate host name mismatch: %s" % error)
+
+ #TODO: Done for now. We should implement a mutt-like behavior
+ #that offers the users to accept a certificate (presenting a
+ #fingerprint of it) (get via self.sslobj.getpeercert()), and
+ #save that, and compare on future connects, rather than having
+ #to trust what the CA certs say.
+
+ def _verifycert(self, cert, hostname):
+ '''Verify that cert (in socket.getpeercert() format) matches hostname.
+ CRLs and subjectAltName are not handled.
+
+ Returns error message if any problems are found and None on success.
+ '''
+ if not cert:
+ return ('no certificate received')
+ dnsname = hostname.lower()
+ for s in cert.get('subject', []):
+ key, value = s[0]
+ if key == 'commonName':
+ certname = value.lower()
+ if (certname == dnsname or
+ '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1]):
+ return None
+ return ('certificate is for %s') % certname
+ return ('no commonName found in certificate')
+
+ def _read_upto (self, n):
+ """Read up to n bytes, emptying existing _readbuffer first"""
+ bytesfrombuf = min(n, len(self._readbuf))
+ if bytesfrombuf:
+ # Return the stuff in readbuf, even if less than n.
+ # It might contain the rest of the line, and if we try to
+ # read more, might block waiting for data that is not
+ # coming to arrive.
+ retval = self._readbuf[:bytesfrombuf]
+ self._readbuf = self._readbuf[bytesfrombuf:]
+ return retval
+ return self.sslobj.read(min(n, 16384))
+
+ def read(self, n):
+ """Read exactly n bytes
+
+ As done in IMAP4_SSL.read() API. If read returns less than n
+ bytes, things break left and right."""
+ chunks = []
+ read = 0
+ while read < n:
+ data = self._read_upto (n-read)
+ read += len(data)
+ chunks.append(data)
+
+ return ''.join(chunks)
def readline(self):
- return self.sslobj.readline()
+ """Get the next line. This implementation is more efficient
+ than IMAP4_SSL.readline() which reads one char at a time and
+ reassembles the string by appending those chars. Uggh."""
+ retval = ''
+ while 1:
+ linebuf = self._read_upto(1024)
+ nlindex = linebuf.find("\n")
+ if nlindex != -1:
+ retval += linebuf[:nlindex + 1]
+ self._readbuf = linebuf[nlindex + 1:] + self._readbuf
+ return retval
+ else:
+ retval += linebuf
class WrappedIMAP4(IMAP4):
@@ -149,9 +214,8 @@ class WrappedIMAP4(IMAP4):
def open(self, host = '', port = IMAP4_PORT):
"""Setup connection to remote server on "host:port"
(default: localhost:standard IMAP4 port).
- This connection will be used by the routines:
- read, readline, send, shutdown.
"""
+ #self.host and self.port are needed by the parent IMAP4 class
self.host = host
self.port = port
res = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
@@ -174,7 +238,6 @@ class WrappedIMAP4(IMAP4):
raise socket.error(last_error)
self.file = self.sock.makefile('rb')
-
mustquote = re.compile(r"[^\w!#$%&'+,.:;<=>?^`|~-]")
def Internaldate2epoch(resp):
diff --git a/offlineimap/imapserver.py b/offlineimap/imapserver.py
index cf1fdc9..0029800 100644
--- a/offlineimap/imapserver.py
+++ b/offlineimap/imapserver.py
@@ -100,7 +100,8 @@ class IMAPServer:
def __init__(self, config, reposname,
username = None, password = None, hostname = None,
port = None, ssl = 1, maxconnections = 1, tunnel = None,
- reference = '""', sslclientcert = None, sslclientkey = None):
+ reference = '""', sslclientcert = None, sslclientkey = None,
+ sslcacertfile= None):
self.reposname = reposname
self.config = config
self.username = username
@@ -113,6 +114,7 @@ class IMAPServer:
self.usessl = ssl
self.sslclientcert = sslclientcert
self.sslclientkey = sslclientkey
+ self.sslcacertfile = sslcacertfile
self.delim = None
self.root = None
if port == None:
@@ -253,7 +255,8 @@ class IMAPServer:
elif self.usessl:
UIBase.getglobalui().connecting(self.hostname, self.port)
imapobj = UsefulIMAP4_SSL(self.hostname, self.port,
- self.sslclientkey, self.sslclientcert)
+ self.sslclientkey, self.sslclientcert,
+ cacertfile = self.sslcacertfile)
else:
UIBase.getglobalui().connecting(self.hostname, self.port)
imapobj = UsefulIMAP4(self.hostname, self.port)
@@ -414,6 +417,7 @@ class ConfigedIMAPServer(IMAPServer):
ssl = self.repos.getssl()
sslclientcert = self.repos.getsslclientcert()
sslclientkey = self.repos.getsslclientkey()
+ sslcacertfile = self.repos.getsslcacertfile()
reference = self.repos.getreference()
server = None
password = None
@@ -435,4 +439,5 @@ class ConfigedIMAPServer(IMAPServer):
self.repos.getmaxconnections(),
reference = reference,
sslclientcert = sslclientcert,
- sslclientkey = sslclientkey)
+ sslclientkey = sslclientkey,
+ sslcacertfile = sslcacertfile)
diff --git a/offlineimap/repository/IMAP.py b/offlineimap/repository/IMAP.py
index 52a8e61..3bfa5db 100644
--- a/offlineimap/repository/IMAP.py
+++ b/offlineimap/repository/IMAP.py
@@ -139,6 +139,9 @@ class IMAPRepository(BaseRepository):
def getsslclientkey(self):
return self.getconf('sslclientkey', None)
+ def getsslcacertfile(self):
+ return self.getconf('sslcacertfile', None)
+
def getpreauthtunnel(self):
return self.getconf('preauthtunnel', None)
--
1.7.1
More information about the OfflineIMAP-project
mailing list