From 8800fa37a3f75506fb7ab57d70ea3579111ff5ce Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 12 Sep 2011 09:50:41 +0200 Subject: [PATCH] Implement Server SSL fingerprint check If we connect to a SSL server (not STARTTLS) and no CA cert has been specified for verification, we check the configured SSL fingerprint and bail out in case it has not been set yet, or it does not match. This means one more mandatory option for SSL configuration, but it improves security a lot. Signed-off-by: Sebastian Spaeth Signed-off-by: Nicolas Sebrecht --- Changelog.draft.rst | 4 ++++ offlineimap.conf | 10 ++++++++++ offlineimap/imaplibutil.py | 22 ++++++++++++++++++++-- offlineimap/imapserver.py | 2 ++ offlineimap/repository/IMAP.py | 3 +++ 5 files changed, 39 insertions(+), 2 deletions(-) diff --git a/Changelog.draft.rst b/Changelog.draft.rst index 74b6368..edd0f75 100644 --- a/Changelog.draft.rst +++ b/Changelog.draft.rst @@ -17,6 +17,10 @@ New Features synchronization, but only skip that message, informing the user at the end of the sync run. +* If you connect via ssl and 'cert_fingerprint' is configured, we check + that the server certificate is actually known and identical by + comparing the stored sha1 fingerprint with the current one. + Changes ------- diff --git a/offlineimap.conf b/offlineimap.conf index 12a09ba..2d5532d 100644 --- a/offlineimap.conf +++ b/offlineimap.conf @@ -327,6 +327,16 @@ ssl = yes # The certificate should be in PEM format. # sslcacertfile = /path/to/cacertfile.crt +# If you connect via SSL/TLS (ssl=true) and you have no CA certificate +# specified, offlineimap will refuse to sync as it connects to a server +# with an unknown "fingerprint". If you are sure you connect to the +# correct server, you can then configure the presented server +# fingerprint here. OfflineImap will verify that the server fingerprint +# has not changed on each connect and refuse to connect otherwise. +# You can also configure this in addition to CA certificate validation +# above and it will check both ways. cert_fingerprint = +# + # Specify the port. If not specified, use a default port. # remoteport = 993 diff --git a/offlineimap/imaplibutil.py b/offlineimap/imaplibutil.py index 529af6f..ca556f0 100644 --- a/offlineimap/imaplibutil.py +++ b/offlineimap/imaplibutil.py @@ -21,8 +21,10 @@ import re import socket import time import subprocess -from offlineimap.ui import getglobalui import threading +from hashlib import sha1 + +from offlineimap.ui import getglobalui from offlineimap import OfflineImapError from offlineimap.imaplib2 import IMAP4, IMAP4_SSL, zlib, IMAP4_PORT, InternalDate, Mon2num @@ -137,7 +139,23 @@ def new_mesg(self, s, tn=None, secs=None): class WrappedIMAP4_SSL(UsefulIMAPMixIn, IMAP4_SSL): """Improved version of imaplib.IMAP4_SSL overriding select()""" - pass + def __init__(self, *args, **kwargs): + self._fingerprint = kwargs.get('fingerprint', None) + if kwargs.has_key('fingerprint'): + del kwargs['fingerprint'] + super(WrappedIMAP4_SSL, self).__init__(*args, **kwargs) + + def open(self, host=None, port=None): + super(WrappedIMAP4_SSL, self).open(host, port) + if self._fingerprint or not self.ca_certs: + # compare fingerprints + fingerprint = sha1(self.sslobj.getpeercert(True)).hexdigest() + if fingerprint != self._fingerprint: + raise OfflineImapError("Server SSL fingerprint '%s' for hostnam" + "e '%s' does not match configured fingerprint. Please ver" + "ify and set 'cert_fingerprint' accordingly if not set ye" + "t." % (fingerprint, host), + OfflineImapError.ERROR.REPO) class WrappedIMAP4(UsefulIMAPMixIn, IMAP4): diff --git a/offlineimap/imapserver.py b/offlineimap/imapserver.py index 05d8111..5989ff1 100644 --- a/offlineimap/imapserver.py +++ b/offlineimap/imapserver.py @@ -209,6 +209,7 @@ class IMAPServer: success = 1 elif self.usessl: self.ui.connecting(self.hostname, self.port) + fingerprint = self.repos.get_ssl_fingerprint() imapobj = imaplibutil.WrappedIMAP4_SSL(self.hostname, self.port, self.sslclientkey, @@ -216,6 +217,7 @@ class IMAPServer: self.sslcacertfile, self.verifycert, timeout=socket.getdefaulttimeout(), + fingerprint=fingerprint ) else: self.ui.connecting(self.hostname, self.port) diff --git a/offlineimap/repository/IMAP.py b/offlineimap/repository/IMAP.py index 68eb637..76d0870 100644 --- a/offlineimap/repository/IMAP.py +++ b/offlineimap/repository/IMAP.py @@ -182,6 +182,9 @@ class IMAPRepository(BaseRepository): % (self.name, cacertfile)) return cacertfile + def get_ssl_fingerprint(self): + return self.getconf('cert_fingerprint', None) + def getpreauthtunnel(self): return self.getconf('preauthtunnel', None)