123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 |
- """
- """
-
- # Created on 2013.08.05
- #
- # Author: Giovanni Cannata
- #
- # Copyright 2013 - 2018 Giovanni Cannata
- #
- # This file is part of ldap3.
- #
- # ldap3 is free software: you can redistribute it and/or modify
- # it under the terms of the GNU Lesser General Public License as published
- # by the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # ldap3 is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU Lesser General Public License for more details.
- #
- # You should have received a copy of the GNU Lesser General Public License
- # along with ldap3 in the COPYING and COPYING.LESSER files.
- # If not, see <http://www.gnu.org/licenses/>.
-
- from .exceptions import LDAPSSLNotSupportedError, LDAPSSLConfigurationError, LDAPStartTLSError, LDAPCertificateError, start_tls_exception_factory
- from .. import SEQUENCE_TYPES
- from ..utils.log import log, log_enabled, ERROR, BASIC, NETWORK
-
- try:
- # noinspection PyUnresolvedReferences
- import ssl
- except ImportError:
- if log_enabled(ERROR):
- log(ERROR, 'SSL not supported in this Python interpreter')
- raise LDAPSSLNotSupportedError('SSL not supported in this Python interpreter')
-
- try:
- from ssl import match_hostname, CertificateError # backport for python2 missing ssl functionalities
- except ImportError:
- from ..utils.tls_backport import CertificateError
- from ..utils.tls_backport import match_hostname
- if log_enabled(BASIC):
- log(BASIC, 'using tls_backport')
-
- try: # try to use SSLContext
- # noinspection PyUnresolvedReferences
- from ssl import create_default_context, Purpose # defined in Python 3.4 and Python 2.7.9
- use_ssl_context = True
- except ImportError:
- use_ssl_context = False
- if log_enabled(BASIC):
- log(BASIC, 'SSLContext unavailable')
-
- from os import path
-
-
- # noinspection PyProtectedMember
- class Tls(object):
- """
- tls/ssl configuration for Server object
- Starting from python 2.7.9 and python 3.4 uses the SSLContext object
- that tries to read the CAs defined at system level
- ca_certs_path and ca_certs_data are valid only when using SSLContext
- local_private_key_password is valid only when using SSLContext
- sni is the server name for Server Name Indication (when available)
- """
-
- def __init__(self,
- local_private_key_file=None,
- local_certificate_file=None,
- validate=ssl.CERT_NONE,
- version=None,
- ca_certs_file=None,
- valid_names=None,
- ca_certs_path=None,
- ca_certs_data=None,
- local_private_key_password=None,
- ciphers=None,
- sni=None):
-
- if validate in [ssl.CERT_NONE, ssl.CERT_OPTIONAL, ssl.CERT_REQUIRED]:
- self.validate = validate
- elif validate:
- if log_enabled(ERROR):
- log(ERROR, 'invalid validate parameter <%s>', validate)
- raise LDAPSSLConfigurationError('invalid validate parameter')
- if ca_certs_file and path.exists(ca_certs_file):
- self.ca_certs_file = ca_certs_file
- elif ca_certs_file:
- if log_enabled(ERROR):
- log(ERROR, 'invalid CA public key file <%s>', ca_certs_file)
- raise LDAPSSLConfigurationError('invalid CA public key file')
- else:
- self.ca_certs_file = None
-
- if ca_certs_path and use_ssl_context and path.exists(ca_certs_path):
- self.ca_certs_path = ca_certs_path
- elif ca_certs_path and not use_ssl_context:
- if log_enabled(ERROR):
- log(ERROR, 'cannot use CA public keys path, SSLContext not available')
- raise LDAPSSLNotSupportedError('cannot use CA public keys path, SSLContext not available')
- elif ca_certs_path:
- if log_enabled(ERROR):
- log(ERROR, 'invalid CA public keys path <%s>', ca_certs_path)
- raise LDAPSSLConfigurationError('invalid CA public keys path')
- else:
- self.ca_certs_path = None
-
- if ca_certs_data and use_ssl_context:
- self.ca_certs_data = ca_certs_data
- elif ca_certs_data:
- if log_enabled(ERROR):
- log(ERROR, 'cannot use CA data, SSLContext not available')
- raise LDAPSSLNotSupportedError('cannot use CA data, SSLContext not available')
- else:
- self.ca_certs_data = None
-
- if local_private_key_password and use_ssl_context:
- self.private_key_password = local_private_key_password
- elif local_private_key_password:
- if log_enabled(ERROR):
- log(ERROR, 'cannot use local private key password, SSLContext not available')
- raise LDAPSSLNotSupportedError('cannot use local private key password, SSLContext is not available')
- else:
- self.private_key_password = None
-
- self.version = version
- self.private_key_file = local_private_key_file
- self.certificate_file = local_certificate_file
- self.valid_names = valid_names
- self.ciphers = ciphers
- self.sni = sni
-
- if log_enabled(BASIC):
- log(BASIC, 'instantiated Tls: <%r>' % self)
-
- def __str__(self):
- s = [
- 'protocol: ' + str(self.version),
- 'client private key: ' + ('present ' if self.private_key_file else 'not present'),
- 'client certificate: ' + ('present ' if self.certificate_file else 'not present'),
- 'private key password: ' + ('present ' if self.private_key_password else 'not present'),
- 'CA certificates file: ' + ('present ' if self.ca_certs_file else 'not present'),
- 'CA certificates path: ' + ('present ' if self.ca_certs_path else 'not present'),
- 'CA certificates data: ' + ('present ' if self.ca_certs_data else 'not present'),
- 'verify mode: ' + str(self.validate),
- 'valid names: ' + str(self.valid_names),
- 'ciphers: ' + str(self.ciphers),
- 'sni: ' + str(self.sni)
- ]
- return ' - '.join(s)
-
- def __repr__(self):
- r = '' if self.private_key_file is None else ', local_private_key_file={0.private_key_file!r}'.format(self)
- r += '' if self.certificate_file is None else ', local_certificate_file={0.certificate_file!r}'.format(self)
- r += '' if self.validate is None else ', validate={0.validate!r}'.format(self)
- r += '' if self.version is None else ', version={0.version!r}'.format(self)
- r += '' if self.ca_certs_file is None else ', ca_certs_file={0.ca_certs_file!r}'.format(self)
- r += '' if self.ca_certs_path is None else ', ca_certs_path={0.ca_certs_path!r}'.format(self)
- r += '' if self.ca_certs_data is None else ', ca_certs_data={0.ca_certs_data!r}'.format(self)
- r += '' if self.ciphers is None else ', ciphers={0.ciphers!r}'.format(self)
- r += '' if self.sni is None else ', sni={0.sni!r}'.format(self)
- r = 'Tls(' + r[2:] + ')'
- return r
-
- def wrap_socket(self, connection, do_handshake=False):
- """
- Adds TLS to the connection socket
- """
- if use_ssl_context:
- if self.version is None: # uses the default ssl context for reasonable security
- ssl_context = create_default_context(purpose=Purpose.SERVER_AUTH,
- cafile=self.ca_certs_file,
- capath=self.ca_certs_path,
- cadata=self.ca_certs_data)
- else: # code from create_default_context in the Python standard library 3.5.1, creates a ssl context with the specificd protocol version
- ssl_context = ssl.SSLContext(self.version)
- if self.ca_certs_file or self.ca_certs_path or self.ca_certs_data:
- ssl_context.load_verify_locations(self.ca_certs_file, self.ca_certs_path, self.ca_certs_data)
- elif self.validate != ssl.CERT_NONE:
- ssl_context.load_default_certs(Purpose.SERVER_AUTH)
-
- if self.certificate_file:
- ssl_context.load_cert_chain(self.certificate_file, keyfile=self.private_key_file, password=self.private_key_password)
- ssl_context.check_hostname = False
- ssl_context.verify_mode = self.validate
-
- if self.ciphers:
- try:
- ssl_context.set_ciphers(self.ciphers)
- except ssl.SSLError:
- pass
-
- if self.sni:
- wrapped_socket = ssl_context.wrap_socket(connection.socket, server_side=False, do_handshake_on_connect=do_handshake, server_hostname=self.sni)
- else:
- wrapped_socket = ssl_context.wrap_socket(connection.socket, server_side=False, do_handshake_on_connect=do_handshake)
- if log_enabled(NETWORK):
- log(NETWORK, 'socket wrapped with SSL using SSLContext for <%s>', connection)
- else:
- if self.version is None and hasattr(ssl, 'PROTOCOL_SSLv23'):
- self.version = ssl.PROTOCOL_SSLv23
- if self.ciphers:
- try:
-
- wrapped_socket = ssl.wrap_socket(connection.socket,
- keyfile=self.private_key_file,
- certfile=self.certificate_file,
- server_side=False,
- cert_reqs=self.validate,
- ssl_version=self.version,
- ca_certs=self.ca_certs_file,
- do_handshake_on_connect=do_handshake,
- ciphers=self.ciphers)
- except ssl.SSLError:
- raise
- except TypeError: # in python2.6 no ciphers argument is present, failback to self.ciphers=None
- self.ciphers = None
-
- if not self.ciphers:
- wrapped_socket = ssl.wrap_socket(connection.socket,
- keyfile=self.private_key_file,
- certfile=self.certificate_file,
- server_side=False,
- cert_reqs=self.validate,
- ssl_version=self.version,
- ca_certs=self.ca_certs_file,
- do_handshake_on_connect=do_handshake)
- if log_enabled(NETWORK):
- log(NETWORK, 'socket wrapped with SSL for <%s>', connection)
-
- if do_handshake and (self.validate == ssl.CERT_REQUIRED or self.validate == ssl.CERT_OPTIONAL):
- check_hostname(wrapped_socket, connection.server.host, self.valid_names)
-
- connection.socket = wrapped_socket
- return
-
- def start_tls(self, connection):
- if connection.server.ssl: # ssl already established at server level
- return False
-
- if (connection.tls_started and not connection._executing_deferred) or connection.strategy._outstanding or connection.sasl_in_progress:
- # Per RFC 4513 (3.1.1)
- if log_enabled(ERROR):
- log(ERROR, "can't start tls because operations are in progress for <%s>", self)
- return False
- connection.starting_tls = True
- if log_enabled(BASIC):
- log(BASIC, 'starting tls for <%s>', connection)
- if not connection.strategy.sync:
- connection._awaiting_for_async_start_tls = True # some flaky servers (OpenLDAP) doesn't return the extended response name in response
- result = connection.extended('1.3.6.1.4.1.1466.20037')
- if not connection.strategy.sync:
- # asynchronous - _start_tls must be executed by the strategy
- response = connection.get_response(result)
- if response != (None, None):
- if log_enabled(BASIC):
- log(BASIC, 'tls started for <%s>', connection)
- return True
- else:
- if log_enabled(BASIC):
- log(BASIC, 'tls not started for <%s>', connection)
- return False
- else:
- if connection.result['description'] not in ['success']:
- # startTLS failed
- connection.last_error = 'startTLS failed - ' + str(connection.result['description'])
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', connection.last_error, connection)
- raise LDAPStartTLSError(connection.last_error)
- if log_enabled(BASIC):
- log(BASIC, 'tls started for <%s>', connection)
- return self._start_tls(connection)
-
- def _start_tls(self, connection):
- exc = None
- try:
- self.wrap_socket(connection, do_handshake=True)
- except Exception as e:
- connection.last_error = 'wrap socket error: ' + str(e)
- exc = e
-
- connection.starting_tls = False
-
- if exc:
- if log_enabled(ERROR):
- log(ERROR, 'error <%s> wrapping socket for TLS in <%s>', connection.last_error, connection)
- raise start_tls_exception_factory(LDAPStartTLSError, exc)(connection.last_error)
-
- if connection.usage:
- connection._usage.wrapped_sockets += 1
-
- connection.tls_started = True
- return True
-
-
- def check_hostname(sock, server_name, additional_names):
- server_certificate = sock.getpeercert()
- if log_enabled(NETWORK):
- log(NETWORK, 'certificate found for %s: %s', sock, server_certificate)
- if additional_names:
- host_names = [server_name] + (additional_names if isinstance(additional_names, SEQUENCE_TYPES) else [additional_names])
- else:
- host_names = [server_name]
-
- for host_name in host_names:
- if not host_name:
- continue
- elif host_name == '*':
- if log_enabled(NETWORK):
- log(NETWORK, 'certificate matches * wildcard')
- return # valid
-
- try:
- match_hostname(server_certificate, host_name) # raise CertificateError if certificate doesn't match server name
- if log_enabled(NETWORK):
- log(NETWORK, 'certificate matches host name <%s>', host_name)
- return # valid
- except CertificateError as e:
- if log_enabled(NETWORK):
- log(NETWORK, str(e))
-
- if log_enabled(ERROR):
- log(ERROR, "hostname doesn't match certificate")
- raise LDAPCertificateError("certificate %s doesn't match any name in %s " % (server_certificate, str(host_names)))
|