10

I've been trying to use Python-LDAP (version 2.4.19) under MacOS X 10.9.5 and Python 2.7.9

I want to validate my connection to a given LDAP server after I've called the .start_tls_s() (or to have the method raise and exception if the certificate cannot be verified). (I'd also like to check for a CRL, but that's a different matter).

Here's my code:

#!python
#!/usr/bin/env python
import ConfigParser, os, sys
import ldap

CACERTFILE='./ca_ldap.bad'
## CACERTFILE='./ca_ldap.crt'

config = ConfigParser.ConfigParser()
config.read(os.path.expanduser('~/.ssh/creds.ini'))
uid = config.get('LDAP', 'uid')
pwd = config.get('LDAP', 'pwd')
svr = config.get('LDAP', 'svr')
bdn = config.get('LDAP', 'bdn')

ld = ldap.initialize(svr)
ld.protocol_version=ldap.VERSION3
ld.set_option(ldap.OPT_DEBUG_LEVEL, 255 )
ld.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
ld.set_option(ldap.OPT_X_TLS_CACERTFILE, CACERTFILE)
ld.set_option(ldap.OPT_X_TLS_DEMAND, True )
ld.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_HARD)

## From: https://stackoverflow.com/a/7810308/149076
## and : http://python-ldap.cvs.sourceforge.net/viewvc/python-ldap/python-ldap/Demo/initialize.py?revision=1.14&view=markup

ld.start_tls_s()

for each in dir(ldap):
    if 'OPT_X_TLS' in each:
        try:
            print '\t*** %s: %s' % (each, ld.get_option((getattr(ldap, each))))
        except Exception, e:
            print >> sys.stderr, '... Except %s: %s\n' % (each, e)

ld.simple_bind_s(uid, pwd)
results = ld.search_s(bdn, ldap.SCOPE_SUBTREE)

print 'Found %s entries under %s' % (len(results), bdn)
sys.exit()

As noted in the comments I have copied most of this from https://stackoverflow.com/a/7810308/149076 and from http://python-ldap.cvs.sourceforge.net/viewvc/python-ldap/python-ldap/Demo/initialize.py?revision=1.14&view=markup ... though I have tried many variations and sequences of this.

