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.

Reported-By: Edd Barrett <edd@theunixzoo.co.uk>
Reviewed-By: Nicolas Sebrecht <nicolas.s-dev@laposte.net>
Signed-off-by: Eygene Ryabinkin <rea@codelabs.ru>
This commit is contained in:
Eygene Ryabinkin 2015-01-18 10:45:46 +03:00
parent 9b911faa58
commit f69613965f
4 changed files with 86 additions and 27 deletions

View File

@ -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.
#

View File

@ -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:

View File

@ -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):

View File

@ -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