From 50de0a74ab6f980875c3cd0de630fee14d4eb14d Mon Sep 17 00:00:00 2001 From: Eygene Ryabinkin Date: Sun, 18 Jan 2015 10:45:46 +0300 Subject: [PATCH] Make OS-default CA certificate file to be requested explicitely This simplifies logics for the user, especially if he uses both fingerprint and certificate validation: it is hard to maintain the compatibility with the prior behaviour and to avoid getting default CA bundle to be disabled when fingerprint verification is requested. See http://thread.gmane.org/gmane.mail.imap.offlineimap.general/6695 for discussion about this change. Default CA bundle is requested via 'sslcertfile = OS-DEFAULT'. I had also enforced all cases where explicitely-requested CA bundles are non-existent to be hard errors: when users asks us to use CA bundle (and, thus, certificate validation), but we can't find one, we must error out rather than happily continue and downgrade to no validation. Signed-off-by: Eygene Ryabinkin Conflicts: offlineimap.conf --- offlineimap.conf | 22 ++++++++++++++++++---- offlineimap/imapserver.py | 8 ++++---- offlineimap/repository/IMAP.py | 42 +++++++++++++++++++++++++++++++++++------- offlineimap/utils/distro.py | 41 +++++++++++++++++++++++++++++------------ 4 files changed, 86 insertions(+), 27 deletions(-) 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 -- 2.1.2