Skip to content

Commit

Permalink
Merge pull request #1042 from CravateRouge/dev
Browse files Browse the repository at this point in the history
Add Digest-MD5, NTLM and Kerberos encryption support
  • Loading branch information
cannatag committed Mar 19, 2024
2 parents d169c19 + dafc513 commit 0bb0b8d
Show file tree
Hide file tree
Showing 13 changed files with 235 additions and 77 deletions.
37 changes: 22 additions & 15 deletions docs/manual/source/bind.rst
Expand Up @@ -154,28 +154,28 @@ server trust the credential provided when establishing the secure channel::
Digest-MD5
^^^^^^^^^^

To use the DIGEST-MD5 mechanism you must pass a 4-value or 5-value tuple as sasl_credentials: (realm, user, password, authz_id, enable_signing). You can pass None
for 'realm', 'authz_id' and 'enable_signing' if not used::
To use the DIGEST-MD5 mechanism you must pass a 4-value or 5-value tuple as sasl_credentials: (realm, user, password, authz_id, enable_protection). You can pass None
for 'realm', 'authz_id' and 'enable_protection' if not used::

from ldap3 import Server, Connection, SASL, DIGEST_MD5
server = Server(host = test_server, port = test_port)
c = Connection(server, auto_bind = True, version = 3, client_strategy = test_strategy, authentication = SASL,
sasl_mechanism = DIGEST_MD5, sasl_credentials = (None, 'username', 'password', None, 'sign'))
sasl_mechanism = DIGEST_MD5, sasl_credentials = (None, 'username', 'password', None, ENCRYPT))

Username is not required to be an LDAP entry, but it can be any identifier recognized by the server (i.e. email, principal, ...). If
you pass None as 'realm' the default realm of the LDAP server will be used.

``enable_signing`` is an optional argument, which is only relevant for Digest-MD5 authentication. This argument enable or disable signing
(Integrity protection) when performing LDAP queries.
``enable_protection`` is an optional argument, which is only relevant for Digest-MD5 authentication. This argument enable or disable signing/encryption
(Integrity or Confidentiality protection) when performing LDAP queries.
LDAP signing is a way to prevent replay attacks without encrypting the LDAP traffic. Microsoft publicly recommend to enforce LDAP signing when talking to
an Active Directory server : https://support.microsoft.com/en-us/help/4520412/2020-ldap-channel-binding-and-ldap-signing-requirements-for-windows
LDAP encryption is a way to prevent eavesdropping, it is especially useful to send/receive sensitive data (e.g password change for a user). Active Directory supports Digest-MD5 encryption : https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/a98c1f56-8246-4212-8c4e-d92da1a9563b.

* When ``enable_signing`` is set to 'sign', LDAP requests are signed and signature of LDAP responses is verified.
* When ``enable_signing`` is set to any other value or not set, LDAP requests are not signed.
* When ``enable_protection`` is set to SIGN, LDAP requests are signed and signature of LDAP responses is verified.
* When ``enable_protection`` is set to ENCRYPT, LDAP requests are encrypted and LDAP responses are decrypted and their signature is verified.
* When ``enable_protection`` is set to any other value or not set, LDAP requests are not signed.

Also, DIGEST-MD5 authentication with encryption in addition to the integrity protection (``qop=auth-conf``) is not yet supported by ldap3.

**Using DIGEST-MD5 without LDAP signing is considered deprecated and should not be used.**
**Using DIGEST-MD5 is considered deprecated (RFC6331, July 2011) and should not be used.**


.. _sasl-kerberos:
Expand Down Expand Up @@ -225,15 +225,18 @@ or pass an appropriate value of the ``ReverseDnsSetting`` enum as the first elem


.. note::
`ldap3` does not currently support any SASL data security layers, only authentication.
`ldap3` currently support SASL authentication and data security layers for encryption.

If your server requires a string Security Strength Factor (SSF), you must enable data security layers using ``session_security=ENCRYPT``.

If your server requries a string Security Strength Factor (SSF), you may receive
from ldap3 import KERBEROS, ENCRYPT, Connection
c = Connection(
server, authentication=SASL, sasl_mechanism=KERBEROS, session_security=ENCRYPT)

Plainmay receive
an ``LDAPStrongerAuthRequiredResult`` error when binding, e.g.:

