diff --git a/offlineimap.conf b/offlineimap.conf index e0ef4f4..cfafb23 100644 --- a/offlineimap.conf +++ b/offlineimap.conf @@ -497,6 +497,17 @@ remotehost = examplehost # # Tilde and environment variable expansions will be performed. # +# Special value OS-DEFAULT makes OfflineIMAP to automatically +# determine system-wide location of standard trusted CA roots file +# for known OS distributions and use the first bundle encountered +# (if any). If no system-wide CA bundle is found, OfflineIMAP +# will refuse to continue; this is done to prevent creation +# of false security expectations ("I had configured CA bundle, +# thou certificate verification shalt be present"). +# +# You can also use fingerprint verification via cert_fingerprint. +# See below for more verbose explanation. +# #sslcacertfile = /path/to/cacertfile.crt @@ -506,10 +517,13 @@ remotehost = examplehost # 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 connection and refuse to connect otherwise. -# You can also configure this in addition to CA certificate validation -# above and it will check both ways. +# fingerprint here. OfflineIMAP will verify that the server fingerprint +# has not changed on each connect and refuse to connect otherwise. +# +# You can also configure fingerprint validation in addition to +# CA certificate validation above and it will check both: +# OfflineIMAP fill verify certificate first and if things will be fine, +# fingerprint will be validated. # # Multiple fingerprints can be specified, separated by commas. # diff --git a/offlineimap/imapserver.py b/offlineimap/imapserver.py index bac681e..1275092 100644 --- a/offlineimap/imapserver.py +++ b/offlineimap/imapserver.py @@ -81,9 +81,10 @@ class IMAPServer: self.sslclientcert = repos.getsslclientcert() self.sslclientkey = repos.getsslclientkey() self.sslcacertfile = repos.getsslcacertfile() - self.sslversion = repos.getsslversion() if self.sslcacertfile is None: self.__verifycert = None # disable cert verification + self.fingerprint = repos.get_ssl_fingerprint() + self.sslversion = repos.getsslversion() self.delim = None self.root = None @@ -394,7 +395,6 @@ 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, @@ -403,7 +403,7 @@ class IMAPServer: self.__verifycert, self.sslversion, timeout=socket.getdefaulttimeout(), - fingerprint=fingerprint + fingerprint=self.fingerprint ) else: self.ui.connecting(self.hostname, self.port) @@ -468,7 +468,7 @@ class IMAPServer: (self.hostname, self.repos) raise OfflineImapError(reason, severity), None, exc_info()[2] - elif isinstance(e, SSLError) and e.errno == 1: + elif isinstance(e, SSLError) and e.errno == errno.EPERM: # SSL unknown protocol error # happens e.g. when connecting via SSL to a non-SSL service if self.port != 993: diff --git a/offlineimap/repository/IMAP.py b/offlineimap/repository/IMAP.py index c59b3a8..c0cd887 100644 --- a/offlineimap/repository/IMAP.py +++ b/offlineimap/repository/IMAP.py @@ -25,7 +25,7 @@ from offlineimap.repository.Base import BaseRepository from offlineimap import folder, imaputil, imapserver, OfflineImapError from offlineimap.folder.UIDMaps import MappedIMAPFolder from offlineimap.threadutil import ExitNotifyThread -from offlineimap.utils.distro import get_os_sslcertfile +from offlineimap.utils.distro import get_os_sslcertfile, get_os_sslcertfile_searchpath class IMAPRepository(BaseRepository): @@ -201,16 +201,44 @@ class IMAPRepository(BaseRepository): return self.getconf_xform('sslclientkey', xforms, None) def getsslcacertfile(self): - """Return the absolute path of the CA certfile to use, if any""" + """Determines CA bundle. + + Returns path to the CA bundle. It is either explicitely specified + or requested via "OS-DEFAULT" value (and we will search known + locations for the current OS and distribution). + + If search via "OS-DEFAULT" route yields nothing, we will throw an + exception to make our callers distinguish between not specified + value and non-existent default CA bundle. + + It is also an error to specify non-existent file via configuration: + it will error out later, but, perhaps, with less verbose explanation, + so we will also throw an exception. It is consistent with + the above behaviour, so any explicitely-requested configuration + that doesn't result in an existing file will give an exception. + """ + xforms = [os.path.expanduser, os.path.expandvars, os.path.abspath] - cacertfile = self.getconf_xform('sslcacertfile', xforms, - get_os_sslcertfile()) + cacertfile = self.getconf_xform('sslcacertfile', xforms, None) + if self.getconf('sslcacertfile', None) == "OS-DEFAULT": + cacertfile = get_os_sslcertfile() + if cacertfile == None: + searchpath = get_os_sslcertfile_searchpath() + if searchpath: + reason = "Default CA bundle was requested, "\ + "but no existing locations available. "\ + "Tried %s." % (", ".join(searchpath)) + else: + reason = "Default CA bundle was requested, "\ + "but OfflineIMAP doesn't know any for your "\ + "current operating system." + raise OfflineImapError(reason, OfflineImapError.ERROR.REPO) if cacertfile is None: return None if not os.path.isfile(cacertfile): - raise SyntaxWarning("CA certfile for repository '%s' could " - "not be found. No such file: '%s'" \ - % (self.name, cacertfile)) + reason = "CA certfile for repository '%s' couldn't be found. "\ + "No such file: '%s'" % (self.name, cacertfile) + raise OfflineImapError(reason, OfflineImapError.ERROR.REPO) return cacertfile def getsslversion(self): diff --git a/offlineimap/utils/distro.py b/offlineimap/utils/distro.py index 7c944b9..8cd2b79 100644 --- a/offlineimap/utils/distro.py +++ b/offlineimap/utils/distro.py @@ -14,7 +14,6 @@ import os __DEF_OS_LOCATIONS = { 'freebsd': '/usr/local/share/certs/ca-root-nss.crt', 'openbsd': '/etc/ssl/cert.pem', - 'netbsd': None, 'dragonfly': '/etc/ssl/cert.pem', 'darwin': [ # MacPorts, port curl-ca-bundle @@ -48,6 +47,26 @@ def get_os_name(): return OS +def get_os_sslcertfile_searchpath(): + """Returns search path for CA bundle for the current OS. + + We will return an iterable even if configuration has just + a single value: it is easier for our callers to be sure + that they can iterate over result. + + Returned value of None means that there is no search path + at all. + """ + + OS = get_os_name() + + l = None + if OS in __DEF_OS_LOCATIONS: + l = __DEF_OS_LOCATIONS[OS] + if not hasattr(l, '__iter__'): + l = (l, ) + return l + def get_os_sslcertfile(): """ @@ -57,18 +76,16 @@ def get_os_sslcertfile(): Returns the location of the file or None if there is no known CA certificate file or all known locations correspond to non-existing filesystem objects. - """ - OS = get_os_name() - if OS in __DEF_OS_LOCATIONS: - l = __DEF_OS_LOCATIONS[OS] - if not hasattr(l, '__iter__'): - l = (l, ) - for f in l: - assert (type(f) == type("")) - if os.path.exists(f) and \ - (os.path.isfile(f) or os.path.islink(f)): - return f + l = get_os_sslcertfile_searchpath() + if l == None: + return None + + for f in l: + assert (type(f) == type("")) + if os.path.exists(f) and \ + (os.path.isfile(f) or os.path.islink(f)): + return f return None