2025-12-25 upload
This commit is contained in:
159
venv/Lib/site-packages/ldap3/protocol/sasl/digestMd5.py
Normal file
159
venv/Lib/site-packages/ldap3/protocol/sasl/digestMd5.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
"""
|
||||
|
||||
# Created on 2014.01.04
|
||||
#
|
||||
# Author: Giovanni Cannata
|
||||
#
|
||||
# Copyright 2014 - 2020 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 binascii import hexlify
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
from ... import SEQUENCE_TYPES
|
||||
from ...protocol.sasl.sasl import abort_sasl_negotiation, send_sasl_negotiation, random_hex_string
|
||||
|
||||
|
||||
STATE_KEY = 0
|
||||
STATE_VALUE = 1
|
||||
|
||||
|
||||
def md5_h(value):
|
||||
if not isinstance(value, bytes):
|
||||
value = value.encode()
|
||||
|
||||
return hashlib.md5(value).digest()
|
||||
|
||||
|
||||
def md5_kd(k, s):
|
||||
if not isinstance(k, bytes):
|
||||
k = k.encode()
|
||||
|
||||
if not isinstance(s, bytes):
|
||||
s = s.encode()
|
||||
|
||||
return md5_h(k + b':' + s)
|
||||
|
||||
|
||||
def md5_hex(value):
|
||||
if not isinstance(value, bytes):
|
||||
value = value.encode()
|
||||
|
||||
return hexlify(value)
|
||||
|
||||
|
||||
def md5_hmac(k, s):
|
||||
if not isinstance(k, bytes):
|
||||
k = k.encode()
|
||||
|
||||
if not isinstance(s, bytes):
|
||||
s = s.encode()
|
||||
|
||||
return hmac.new(k, s, digestmod=hashlib.md5).hexdigest()
|
||||
|
||||
|
||||
def sasl_digest_md5(connection, controls):
|
||||
# sasl_credential must be a tuple made up of the following elements: (realm, user, password, authorization_id) or (realm, user, password, authorization_id, enable_signing)
|
||||
# if realm is None will be used the realm received from the server, if available
|
||||
if not isinstance(connection.sasl_credentials, SEQUENCE_TYPES) or not len(connection.sasl_credentials) in (4, 5):
|
||||
return None
|
||||
|
||||
# step One of RFC2831
|
||||
result = send_sasl_negotiation(connection, controls, None)
|
||||
if 'saslCreds' in result and result['saslCreds'] is not None:
|
||||
server_directives = decode_directives(result['saslCreds'])
|
||||
else:
|
||||
return None
|
||||
|
||||
if 'realm' not in server_directives or 'nonce' not in server_directives or 'algorithm' not in server_directives: # mandatory directives, as per RFC2831
|
||||
abort_sasl_negotiation(connection, controls)
|
||||
return None
|
||||
|
||||
# step Two of RFC2831
|
||||
charset = server_directives['charset'] if 'charset' in server_directives and server_directives['charset'].lower() == 'utf-8' else 'iso8859-1'
|
||||
user = connection.sasl_credentials[1].encode(charset)
|
||||
realm = (connection.sasl_credentials[0] if connection.sasl_credentials[0] else (server_directives['realm'] if 'realm' in server_directives else '')).encode(charset)
|
||||
password = connection.sasl_credentials[2].encode(charset)
|
||||
authz_id = connection.sasl_credentials[3].encode(charset) if connection.sasl_credentials[3] else b''
|
||||
nonce = server_directives['nonce'].encode(charset)
|
||||
cnonce = random_hex_string(16).encode(charset)
|
||||
uri = b'ldap/' + connection.server.host.encode(charset)
|
||||
qop = b'auth'
|
||||
if len(connection.sasl_credentials) == 5 and connection.sasl_credentials[4] == 'sign' and not connection.server.ssl:
|
||||
qop = b'auth-int'
|
||||
connection._digest_md5_sec_num = 0
|
||||
|
||||
digest_response = b'username="' + user + b'",'
|
||||
digest_response += b'realm="' + realm + b'",'
|
||||
digest_response += (b'authzid="' + authz_id + b'",') if authz_id else b''
|
||||
digest_response += b'nonce="' + nonce + b'",'
|
||||
digest_response += b'cnonce="' + cnonce + b'",'
|
||||
digest_response += b'digest-uri="' + uri + b'",'
|
||||
digest_response += b'qop=' + qop + b','
|
||||
digest_response += b'nc=00000001' + b','
|
||||
if charset == 'utf-8':
|
||||
digest_response += b'charset="utf-8",'
|
||||
|
||||
a0 = md5_h(b':'.join([user, realm, password]))
|
||||
a1 = b':'.join([a0, nonce, cnonce, authz_id]) if authz_id else b':'.join([a0, nonce, cnonce])
|
||||
a2 = b'AUTHENTICATE:' + uri + (b':00000000000000000000000000000000' if qop in [b'auth-int', b'auth-conf'] else b'')
|
||||
|
||||
if qop == b'auth-int':
|
||||
connection._digest_md5_kis = md5_h(md5_h(a1) + b"Digest session key to server-to-client signing key magic constant")
|
||||
connection._digest_md5_kic = md5_h(md5_h(a1) + b"Digest session key to client-to-server signing key magic constant")
|
||||
|
||||
digest_response += b'response="' + md5_hex(md5_kd(md5_hex(md5_h(a1)), b':'.join([nonce, b'00000001', cnonce, qop, md5_hex(md5_h(a2))]))) + b'"'
|
||||
|
||||
result = send_sasl_negotiation(connection, controls, digest_response)
|
||||
return result
|
||||
|
||||
|
||||
def decode_directives(directives_string):
|
||||
"""
|
||||
converts directives to dict, unquote values
|
||||
"""
|
||||
|
||||
# old_directives = dict((attr[0], attr[1].strip('"')) for attr in [line.split('=') for line in directives_string.split(',')])
|
||||
state = STATE_KEY
|
||||
tmp_buffer = ''
|
||||
quoting = False
|
||||
key = ''
|
||||
directives = dict()
|
||||
for c in directives_string.decode('utf-8'):
|
||||
if state == STATE_KEY and c == '=':
|
||||
key = tmp_buffer
|
||||
tmp_buffer = ''
|
||||
state = STATE_VALUE
|
||||
elif state == STATE_VALUE and c == '"' and not quoting and not tmp_buffer:
|
||||
quoting = True
|
||||
elif state == STATE_VALUE and c == '"' and quoting:
|
||||
quoting = False
|
||||
elif state == STATE_VALUE and c == ',' and not quoting:
|
||||
directives[key] = tmp_buffer
|
||||
tmp_buffer = ''
|
||||
key = ''
|
||||
state = STATE_KEY
|
||||
else:
|
||||
tmp_buffer += c
|
||||
|
||||
if key and tmp_buffer:
|
||||
directives[key] = tmp_buffer
|
||||
|
||||
return directives
|
||||
32
venv/Lib/site-packages/ldap3/protocol/sasl/external.py
Normal file
32
venv/Lib/site-packages/ldap3/protocol/sasl/external.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
"""
|
||||
|
||||
# Created on 2014.01.04
|
||||
#
|
||||
# Author: Giovanni Cannata
|
||||
#
|
||||
# Copyright 2014 - 2020 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 ...protocol.sasl.sasl import send_sasl_negotiation
|
||||
|
||||
|
||||
def sasl_external(connection, controls):
|
||||
result = send_sasl_negotiation(connection, controls, connection.sasl_credentials)
|
||||
|
||||
return result
|
||||
302
venv/Lib/site-packages/ldap3/protocol/sasl/kerberos.py
Normal file
302
venv/Lib/site-packages/ldap3/protocol/sasl/kerberos.py
Normal file
@@ -0,0 +1,302 @@
|
||||
"""
|
||||
"""
|
||||
|
||||
# Created on 2015.04.08
|
||||
#
|
||||
# Author: Giovanni Cannata
|
||||
#
|
||||
# Copyright 2015 - 2020 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/>.
|
||||
|
||||
# original code by Hugh Cole-Baker, modified by Peter Foley, modified again by Azaria Zornberg
|
||||
# it needs the gssapi package
|
||||
import base64
|
||||
import socket
|
||||
|
||||
from ...core.exceptions import LDAPPackageUnavailableError, LDAPCommunicationError
|
||||
from ...core.rdns import ReverseDnsSetting, get_hostname_by_addr, is_ip_addr
|
||||
|
||||
posix_gssapi_unavailable = True
|
||||
try:
|
||||
# noinspection PyPackageRequirements,PyUnresolvedReferences
|
||||
import gssapi
|
||||
from gssapi.raw import ChannelBindings
|
||||
posix_gssapi_unavailable = False
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
windows_gssapi_unavailable = True
|
||||
# only attempt to import winkerberos if gssapi is unavailable
|
||||
if posix_gssapi_unavailable:
|
||||
try:
|
||||
import winkerberos
|
||||
windows_gssapi_unavailable = False
|
||||
except ImportError:
|
||||
raise LDAPPackageUnavailableError('package gssapi (or winkerberos) missing')
|
||||
|
||||
from .sasl import send_sasl_negotiation, abort_sasl_negotiation
|
||||
|
||||
|
||||
NO_SECURITY_LAYER = 1
|
||||
INTEGRITY_PROTECTION = 2
|
||||
CONFIDENTIALITY_PROTECTION = 4
|
||||
|
||||
|
||||
def get_channel_bindings(ssl_socket):
|
||||
try:
|
||||
server_certificate = ssl_socket.getpeercert(True)
|
||||
except:
|
||||
# it is not SSL socket
|
||||
return None
|
||||
try:
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
except ImportError:
|
||||
raise LDAPPackageUnavailableError('package cryptography missing')
|
||||
cert = x509.load_der_x509_certificate(server_certificate, default_backend())
|
||||
hash_algorithm = cert.signature_hash_algorithm
|
||||
# According to https://tools.ietf.org/html/rfc5929#section-4.1, we have to convert the the hash function for md5 and sha1
|
||||
if hash_algorithm.name in ('md5', 'sha1'):
|
||||
digest = hashes.Hash(hashes.SHA256(), default_backend())
|
||||
else:
|
||||
digest = hashes.Hash(hash_algorithm, default_backend())
|
||||
digest.update(server_certificate)
|
||||
application_data = b'tls-server-end-point:' + digest.finalize()
|
||||
# posix gssapi and windows winkerberos use different channel bindings classes
|
||||
if not posix_gssapi_unavailable:
|
||||
return ChannelBindings(application_data=application_data)
|
||||
else:
|
||||
return winkerberos.channelBindings(application_data=application_data)
|
||||
|
||||
|
||||
def sasl_gssapi(connection, controls):
|
||||
"""
|
||||
Performs a bind using the Kerberos v5 ("GSSAPI") SASL mechanism
|
||||
from RFC 4752. Does not support any security layers, only authentication!
|
||||
|
||||
sasl_credentials can be empty or a tuple with one or two elements.
|
||||
The first element determines which service principal to request a ticket for and can be one of the following:
|
||||
|
||||
- None or False, to use the hostname from the Server object
|
||||
- True to perform a reverse DNS lookup to retrieve the canonical hostname for the hosts IP address
|
||||
- A string containing the hostname
|
||||
|
||||
The optional second element is what authorization ID to request.
|
||||
|
||||
- If omitted or None, the authentication ID is used as the authorization ID
|
||||
- If a string, the authorization ID to use. Should start with "dn:" or "user:".
|
||||
|
||||
The optional third element is a raw gssapi credentials structure which can be used over
|
||||
the implicit use of a krb ccache.
|
||||
"""
|
||||
if not posix_gssapi_unavailable:
|
||||
return _posix_sasl_gssapi(connection, controls)
|
||||
else:
|
||||
return _windows_sasl_gssapi(connection, controls)
|
||||
|
||||
|
||||
def _common_determine_target_name(connection):
|
||||
""" Common logic for determining our target name for kerberos negotiation, regardless of whether we are using
|
||||
gssapi on a posix system or winkerberos on windows.
|
||||
Returns a string in the form "ldap@" + a target hostname.
|
||||
The hostname can either be user specified, come from the connection, or resolved using reverse DNS based
|
||||
on parameters set in sasl_credentials.
|
||||
The default if no sasl_credentials are specified is to use the host in the connection server object.
|
||||
"""
|
||||
# if we don't have any sasl_credentials specified, or the first entry is False (which is the legacy equivalent
|
||||
# to ReverseDnsSetting.OFF that has value 0) then our gssapi name is just
|
||||
if (not connection.sasl_credentials or len(connection.sasl_credentials) == 0
|
||||
or not connection.sasl_credentials[0]):
|
||||
return 'ldap@' + connection.server.host
|
||||
# older code will still be using a boolean True for the equivalent of
|
||||
# ReverseDnsSetting.REQUIRE_RESOLVE_ALL_ADDRESSES
|
||||
if connection.sasl_credentials[0] is True:
|
||||
hostname = get_hostname_by_addr(connection.socket.getpeername()[0])
|
||||
target_name = 'ldap@' + hostname
|
||||
elif connection.sasl_credentials[0] in ReverseDnsSetting.SUPPORTED_VALUES:
|
||||
rdns_setting = connection.sasl_credentials[0]
|
||||
# if the rdns_setting is OFF then we won't enter any branch here and will leave hostname as server host,
|
||||
# so we'll just use the server host, whatever it is
|
||||
peer_ip = connection.socket.getpeername()[0]
|
||||
hostname = connection.server.host
|
||||
if rdns_setting == ReverseDnsSetting.REQUIRE_RESOLVE_ALL_ADDRESSES:
|
||||
# resolve our peer ip and use it as our target name
|
||||
hostname = get_hostname_by_addr(peer_ip)
|
||||
elif rdns_setting == ReverseDnsSetting.REQUIRE_RESOLVE_IP_ADDRESSES_ONLY:
|
||||
# resolve our peer ip (if the server host is an ip address) and use it as our target name
|
||||
if is_ip_addr(hostname):
|
||||
hostname = get_hostname_by_addr(peer_ip)
|
||||
elif rdns_setting == ReverseDnsSetting.OPTIONAL_RESOLVE_ALL_ADDRESSES:
|
||||
# try to resolve our peer ip in dns and if we can, use it as our target name.
|
||||
# if not, just use the server host
|
||||
resolved_hostname = get_hostname_by_addr(peer_ip, success_required=False)
|
||||
if resolved_hostname is not None:
|
||||
hostname = resolved_hostname
|
||||
elif rdns_setting == ReverseDnsSetting.OPTIONAL_RESOLVE_IP_ADDRESSES_ONLY:
|
||||
# try to resolve our peer ip in dns if our server host is an ip. if we can, use it as our target
|
||||
# name. if not, just use the server host
|
||||
if is_ip_addr(hostname):
|
||||
resolved_hostname = get_hostname_by_addr(peer_ip, success_required=False)
|
||||
if resolved_hostname is not None:
|
||||
hostname = resolved_hostname
|
||||
# construct our target name
|
||||
target_name = 'ldap@' + hostname
|
||||
else: # string hostname directly provided
|
||||
target_name = 'ldap@' + connection.sasl_credentials[0]
|
||||
return target_name
|
||||
|
||||
|
||||
def _common_determine_authz_id_and_creds(connection):
|
||||
""" Given our connection, figure out the authorization id (i.e. the kerberos principal) and kerberos credentials
|
||||
being used for our SASL bind.
|
||||
On posix systems, we can actively negotiate with raw credentials and receive a kerberos client ticket during our
|
||||
SASL bind. So credentials can be specified.
|
||||
However, on windows systems, winkerberos expects to use the credentials cached by the system because windows
|
||||
machines are generally domain-joined. So no initiation is supported, as the TGT must already be valid prior
|
||||
to beginning the SASL bind, and the windows system credentials will be used as needed with that.
|
||||
"""
|
||||
authz_id = b""
|
||||
creds = None
|
||||
# the default value for credentials is only something we should instantiate on systems using the
|
||||
# posix GSSAPI, as windows kerberos expects that a tgt already exists (i.e. the machine is domain-joined)
|
||||
# and does not support initiation from raw credentials
|
||||
if not posix_gssapi_unavailable:
|
||||
creds = gssapi.Credentials(name=gssapi.Name(connection.user), usage='initiate', store=connection.cred_store) if connection.user else None
|
||||
if connection.sasl_credentials:
|
||||
if len(connection.sasl_credentials) >= 2 and connection.sasl_credentials[1]:
|
||||
authz_id = connection.sasl_credentials[1].encode("utf-8")
|
||||
if len(connection.sasl_credentials) >= 3 and connection.sasl_credentials[2]:
|
||||
if posix_gssapi_unavailable:
|
||||
raise LDAPPackageUnavailableError('The winkerberos package does not support specifying raw credentials'
|
||||
'to initiate GSSAPI Kerberos communication. A ticket granting ticket '
|
||||
'must have already been obtained for the user before beginning a '
|
||||
'SASL bind.')
|
||||
raw_creds = connection.sasl_credentials[2]
|
||||
creds = gssapi.Credentials(base=raw_creds, usage='initiate', store=connection.cred_store)
|
||||
return authz_id, creds
|
||||
|
||||
|
||||
def _common_process_end_token_get_security_layers(negotiated_token):
|
||||
""" Process the response we got at the end of our SASL negotiation wherein the server told us what
|
||||
minimum security layers we need, and return a bytearray for the client security layers we want.
|
||||
This function throws an error on a malformed token from the server.
|
||||
The ldap3 library does not support security layers, and only supports authentication with kerberos,
|
||||
so an error will be thrown for any tokens that indicate a security layer requirement.
|
||||
"""
|
||||
if len(negotiated_token) != 4:
|
||||
raise LDAPCommunicationError("Incorrect response from server")
|
||||
|
||||
server_security_layers = negotiated_token[0]
|
||||
if not isinstance(server_security_layers, int):
|
||||
server_security_layers = ord(server_security_layers)
|
||||
if server_security_layers in (0, NO_SECURITY_LAYER):
|
||||
if negotiated_token[1:] != '\x00\x00\x00':
|
||||
raise LDAPCommunicationError("Server max buffer size must be 0 if no security layer")
|
||||
if not (server_security_layers & NO_SECURITY_LAYER):
|
||||
raise LDAPCommunicationError("Server requires a security layer, but this is not implemented")
|
||||
|
||||
# this is here to encourage anyone implementing client security layers to do it
|
||||
# for both windows and posix
|
||||
client_security_layers = bytearray([NO_SECURITY_LAYER, 0, 0, 0])
|
||||
return client_security_layers
|
||||
|
||||
def _posix_sasl_gssapi(connection, controls):
|
||||
""" Performs a bind using the Kerberos v5 ("GSSAPI") SASL mechanism
|
||||
from RFC 4752 using the gssapi package that works natively on most
|
||||
posix operating systems.
|
||||
"""
|
||||
target_name = gssapi.Name(_common_determine_target_name(connection), gssapi.NameType.hostbased_service)
|
||||
authz_id, creds = _common_determine_authz_id_and_creds(connection)
|
||||
|
||||
ctx = gssapi.SecurityContext(name=target_name, mech=gssapi.MechType.kerberos, creds=creds,
|
||||
channel_bindings=get_channel_bindings(connection.socket))
|
||||
in_token = None
|
||||
try:
|
||||
while True:
|
||||
out_token = ctx.step(in_token)
|
||||
if out_token is None:
|
||||
out_token = ''
|
||||
result = send_sasl_negotiation(connection, controls, out_token)
|
||||
in_token = result['saslCreds']
|
||||
try:
|
||||
# This raised an exception in gssapi<1.1.2 if the context was
|
||||
# incomplete, but was fixed in
|
||||
# https://github.com/pythongssapi/python-gssapi/pull/70
|
||||
if ctx.complete:
|
||||
break
|
||||
except gssapi.exceptions.MissingContextError:
|
||||
pass
|
||||
|
||||
unwrapped_token = ctx.unwrap(in_token)
|
||||
client_security_layers = _common_process_end_token_get_security_layers(unwrapped_token.message)
|
||||
out_token = ctx.wrap(bytes(client_security_layers)+authz_id, False)
|
||||
return send_sasl_negotiation(connection, controls, out_token.message)
|
||||
except (gssapi.exceptions.GSSError, LDAPCommunicationError):
|
||||
abort_sasl_negotiation(connection, controls)
|
||||
raise
|
||||
|
||||
|
||||
def _windows_sasl_gssapi(connection, controls):
|
||||
""" Performs a bind using the Kerberos v5 ("GSSAPI") SASL mechanism
|
||||
from RFC 4752 using the winkerberos package that works natively on most
|
||||
windows operating systems.
|
||||
"""
|
||||
target_name = _common_determine_target_name(connection)
|
||||
# initiation happens before beginning the SASL bind when using windows kerberos
|
||||
authz_id, _ = _common_determine_authz_id_and_creds(connection)
|
||||
gssflags = (
|
||||
winkerberos.GSS_C_MUTUAL_FLAG |
|
||||
winkerberos.GSS_C_SEQUENCE_FLAG |
|
||||
winkerberos.GSS_C_INTEG_FLAG |
|
||||
winkerberos.GSS_C_CONF_FLAG
|
||||
)
|
||||
_, ctx = winkerberos.authGSSClientInit(target_name, gssflags=gssflags)
|
||||
|
||||
in_token = b''
|
||||
try:
|
||||
negotiation_complete = False
|
||||
while not negotiation_complete:
|
||||
# GSSAPI is a "client goes first" SASL mechanism. Send the first "response" to the server and
|
||||
# recieve its first challenge.
|
||||
# Despite this, we can get channel binding, which includes CBTs for windows environments computed from
|
||||
# the peer certificate, before starting.
|
||||
status = winkerberos.authGSSClientStep(ctx, base64.b64encode(in_token).decode('utf-8'),
|
||||
channel_bindings=get_channel_bindings(connection.socket))
|
||||
# figure out if we're done with our sasl negotiation
|
||||
negotiation_complete = (status == winkerberos.AUTH_GSS_COMPLETE)
|
||||
out_token = winkerberos.authGSSClientResponse(ctx) or ''
|
||||
out_token_bytes = base64.b64decode(out_token)
|
||||
result = send_sasl_negotiation(connection, controls, out_token_bytes)
|
||||
in_token = result['saslCreds'] or b''
|
||||
|
||||
winkerberos.authGSSClientUnwrap( ctx,base64.b64encode(in_token).decode('utf-8'))
|
||||
negotiated_token = ''
|
||||
if winkerberos.authGSSClientResponse(ctx):
|
||||
negotiated_token = base64.standard_b64decode(winkerberos.authGSSClientResponse(ctx))
|
||||
client_security_layers = _common_process_end_token_get_security_layers(negotiated_token)
|
||||
# manually construct a message indicating use of authorization-only layer
|
||||
# see winkerberos example: https://github.com/mongodb/winkerberos/blob/master/test/test_winkerberos.py
|
||||
authz_only_msg = base64.b64encode(bytes(client_security_layers) + authz_id).decode('utf-8')
|
||||
winkerberos.authGSSClientWrap(ctx, authz_only_msg)
|
||||
out_token = winkerberos.authGSSClientResponse(ctx) or ''
|
||||
|
||||
return send_sasl_negotiation(connection, controls, base64.b64decode(out_token))
|
||||
except (winkerberos.GSSError, LDAPCommunicationError):
|
||||
abort_sasl_negotiation(connection, controls)
|
||||
raise
|
||||
70
venv/Lib/site-packages/ldap3/protocol/sasl/plain.py
Normal file
70
venv/Lib/site-packages/ldap3/protocol/sasl/plain.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
"""
|
||||
|
||||
# Created on 2014.01.04
|
||||
#
|
||||
# Author: Giovanni Cannata
|
||||
#
|
||||
# Copyright 2014 - 2020 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/>.
|
||||
|
||||
# payload for PLAIN mechanism
|
||||
# message = [authzid] UTF8NUL authcid UTF8NUL passwd
|
||||
# authcid = 1*SAFE ; MUST accept up to 255 octets
|
||||
# authzid = 1*SAFE ; MUST accept up to 255 octets
|
||||
# passwd = 1*SAFE ; MUST accept up to 255 octets
|
||||
# UTF8NUL = %x00 ; UTF-8 encoded NUL character
|
||||
#
|
||||
# SAFE = UTF1 / UTF2 / UTF3 / UTF4
|
||||
# ;; any UTF-8 encoded Unicode character except NUL
|
||||
#
|
||||
# UTF1 = %x01-7F ;; except NUL
|
||||
# UTF2 = %xC2-DF UTF0
|
||||
# UTF3 = %xE0 %xA0-BF UTF0 / %xE1-EC 2(UTF0) /
|
||||
# %xED %x80-9F UTF0 / %xEE-EF 2(UTF0)
|
||||
# UTF4 = %xF0 %x90-BF 2(UTF0) / %xF1-F3 3(UTF0) /
|
||||
# %xF4 %x80-8F 2(UTF0)
|
||||
# UTF0 = %x80-BF
|
||||
|
||||
from ...protocol.sasl.sasl import send_sasl_negotiation
|
||||
from .sasl import sasl_prep
|
||||
from ...utils.conv import to_raw, to_unicode
|
||||
|
||||
|
||||
def sasl_plain(connection, controls):
|
||||
authzid = connection.sasl_credentials[0]
|
||||
authcid = connection.sasl_credentials[1]
|
||||
passwd = connection.sasl_credentials[2]
|
||||
|
||||
payload = b''
|
||||
if authzid:
|
||||
payload += to_raw(sasl_prep(to_unicode(authzid)))
|
||||
|
||||
payload += b'\0'
|
||||
|
||||
if authcid:
|
||||
payload += to_raw(sasl_prep(to_unicode(authcid)))
|
||||
|
||||
payload += b'\0'
|
||||
|
||||
if passwd:
|
||||
payload += to_raw(sasl_prep(to_unicode(passwd)))
|
||||
|
||||
result = send_sasl_negotiation(connection, controls, payload)
|
||||
|
||||
return result
|
||||
171
venv/Lib/site-packages/ldap3/protocol/sasl/sasl.py
Normal file
171
venv/Lib/site-packages/ldap3/protocol/sasl/sasl.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
"""
|
||||
|
||||
# Created on 2013.09.11
|
||||
#
|
||||
# Author: Giovanni Cannata
|
||||
#
|
||||
# Copyright 2013 - 2020 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/>.
|
||||
|
||||
import stringprep
|
||||
from unicodedata import ucd_3_2_0 as unicode32
|
||||
from os import urandom
|
||||
from binascii import hexlify
|
||||
|
||||
from ... import SASL
|
||||
from ...core.results import RESULT_AUTH_METHOD_NOT_SUPPORTED
|
||||
from ...core.exceptions import LDAPSASLPrepError, LDAPPasswordIsMandatoryError
|
||||
|
||||
|
||||
def sasl_prep(data):
|
||||
"""
|
||||
implement SASLPrep profile as per RFC4013:
|
||||
it defines the "SASLprep" profile of the "stringprep" algorithm [StringPrep].
|
||||
The profile is designed for use in Simple Authentication and Security
|
||||
Layer ([SASL]) mechanisms, such as [PLAIN], [CRAM-MD5], and
|
||||
[DIGEST-MD5]. It may be applicable where simple user names and
|
||||
passwords are used. This profile is not intended for use in
|
||||
preparing identity strings that are not simple user names (e.g.,
|
||||
email addresses, domain names, distinguished names), or where
|
||||
identity or password strings that are not character data, or require
|
||||
different handling (e.g., case folding).
|
||||
"""
|
||||
|
||||
# mapping
|
||||
prepared_data = ''
|
||||
for c in data:
|
||||
if stringprep.in_table_c12(c):
|
||||
# non-ASCII space characters [StringPrep, C.1.2] that can be mapped to SPACE (U+0020)
|
||||
prepared_data += ' '
|
||||
elif stringprep.in_table_b1(c):
|
||||
# the "commonly mapped to nothing" characters [StringPrep, B.1] that can be mapped to nothing.
|
||||
pass
|
||||
else:
|
||||
prepared_data += c
|
||||
|
||||
# normalizing
|
||||
# This profile specifies using Unicode normalization form KC
|
||||
# The repertoire is Unicode 3.2 as per RFC 4013 (2)
|
||||
|
||||
prepared_data = unicode32.normalize('NFKC', prepared_data)
|
||||
|
||||
if not prepared_data:
|
||||
raise LDAPSASLPrepError('SASLprep error: unable to normalize string')
|
||||
|
||||
# prohibit
|
||||
for c in prepared_data:
|
||||
if stringprep.in_table_c12(c):
|
||||
# Non-ASCII space characters [StringPrep, C.1.2]
|
||||
raise LDAPSASLPrepError('SASLprep error: non-ASCII space character present')
|
||||
elif stringprep.in_table_c21(c):
|
||||
# ASCII control characters [StringPrep, C.2.1]
|
||||
raise LDAPSASLPrepError('SASLprep error: ASCII control character present')
|
||||
elif stringprep.in_table_c22(c):
|
||||
# Non-ASCII control characters [StringPrep, C.2.2]
|
||||
raise LDAPSASLPrepError('SASLprep error: non-ASCII control character present')
|
||||
elif stringprep.in_table_c3(c):
|
||||
# Private Use characters [StringPrep, C.3]
|
||||
raise LDAPSASLPrepError('SASLprep error: private character present')
|
||||
elif stringprep.in_table_c4(c):
|
||||
# Non-character code points [StringPrep, C.4]
|
||||
raise LDAPSASLPrepError('SASLprep error: non-character code point present')
|
||||
elif stringprep.in_table_c5(c):
|
||||
# Surrogate code points [StringPrep, C.5]
|
||||
raise LDAPSASLPrepError('SASLprep error: surrogate code point present')
|
||||
elif stringprep.in_table_c6(c):
|
||||
# Inappropriate for plain text characters [StringPrep, C.6]
|
||||
raise LDAPSASLPrepError('SASLprep error: inappropriate for plain text character present')
|
||||
elif stringprep.in_table_c7(c):
|
||||
# Inappropriate for canonical representation characters [StringPrep, C.7]
|
||||
raise LDAPSASLPrepError('SASLprep error: inappropriate for canonical representation character present')
|
||||
elif stringprep.in_table_c8(c):
|
||||
# Change display properties or deprecated characters [StringPrep, C.8]
|
||||
raise LDAPSASLPrepError('SASLprep error: change display property or deprecated character present')
|
||||
elif stringprep.in_table_c9(c):
|
||||
# Tagging characters [StringPrep, C.9]
|
||||
raise LDAPSASLPrepError('SASLprep error: tagging character present')
|
||||
|
||||
# check bidi
|
||||
# if a string contains any r_and_al_cat character, the string MUST NOT contain any l_cat character.
|
||||
flag_r_and_al_cat = False
|
||||
flag_l_cat = False
|
||||
for c in prepared_data:
|
||||
if stringprep.in_table_d1(c):
|
||||
flag_r_and_al_cat = True
|
||||
elif stringprep.in_table_d2(c):
|
||||
flag_l_cat = True
|
||||
|
||||
if flag_r_and_al_cat and flag_l_cat:
|
||||
raise LDAPSASLPrepError('SASLprep error: string cannot contain (R or AL) and L bidirectional chars')
|
||||
|
||||
# If a string contains any r_and_al_cat character, a r_and_al_cat character MUST be the first character of the string
|
||||
# and a r_and_al_cat character MUST be the last character of the string.
|
||||
if flag_r_and_al_cat and not stringprep.in_table_d1(prepared_data[0]) and not stringprep.in_table_d2(prepared_data[-1]):
|
||||
raise LDAPSASLPrepError('r_and_al_cat character present, must be first and last character of the string')
|
||||
|
||||
return prepared_data
|
||||
|
||||
|
||||
def validate_simple_password(password, accept_empty=False):
|
||||
"""
|
||||
validate simple password as per RFC4013 using sasl_prep:
|
||||
"""
|
||||
|
||||
if accept_empty and not password:
|
||||
return password
|
||||
elif not password:
|
||||
raise LDAPPasswordIsMandatoryError("simple password can't be empty")
|
||||
|
||||
if not isinstance(password, bytes): # bytes are returned raw, as per RFC (4.2)
|
||||
password = sasl_prep(password)
|
||||
if not isinstance(password, bytes):
|
||||
password = password.encode('utf-8')
|
||||
|
||||
return password
|
||||
|
||||
|
||||
def abort_sasl_negotiation(connection, controls):
|
||||
from ...operation.bind import bind_operation
|
||||
|
||||
request = bind_operation(connection.version, SASL, None, None, '', None)
|
||||
response = connection.post_send_single_response(connection.send('bindRequest', request, controls))
|
||||
if connection.strategy.sync:
|
||||
result = connection.result
|
||||
else:
|
||||
result = connection.get_response(response)[0][0]
|
||||
|
||||
return True if result['result'] == RESULT_AUTH_METHOD_NOT_SUPPORTED else False
|
||||
|
||||
|
||||
def send_sasl_negotiation(connection, controls, payload):
|
||||
from ...operation.bind import bind_operation
|
||||
|
||||
request = bind_operation(connection.version, SASL, None, None, connection.sasl_mechanism, payload)
|
||||
response = connection.post_send_single_response(connection.send('bindRequest', request, controls))
|
||||
|
||||
if connection.strategy.sync:
|
||||
result = connection.result
|
||||
else:
|
||||
_, result = connection.get_response(response)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def random_hex_string(size):
|
||||
return str(hexlify(urandom(size)).decode('ascii')) # str fix for Python 2
|
||||
Reference in New Issue
Block a user