SASL:[GSSAPI]: Sign or Seal are required.


Plain
^^^^^
The PLAIN SASL mechanism sends data in clear text, so it must rely on other means of securing the connection between the client and the LDAP server.
As stated in RFC4616 the PLAIN mechanism should not be used without adequate data security protection as this mechanism affords no integrity or confidentiality
Expand Down Expand Up @@ -269,6 +272,10 @@ When binding via NTLM, it is also possible to authenticate with an LM:NTLM hash

c = Connection(s, user="AUTHTEST\\Administrator", password="E52CAC67419A9A224A3B108F3FA6CB6D:8846F7EAEE8FB117AD06BDD830B7586C", authentication=NTLM)

It also supports confidentiality when performing LDAP Queries using the following:

c = Connection(s, user="AUTHTEST\\Administrator", password="E52CAC67419A9A224A3B108F3FA6CB6D:8846F7EAEE8FB117AD06BDD830B7586C", authentication=NTLM, session_security=ENCRYPT)

LDAPI (LDAP over IPC)
---------------------

Expand Down
2 changes: 2 additions & 0 deletions docs/manual/source/connection.rst
Expand Up @@ -56,6 +56,8 @@ Connection parameters are:

* password: the password of the user for simple bind (defaults to None)

* session_security: the session security to provide if the authentication protocol supports it. Can be ENCRYPT

* auto_bind: automatically opens and binds the connection. Can be AUTO_BIND_NONE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_AFTER_BIND, AUTO_BIND_TLS_BEFORE_BIND.

* version: LDAP protocol version (defaults to 3).
Expand Down
24 changes: 13 additions & 11 deletions docs/manual/source/ssltls.rst
Expand Up @@ -90,26 +90,29 @@ you can try::
Digest-MD5
^^^^^^^^^^

To use the DIGEST-MD5 you must pass a 4-value or 5-value tuple as sasl_credentials: (realm, user, password, authz_id, enable_signing). You can pass None for 'realm', 'authz_id' and 'enable_signing' if not used::
To use the DIGEST-MD5 mechanism you must pass a 4-value or 5-value tuple as sasl_credentials: (realm, user, password, authz_id, enable_protection). You can pass None
for 'realm', 'authz_id' and 'enable_protection' if not used::

server = Server(host = test_server, port = test_port)
connection = Connection(server, auto_bind = True, version = 3, client_strategy = test_strategy, authentication = SASL,
sasl_mechanism = 'DIGEST-MD5', sasl_credentials = (None, 'username', 'password', None, 'sign'))
from ldap3 import Server, Connection, SASL, DIGEST_MD5
server = Server(host = test_server, port = test_port)
c = Connection(server, auto_bind = True, version = 3, client_strategy = test_strategy, authentication = SASL,
sasl_mechanism = DIGEST_MD5, sasl_credentials = (None, 'username', 'password', None, ENCRYPT))

Username is not required to be an LDAP entry, but it can be any identifier recognized by the server (i.e. email, principal, ...). If
you pass None as 'realm' the default realm of the LDAP server will be used.

``enable_signing`` is an optional argument, which is only relevant for Digest-MD5 authentication. This argument enable or disable signing
(Integrity protection) when performing LDAP queries.
``enable_protection`` is an optional argument, which is only relevant for Digest-MD5 authentication. This argument enable or disable signing/encryption
(Integrity or Confidentiality protection) when performing LDAP queries.
LDAP signing is a way to prevent replay attacks without encrypting the LDAP traffic. Microsoft publicly recommend to enforce LDAP signing when talking to
an Active Directory server : https://support.microsoft.com/en-us/help/4520412/2020-ldap-channel-binding-and-ldap-signing-requirements-for-windows
LDAP encryption is a way to prevent eavesdropping, it is especially useful to send/receive sensitive data (e.g password change for a user). Active Directory supports Digest-MD5 encryption : https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/a98c1f56-8246-4212-8c4e-d92da1a9563b.

* When ``enable_signing`` is set to 'sign', LDAP requests are signed and signature of LDAP responses is verified.
* When ``enable_signing`` is set to any other value or not set, LDAP requests are not signed.
* When ``enable_protection`` is set to SIGN, LDAP requests are signed and signature of LDAP responses is verified.
* When ``enable_protection`` is set to ENCRYPT, LDAP requests are encrypted and LDAP responses are decrypted and their signature is verified.
* When ``enable_protection`` is set to any other value or not set, LDAP requests are not signed.

