Repository: privacyidea/privacyidea

Issue:
Creating TLS context fails when no TLS_CA_FILE is given even if TLS_VERIFY is false
We are initializing the TLS context when creating the LDAPIdResolver here:
https://github.com/privacyidea/privacyidea/blob/28257da4cacdd0ed68ce4eb6e6b65e0c498ea884/privacyidea/lib/resolvers/LDAPIdResolver.py#L821
The `tls_ca_file` parameter is always set (either from the config or with `DEFAULT_CA_FILE`.
When creating the TLS context, the content of `tls_ca_file` is passed independent of the `tls_verify` setting:
https://github.com/privacyidea/privacyidea/blob/28257da4cacdd0ed68ce4eb6e6b65e0c498ea884/privacyidea/lib/resolvers/LDAPIdResolver.py#L529
Unfortunately testing the connection won't catch this error since the `_get_tls_context()` function is called with the `tls_ca_file` parameter correctly set to `None` in case it is missing:
https://github.com/privacyidea/privacyidea/blob/28257da4cacdd0ed68ce4eb6e6b65e0c498ea884/privacyidea/lib/resolvers/LDAPIdResolver.py#L1033


Discussion:
Expected behaviour: CA file should be ignored / set to None, if TLS_VERIFY is false.

Resolving patch:
diff --git a/privacyidea/lib/resolvers/LDAPIdResolver.py b/privacyidea/lib/resolvers/LDAPIdResolver.py
--- a/privacyidea/lib/resolvers/LDAPIdResolver.py
+++ b/privacyidea/lib/resolvers/LDAPIdResolver.py
@@ -526,6 +526,7 @@ def _get_tls_context(ldap_uri=None, start_tls=False, tls_version=None, tls_verif
                 tls_ca_file = tls_ca_file or DEFAULT_CA_FILE
             else:
                 tls_verify = ssl.CERT_NONE
+                tls_ca_file = None
             tls_context = Tls(validate=tls_verify,
                               version=int(tls_version),
                               ssl_options=tls_options,
@@ -817,11 +818,11 @@ def loadConfig(self, config):
         self.tls_verify = is_true(config.get("TLS_VERIFY", False))
         # Fallback to DEFAULT_TLS_PROTOCOL (TLSv1: 3, TLSv1.1: 4, v1.2: 5, TLS negotiation: 2)
         self.tls_version = int(config.get("TLS_VERSION") or DEFAULT_TLS_PROTOCOL)
-        self.tls_ca_file = config.get("TLS_CA_FILE") or DEFAULT_CA_FILE
+        self.tls_ca_file = config.get("TLS_CA_FILE")
         self.tls_context = self._get_tls_context(ldap_uri=self.uri, start_tls=self.start_tls,
-                                            tls_version=self.tls_version,
-                                            tls_verify=self.tls_verify,
-                                            tls_ca_file=self.tls_ca_file)
+                                                 tls_version=self.tls_version,
+                                                 tls_verify=self.tls_verify,
+                                                 tls_ca_file=self.tls_ca_file)
         self.serverpool_persistent = is_true(config.get("SERVERPOOL_PERSISTENT", False))
         self.serverpool_rounds = int(config.get("SERVERPOOL_ROUNDS") or SERVERPOOL_ROUNDS)
         self.serverpool_skip = int(config.get("SERVERPOOL_SKIP") or SERVERPOOL_SKIP)


Test patch:
diff --git a/tests/test_lib_resolver.py b/tests/test_lib_resolver.py
--- a/tests/test_lib_resolver.py
+++ b/tests/test_lib_resolver.py
@@ -21,6 +21,7 @@
 import uuid
 import pytest
 import json
+import ssl
 from privacyidea.lib.resolvers.LDAPIdResolver import IdResolver as LDAPResolver, LockingServerPool
 from privacyidea.lib.resolvers.SQLIdResolver import IdResolver as SQLResolver
 from privacyidea.lib.resolvers.SCIMIdResolver import IdResolver as SCIMResolver
@@ -38,7 +39,6 @@
 from privacyidea.models import ResolverConfig
 from privacyidea.lib.utils import to_bytes, to_unicode
 from requests import HTTPError
-from privacyidea.lib.error import ParameterError
 
 objectGUIDs = [
     '039b36ef-e7c0-42f3-9bf9-ca6a6c0d4d31',
@@ -1597,7 +1597,7 @@ def test_24_ldaps(self):
                   'UIDTYPE': 'unknownType',
                   'CACHE_TIMEOUT': 0,
                   'TLS_VERIFY': '1'
-        }
+                  }
         start_tls_resolver = LDAPResolver()
         start_tls_resolver.loadConfig(config)
         result = start_tls_resolver.getUserList({'username': '*'})
@@ -1630,7 +1630,7 @@ def check_tls_version_ldap3(tls_version_pi, tls_version_ldap3):
                       'CACHE_TIMEOUT': 0,
                       'START_TLS': '1',
                       'TLS_VERIFY': '1'
-        }
+                      }
             config.update({"TLS_VERSION": tls_version_pi})
             start_tls_resolver = LDAPResolver()
             start_tls_resolver.loadConfig(config)
@@ -1653,6 +1653,40 @@ def check_tls_version_ldap3(tls_version_pi, tls_version_ldap3):
             check_tls_version_ldap3(tls_version_pi, tls_version_ldap3)
         self.assertRaises(ValueError, check_tls_version_ldap3, "version is nonsense", "nothing to check")
 
+    @ldap3mock.activate
+    def test_24c_no_tls_verify(self):
+        # Check that tls should not verify the server
+        ldap3mock.setLDAPDirectory(LDAPDirectory)
+        config = {'LDAPURI': 'ldaps://localhost',
+                  'LDAPBASE': 'o=test',
+                  'BINDDN': 'cn=manager,ou=example,o=test',
+                  'BINDPW': 'ldaptest',
+                  'LOGINNAMEATTRIBUTE': 'cn',
+                  'LDAPSEARCHFILTER': '(cn=*)',
+                  'USERINFO': '{ "username": "cn",'
+                                '"phone" : "telephoneNumber", '
+                                '"mobile" : "mobile"'
+                                ', "email" : "mail", '
+                                '"surname" : "sn", '
+                                '"givenname" : "givenName" }',
+                  'UIDTYPE': 'unknownType',
+                  'CACHE_TIMEOUT': 0,
+                  'TLS_CA_FILE': '/unknown/path/to/ca_certs.crt',
+                  'TLS_VERIFY': '0'
+                  }
+        start_tls_resolver = LDAPResolver()
+        start_tls_resolver.loadConfig(config)
+        result = start_tls_resolver.getUserList({'username': '*'})
+        self.assertEqual(len(result), len(LDAPDirectory))
+        # We check that all Server objects were constructed with a non-None TLS context and use_ssl=True
+        for _, kwargs in ldap3mock.get_server_mock().call_args_list:
+            self.assertIsNotNone(kwargs['tls'])
+            self.assertTrue(kwargs['use_ssl'])
+            self.assertEqual(2, kwargs['tls'].version, kwargs['tls'])
+            # the ca_certs_file should be None since we passed a non-existing path
+            self.assertIsNone(kwargs['tls'].ca_certs_file, kwargs['tls'])
+            self.assertEqual(ssl.CERT_NONE, kwargs['tls'].validate, kwargs['tls'])
+
     @ldap3mock.activate
     def test_25_LDAP_DN_with_utf8(self):
         # This tests usernames are entered in the LDAPresolver as utf-8 encoded