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)