Also, DIGEST-MD5 authentication with encryption in addition to the integrity protection (``qop=auth-conf``) is not yet supported by ldap3.

**Using DIGEST-MD5 without LDAP signing is considered deprecated and should not be used.**
**Using DIGEST-MD5 without LDAP signing is deprecated and should not be used.**

Using certificate authentication with Microsoft Active Directory
================================================================
Expand Down Expand Up @@ -140,4 +143,3 @@ The ``authzId`` field must contains the distinguished name of the object (prefix

ldap_connection = ldap3.Connection(ldap_server, authentication=ldap3.SASL, sasl_mechanism=ldap3.EXTERNAL, auto_bind=ldap3.AUTO_BIND_TLS_BEFORE_BIND,
sasl_credentials='dn:CN=John Doe,CN=Users,DC=contoso,DC=com')

4 changes: 4 additions & 0 deletions ldap3/__init__.py
Expand Up @@ -37,6 +37,10 @@
KERBEROS = GSSAPI = 'GSSAPI'
PLAIN = 'PLAIN'

# SESSION SECURITY
SIGN = 'sign'
ENCRYPT = 'ENCRYPT'

AUTO_BIND_DEFAULT = 'DEFAULT' # binds connection when using "with" context manager
AUTO_BIND_NONE = 'NONE' # same as False, no bind is performed
AUTO_BIND_NO_TLS = 'NO_TLS' # same as True, bind is performed without tls
Expand Down
32 changes: 27 additions & 5 deletions ldap3/core/connection.py
Expand Up @@ -32,7 +32,7 @@
SUBTREE, ASYNC, SYNC, NO_ATTRIBUTES, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, MODIFY_INCREMENT, LDIF, ASYNC_STREAM, \
RESTARTABLE, ROUND_ROBIN, REUSABLE, AUTO_BIND_DEFAULT, AUTO_BIND_NONE, AUTO_BIND_TLS_BEFORE_BIND, SAFE_SYNC, SAFE_RESTARTABLE, \
AUTO_BIND_TLS_AFTER_BIND, AUTO_BIND_NO_TLS, STRING_TYPES, SEQUENCE_TYPES, MOCK_SYNC, MOCK_ASYNC, NTLM, EXTERNAL,\
DIGEST_MD5, GSSAPI, PLAIN, DSA, SCHEMA, ALL
DIGEST_MD5, GSSAPI, PLAIN, DSA, SCHEMA, ALL, ENCRYPT, SIGN

from .results import RESULT_SUCCESS, RESULT_COMPARE_TRUE, RESULT_COMPARE_FALSE
from ..extend import ExtendedOperationsRoot
Expand Down Expand Up @@ -138,6 +138,8 @@ class Connection(object):
:type user: str
:param password: the password for simple authentication
:type password: str
:param session_security: the session security to provide if the authentication protocol supports it
:type session_security: str, can ENCRYPT
:param auto_bind: specify if the bind will be performed automatically when defining the Connection object
:type auto_bind: int, can be one of AUTO_BIND_DEFAULT, AUTO_BIND_NONE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND, AUTO_BIND_TLS_AFTER_BIND as specified in ldap3
:param version: LDAP version, default to 3
Expand Down Expand Up @@ -187,6 +189,7 @@ def __init__(self,
server,
user=None,
password=None,
session_security=None,
auto_bind=AUTO_BIND_DEFAULT,
version=3,
authentication=None,
Expand Down Expand Up @@ -288,8 +291,18 @@ def __init__(self,
self.auto_encode = auto_encode
self._digest_md5_kic = None
self._digest_md5_kis = None
self._digest_md5_kcc_cipher = None
self._digest_md5_kcs_cipher = None
self._digest_md5_sec_num = 0
self.krb_ctx = None

if session_security and not (self.authentication == NTLM or self.sasl_mechanism == GSSAPI):
self.last_error = '"session_security" option only available for NTLM and GSSAPI'
if log_enabled(ERROR):
log(ERROR, '%s for <%s>', self.last_error, self)
raise LDAPInvalidValueError(self.last_error)
self.session_security = session_security

port_err = check_port_and_port_list(source_port, source_port_list)
if port_err:
if log_enabled(ERROR):
Expand Down Expand Up @@ -434,6 +447,7 @@ def __repr__(self):
r = 'Connection(server={0.server!r}'.format(self)
r += '' if self.user is None else ', user={0.user!r}'.format(self)
r += '' if self.password is None else ', password={0.password!r}'.format(self)
r += '' if self.session_security is None else ',session_security={0.session_security!r}'.format(self)
r += '' if self.auto_bind is None else ', auto_bind={0.auto_bind!r}'.format(self)
r += '' if self.version is None else ', version={0.version!r}'.format(self)
r += '' if self.authentication is None else ', authentication={0.authentication!r}'.format(self)
Expand Down Expand Up @@ -470,6 +484,7 @@ def repr_with_sensitive_data_stripped(self):
r = 'Connection(server={0.server!r}'.format(self)
r += '' if self.user is None else ', user={0.user!r}'.format(self)
r += '' if self.password is None else ", password='{0}'".format('<stripped %d characters of sensitive data>' % len(self.password))
r += '' if self.session_security is None else ',session_security={0.session_security!r}'.format(self)
r += '' if self.auto_bind is None else ', auto_bind={0.auto_bind!r}'.format(self)
r += '' if self.version is None else ', version={0.version!r}'.format(self)
r += '' if self.authentication is None else ', authentication={0.authentication!r}'.format(self)
Expand Down Expand Up @@ -690,6 +705,11 @@ def rebind(self,
log(BASIC, 'start (RE)BIND operation via <%s>', self)
self.last_error = None
with self.connection_lock:
if self.session_security == ENCRYPT or self.self.connection._digest_md5_kcs_cipher:
self.last_error = 'Rebind not supported with previous encryption'
if log_enabled(ERROR):
log(ERROR, '%s for <%s>', self.last_error, self)
raise LDAPBindError(self.last_error)
if user:
self.user = user
if password is not None:
Expand Down Expand Up @@ -1363,11 +1383,13 @@ def do_ntlm_bind(self,
# additional import for NTLM
from ..utils.ntlm import NtlmClient
domain_name, user_name = self.user.split('\\', 1)
ntlm_client = NtlmClient(user_name=user_name, domain=domain_name, password=self.password)
self.ntlm_client = NtlmClient(user_name=user_name, domain=domain_name, password=self.password)
if self.session_security == ENCRYPT:
self.ntlm_client.confidentiality = True

# as per https://msdn.microsoft.com/en-us/library/cc223501.aspx
# send a sicilyPackageDiscovery request (in the bindRequest)
request = bind_operation(self.version, 'SICILY_PACKAGE_DISCOVERY', ntlm_client)
request = bind_operation(self.version, 'SICILY_PACKAGE_DISCOVERY', self.ntlm_client)
if log_enabled(PROTOCOL):
log(PROTOCOL, 'NTLM SICILY PACKAGE DISCOVERY request sent via <%s>', self)
response = self.post_send_single_response(self.send('bindRequest', request, controls))
Expand All @@ -1378,7 +1400,7 @@ def do_ntlm_bind(self,
if 'server_creds' in result:
sicily_packages = result['server_creds'].decode('ascii').split(';')
if 'NTLM' in sicily_packages: # NTLM available on server
request = bind_operation(self.version, 'SICILY_NEGOTIATE_NTLM', ntlm_client)
request = bind_operation(self.version, 'SICILY_NEGOTIATE_NTLM', self.ntlm_client)
if log_enabled(PROTOCOL):
log(PROTOCOL, 'NTLM SICILY NEGOTIATE request sent via <%s>', self)
response = self.post_send_single_response(self.send('bindRequest', request, controls))
Expand All @@ -1391,7 +1413,7 @@ def do_ntlm_bind(self,
result = response[0]

if result['result'] == RESULT_SUCCESS:
request = bind_operation(self.version, 'SICILY_RESPONSE_NTLM', ntlm_client,
request = bind_operation(self.version, 'SICILY_RESPONSE_NTLM', self.ntlm_client,
result['server_creds'])
if log_enabled(PROTOCOL):
log(PROTOCOL, 'NTLM SICILY RESPONSE NTLM request sent via <%s>', self)
Expand Down

0 comments on commit 0bb0b8d

Please sign in to comment.