As shown I have two files which represent a bad certificate and one which should work (it's actually taken from one of our systems which is configured to run sssd (System Security Services Daemon) which is presumed to be checking this correctly.

In the "bad" copy I've simply replaced the first character of each key line with the letter 'x' on the assumption that this would corrupt the CA key and cause any code attempting to verify a chain of signatures to fail.

However, it seems that the Python LDAP code ignores this; even if I set it to /dev/null or an entirely bogus path my code still runs, still binds to the LDAP server and still completes my search request.

So the question is, how do I get this to "fail" as intended (or, more broadly, how do I prevent my code from being vulnerable to MITM (Mallory) attacks?

If it's of any consequence in this discussion here's my OpenSSL version:

$ openssl version
OpenSSL 0.9.8za 5 Jun 2014

The LDAP server is running OpenLDAP, but I don't know any details about its version nor configuration.

Here's sample output from my code:

    *** OPT_X_TLS: 0
    *** OPT_X_TLS_ALLOW: 0
    *** OPT_X_TLS_CACERTDIR: None
    *** OPT_X_TLS_CACERTFILE: /bogus/null
    *** OPT_X_TLS_CERTFILE: None
    *** OPT_X_TLS_CIPHER_SUITE: None
    *** OPT_X_TLS_CRLCHECK: 0
    *** OPT_X_TLS_CRLFILE: None
    *** OPT_X_TLS_CRL_ALL: 1
    *** OPT_X_TLS_CRL_NONE: {'info_version': 1, 'extensions': ('X_OPENLDAP', 'THREAD_SAFE', 'SESSION_THREAD_SAFE', 'OPERATION_THREAD_SAFE', 'X_OPENLDAP_THREAD_SAFE'), 'vendor_version': 20428, 'protocol_version': 3, 'vendor_name': 'OpenLDAP', 'api_version': 3001}
    *** OPT_X_TLS_CRL_PEER: 3
... Except OPT_X_TLS_CTX: unknown option 24577

    *** OPT_X_TLS_DEMAND: 1
    *** OPT_X_TLS_DHFILE: None
    *** OPT_X_TLS_HARD: 3
    *** OPT_X_TLS_KEYFILE: None
    *** OPT_X_TLS_NEVER: {'info_version': 1, 'extensions': ('X_OPENLDAP', 'THREAD_SAFE', 'SESSION_THREAD_SAFE', 'OPERATION_THREAD_SAFE', 'X_OPENLDAP_THREAD_SAFE'), 'vendor_version': 20428, 'protocol_version': 3, 'vendor_name': 'OpenLDAP', 'api_version': 3001}
... Except OPT_X_TLS_NEWCTX: unknown option 24591

    *** OPT_X_TLS_PACKAGE: OpenSSL
    *** OPT_X_TLS_PROTOCOL_MIN: 0
    *** OPT_X_TLS_RANDOM_FILE: None
    *** OPT_X_TLS_REQUIRE_CERT: 1
    *** OPT_X_TLS_TRY: 0

Found 883 entries under [... redacted ...]
Community
  • 1
  • 1
Jim Dennis
  • 17,054
  • 13
  • 68
  • 116
  • *"I'd also like to check for a CRL, but that's a different matter"* - be careful with this. You could be setting yourself up for a DoS due to missing/broken CRLs and OCSP responders. Mozilla turned it off for a while because it had such a negative effect on the user experience. Stapled responses would probably be the way to go. – jww Apr 13 '15 at 06:18
  • In my case this will be working in an environment that provides their own CA and manages their own CRLs. It's all internal. – Jim Dennis Apr 13 '15 at 06:48
  • I might be the bearer of bad news... Python-LDAP can use OpenSSL. But the calls one typically utilizes or encounters for certificate handling are not present. For example, I get 0 hits for `cd python-ldap; grep -R -i ca_certs *; grep -R -i wrap_socket *; grep -R -i certs_reqs *`. You might want to reach out to the developers to see their position. They may say something like "the code is meant to be run in the same security boundary as the ldap server. So we use a certificate for confidentiality, but we don't verify the server or employ server authentication because there's no active MitM". – jww Apr 13 '15 at 07:24
  • 1
    According to their web page, Python-LDAP wraps OpenLDAP (and not OpenSSL directly). As far as I remember, the OpenLDAP client library can be configured via environment variables, so I'd expect this feature to work, even if Python-LDAP has no specific provisions for it, or even if they exist but do not work. How you get the error message back is an interesting part, but it should not continue with the connection. I'd test the same search and environmental settings from CLI as well, eg with `ldapsearch`. `man ldap.conf` shows the names of the environmental variables expected. – Laszlo Valko Apr 18 '15 at 14:53

1 Answers1

10

Your code works for me as expected. Actually, I had exactly the opposite problem, when I executed your code for the first time. It always said 'certificate verify failed'. Adding the following lines fixed this:

# Force libldap to create a new SSL context (must be last TLS option!)
ld.set_option(ldap.OPT_X_TLS_NEWCTX, 0)

Now when I use the wrong CA certificate or one that has been modified as you described it, the result is this error message:

Traceback (most recent call last):
  File "ldap_ssl.py", line 28, in <module>
    ld.start_tls_s()
  File "/Library/Python/2.7/site-packages/python_ldap-2.4.19-py2.7-macosx-10.10-intel.egg/ldap/ldapobject.py", line 571, in start_tls_s
    return self._ldap_call(self._l.start_tls_s)
  File "/Library/Python/2.7/site-packages/python_ldap-2.4.19-py2.7-macosx-10.10-intel.egg/ldap/ldapobject.py", line 106, in _ldap_call
    result = func(*args,**kwargs)
ldap.CONNECT_ERROR: {'info': 'error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed (unable to get local issuer certificate)', 'desc': 'Connect error'}

When I use the right CA certificate, the output is like yours.


Now the interesting question is: What are the differences between our setups and especially which difference causes this strange behaviour on your machine?

My setup is:

  • Mac OS X 10.10
  • Python 2.7.6
  • python-ldap 2.4.19 (manual installation)
  • OpenLDAP 2.4.39 (via Homebrew)
  • OpenSSL 1.0.1l (via Homebrew)

I have a local OpenLDAP running, installed with Homebrew:

brew install homebrew/dupes/openldap --with-berkeley-db

On Yosemite python-ldap is quite buggy when installed with pip (see Python-ldap set_option not working on Yosemite), therefore I had to download the tarball and compile/install it, which was fortunately pretty easy, because I already had the OpenLDAP installation with current libs/headers:

First edit the [_ldap] section in setup.cfg like this:

[_ldap]
library_dirs = /usr/local/opt/openldap/lib /usr/lib /usr/local/lib
include_dirs = /usr/local/opt/openldap/include /usr/include/sasl /usr/include /usr/local/include
extra_compile_args = -g -arch x86_64
extra_objects = 
libs = ldap_r lber sasl2 ssl crypto

Some header files are in Mac OS SDK, link the directory (change the path according to your version) to /usr/include:

sudo ln -s /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.10.sdk/usr/include/ /usr/include

Then build and install:

python setup.py build
sudo python setup.py install

The output of otool shows that python-ldap is now linked to the libraries of OpenLDAP 2.4.39 and OpenSSL 0.9.8:

$ otool -L /Library/Python/2.7/site-packages/python_ldap-2.4.19-py2.7-macosx-10.10-intel.egg/_ldap.so
/Library/Python/2.7/site-packages/python_ldap-2.4.19-py2.7-macosx-10.10-intel.egg/_ldap.so:
    /usr/local/lib/libldap_r-2.4.2.dylib (compatibility version 13.0.0, current version 13.2.0)
    /usr/local/lib/liblber-2.4.2.dylib (compatibility version 13.0.0, current version 13.2.0)
    /usr/lib/libsasl2.2.dylib (compatibility version 3.0.0, current version 3.15.0)
    /usr/lib/libssl.0.9.8.dylib (compatibility version 0.9.8, current version 0.9.8)
    /usr/lib/libcrypto.0.9.8.dylib (compatibility version 0.9.8, current version 0.9.8)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1213.0.0)

An alternative approach for building python-ldap is to install only the OpenLDAP libraries and headers needed for building: http://projects.skurfer.com/posts/2011/python_ldap_lion/

All these steps should work under Mavericks as well and I assume using the latest OpenLDAP and OpenSSL libraries will solve your problem.

Community
  • 1
  • 1
Omikron
  • 4,072
  • 1
  • 27
  • 28
  • On my Mac under MacOS 10.9.5, I was not getting any warning or failure. On a Linux VM under CentOS 6.5, last I checked, I'm getting the behavior you describe here (at least up to the point where it "it always raises the exception"). I'll try the `ld.set_option(ldap.OPT_X_TLS_NEWCTX, 0)` setting and adjusting some of my `brew` libraries. (I'm using Python 2.7.9 as noted above; my LDAP/OpenSSL is either MacOS default or corporate loaded --- I haven't messed with either of those. – Jim Dennis Apr 19 '15 at 00:34
  • Omikron, you are definitely the front runner for the bounty so far. ;) – Jim Dennis Apr 19 '15 at 00:34
  • I have added the output of otool, it seems like the OpenSSL version does not matter here, because my python-ldap is linked to 0.9.8z not to 1.0.1l. – Omikron Apr 19 '15 at 10:40