Skip to content

Commit 56e9251

Browse files
author
Michael Schlenker
committed
Proof of concept for SASL GSSAPI changes
1 parent 42ce859 commit 56e9251

File tree

6 files changed

+260
-27
lines changed

6 files changed

+260
-27
lines changed

bin/ldaptor-search

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,46 @@
11
#!/usr/bin/python
22

33
import sys
4-
from ldaptor.protocols.ldap import ldapclient, ldif, ldapsyntax, ldapconnector
5-
from ldaptor.protocols import pureber, pureldap
6-
from ldaptor import usage, ldapfilter, config
7-
from twisted.internet import protocol, reactor, defer
4+
from ldaptor.protocols.ldap import ldapclient, ldapsyntax, ldapconnector
5+
from ldaptor import usage, config
6+
from twisted.internet import reactor
87

98
def printResults(o):
109
sys.stdout.write(str(o))
1110

12-
def search(client, baseDN, filter_text, attributes):
13-
o=ldapsyntax.LDAPEntry(client=client, dn=baseDN)
14-
d=o.search(filterText=filter_text,
15-
attributes=attributes,
16-
callback=printResults)
11+
12+
def do_anon_bind(client, bindDN):
13+
o=ldapsyntax.LDAPEntryWithClient(client, bindDN)
14+
d = o.bind(None)
15+
d.addCallback(lambda x: client)
1716
return d
1817

18+
19+
def do_sasl_bind(client, bindDN):
20+
from ldaptor import gssapi
21+
o=ldapsyntax.LDAPEntryWithClient(client, bindDN)
22+
service = 'ldap'
23+
host = client.transport.addr[0]
24+
ctx = gssapi.SASL_GSSAPIClientContext(service, host)
25+
d = o.bind(None, sasl=True, sasl_ctx=ctx)
26+
d.addCallback(lambda x: client)
27+
return d
28+
29+
30+
def do_search(client, baseDN, filter_text, attributes):
31+
o=ldapsyntax.LDAPEntryWithClient(client, baseDN)
32+
return o.search(filterText=filter_text, attributes=attributes, callback=printResults)
33+
34+
1935
exitStatus=0
2036

2137
def error(fail):
2238
print >>sys.stderr, 'fail:', fail.getErrorMessage()
39+
print >>sys.stderr, fail.getTraceback()
2340
global exitStatus
2441
exitStatus=1
2542

26-
def main(cfg, filter_text, attributes):
43+
def main(cfg, bindfunc, bindDN, filter_text, attributes):
2744
try:
2845
baseDN = cfg.getBaseDN()
2946
except config.MissingBaseDNError, e:
@@ -32,16 +49,21 @@ def main(cfg, filter_text, attributes):
3249

3350
c = ldapconnector.LDAPClientCreator(reactor,
3451
ldapclient.LDAPClient)
35-
d = c.connectAnonymously(dn=baseDN,
36-
overrides=cfg.getServiceLocationOverrides())
37-
d.addCallback(search, baseDN, filter_text, attributes)
52+
d = c.connect(dn=baseDN,
53+
overrides=cfg.getServiceLocationOverrides())
54+
55+
if not bindDN:
56+
bindDN = baseDN
57+
d.addCallback(bindfunc, bindDN)
58+
d.addCallback(do_search, baseDN, filter_text, attributes)
3859
d.addErrback(error)
3960
d.addBoth(lambda x: reactor.stop())
4061

4162
reactor.run()
4263
sys.exit(exitStatus)
4364

44-
class MyOptions(usage.Options, usage.Options_service_location, usage.Options_base_optional):
65+
class MyOptions(usage.Options, usage.Options_service_location,
66+
usage.Options_base_optional, usage.Options_bind):
4567
"""LDAPtor command line search utility"""
4668

4769
def parseArgs(self, filter, *attributes):
@@ -58,6 +80,14 @@ if __name__ == "__main__":
5880

5981
cfg = config.LDAPConfig(baseDN=opts['base'],
6082
serviceLocationOverrides=opts['service-location'])
83+
84+
if opts['bind-sasl-mech'] == 'GSSAPI':
85+
bindfunc = do_sasl_bind
86+
else:
87+
bindfunc = do_anon_bind
88+
6189
main(cfg,
90+
bindfunc,
91+
opts['binddn'],
6292
opts['filter'],
6393
opts['attributes'])

ldaptor/gssapi.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# -*- python -*-
2+
#
3+
# GSSAPI SASL Code for LDAP Auth
4+
#
5+
# This implements the RFC 4752 SASL Mechanism GSSAPI for ldaptor
6+
#
7+
# (c) 2016 CONTACT SOFTWARE GmbH (www.contact-software.com)
8+
#
9+
# MIT License.
10+
#
11+
# Permission is hereby granted, free of charge, to any person obtaining
12+
# a copy of this software and associated documentation files (the
13+
# "Software"), to deal in the Software without restriction, including
14+
# without limitation the rights to use, copy, modify, merge, publish,
15+
# distribute, sublicense, and/or sell copies of the Software, and to
16+
# permit persons to whom the Software is furnished to do so, subject to
17+
# the following conditions:
18+
#
19+
# The above copyright notice and this permission notice shall be
20+
# included in all copies or substantial portions of the Software.
21+
#
22+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
26+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
27+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
28+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29+
#
30+
31+
import sys
32+
import base64
33+
import struct
34+
35+
if sys.platform == 'win32':
36+
import kerberos_sspi as kerberos
37+
else:
38+
import kerberos
39+
40+
__all__ = [
41+
'SASL_GSSAPIClientContext',
42+
'SASL_GSSAPIServerContext',
43+
]
44+
45+
46+
# RFC 4752 Sect. 3
47+
SASL_MECHANISM = 'GSSAPI'
48+
49+
50+
class SASL_GSSAPIClientContext(object):
51+
52+
def __init__(self, service, host):
53+
spn = "%s@%s" % (service, host)
54+
self.client = None
55+
self.response = ""
56+
self.cres = kerberos.AUTH_GSS_CONTINUE
57+
self.ctx = None
58+
59+
flags = (kerberos.GSS_C_CONF_FLAG
60+
| kerberos.GSS_C_INTEG_FLAG
61+
| kerberos.GSS_C_REPLAY_FLAG
62+
| kerberos.GSS_C_SEQUENCE_FLAG)
63+
64+
errc, self.client = kerberos.authGSSClientInit(spn, gssflags=flags)
65+
66+
self._round = 0
67+
68+
def send(self, token_in):
69+
if not self.ctx:
70+
self.ctx = self._coro()
71+
# Move to first waiting yield
72+
self.ctx.send(None)
73+
return self.ctx.send(token_in)
74+
75+
def __del__(self):
76+
# TODO: Find a nicer way to trigger cleanup
77+
self.ctx = None
78+
client = self.client
79+
self.client = None
80+
if client:
81+
kerberos.authGSSClientClean(client)
82+
83+
def start(self):
84+
ctx = self._coro()
85+
# Move to first waiting yield
86+
ctx.send(None)
87+
return ctx
88+
89+
def _handle_sasl_gssapi(self, token_in):
90+
if sys.platform == "win32":
91+
return self._handle_sasl_gssapi_win32(token_in)
92+
else:
93+
return self._handle_sasl_gssapi_unix(token_in)
94+
95+
def _handle_sasl_gssapi_win32(self, token_in):
96+
# TODO: Simplify if kerberos_sspi gets fixed and authGSSClientUnwrap()
97+
# works as it should.
98+
# (https://github.com/may-day/kerberos-sspi/pull/3)
99+
code = kerberos.authGSSClientUnwrap(
100+
self.ctx, base64.encodestring(token_in))
101+
if code == -1:
102+
raise RuntimeError("SASL GSSAPI Auth failed")
103+
104+
data = kerberos.authGSSClientResponse(self.ctx)
105+
data = self._process_security_options(data)
106+
import sspicon
107+
import win32security
108+
ca = self.ctx['csa']
109+
context = self.ctx
110+
pkg_size_info = ca.ctxt.QueryContextAttributes(sspicon.SECPKG_ATTR_SIZES)
111+
trailersize = pkg_size_info['SecurityTrailer']
112+
blocksize = pkg_size_info['BlockSize']
113+
114+
encbuf = win32security.PySecBufferDescType()
115+
encbuf.append(win32security.PySecBufferType(trailersize, sspicon.SECBUFFER_TOKEN))
116+
encbuf.append(win32security.PySecBufferType(len(data), sspicon.SECBUFFER_DATA))
117+
encbuf.append(win32security.PySecBufferType(blocksize, sspicon.SECBUFFER_PADDING))
118+
encbuf[1].Buffer = data
119+
ca.ctxt.EncryptMessage(0, encbuf, ca._get_next_seq_num())
120+
121+
context["response"] = encbuf[0].Buffer+encbuf[1].Buffer+encbuf[2].Buffer
122+
self.response = kerberos.authGSSClientResponse(self.ctx)
123+
124+
def _handle_sasl_gssapi_unix(self, token_in):
125+
# TODO: Probably needs a fix similar to kerberos_sspi
126+
pass
127+
128+
def _process_security_options(self, data, user=None):
129+
"""
130+
Handle the security layer settings
131+
"""
132+
conf_and_size = data[:struct.calcsize("!L")] # network unsigned long
133+
size = struct.unpack("!L", conf_and_size)[0] & 0x00ffffff
134+
conf = struct.unpack("B", conf_and_size[0])[0] # B .. unsigned char
135+
136+
# FIXME: Debug prints...
137+
print "N" if conf & kerberos.GSS_AUTH_P_NONE else "-"
138+
print "I" if conf & kerberos.GSS_AUTH_P_INTEGRITY else "-"
139+
print "P" if conf & kerberos.GSS_AUTH_P_PRIVACY else "-"
140+
print "Maximum GSS token size is %d" % size
141+
142+
# Tell the truth, we do not handle any security layer
143+
# (aka GSS_AUTH_P_NONE). RFC 4752 demands that the
144+
# max client message size is zero in this case.
145+
max_size_client_message = 0
146+
security_layer = kerberos.GSS_AUTH_P_NONE
147+
data = struct.pack("!L", security_layer << 24 +
148+
(max_size_client_message & 0x00ffffff))
149+
if user:
150+
data = data + user.encode("utf-8")
151+
return data
152+
153+
def _coro(self):
154+
"""
155+
Statemachine for the SASL progress
156+
"""
157+
token_in = yield
158+
while self.cres == kerberos.AUTH_GSS_CONTINUE:
159+
self.cres = kerberos.authGSSClientStep(
160+
self.ctx,
161+
base64.encodestring(token_in) if token_in is not None else None
162+
)
163+
if self.cres == -1:
164+
break
165+
self.response = kerberos.authGSSClientResponse(self.ctx)
166+
self._round += 1
167+
token_in = yield (SASL_MECHANISM, base64.decodestring(self.response))
168+
if self.cres == kerberos.AUTH_GSS_COMPLETE:
169+
token_in = yield (SASL_MECHANISM, base64.decodestring(self.response))
170+
self._handle_sasl_gssapi(token_in)
171+
yield (SASL_MECHANISM, base64.decodestring(self.response))
172+
else:
173+
raise RuntimeError("Unexpected extra token. Auth Failed")
174+
175+
176+
class SASL_GSSAPIServerContext(object):
177+
def __init__(self):
178+
pass

ldaptor/interfaces.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def __len__(self):
116116
def __nonzero__(self):
117117
"""Always return True"""
118118

119-
def bind(self, password):
119+
def bind(self, password, sasl=False, sasl_ctx=None):
120120
"""
121121
Try to authenticate with given secret.
122122

ldaptor/protocols/ldap/ldapsyntax.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -260,12 +260,27 @@ def __len__(self):
260260
def __nonzero__(self):
261261
return True
262262

263-
def bind(self, password):
264-
r=pureldap.LDAPBindRequest(dn=str(self.dn), auth=password)
263+
def bind(self, password, sasl=False, sasl_ctx=None):
264+
if sasl and sasl_ctx:
265+
password = sasl_ctx.send(password)
266+
r=pureldap.LDAPBindRequest(dn=str(self.dn), auth=password, sasl=sasl)
265267
d = self.client.send(r)
266-
d.addCallback(self._handle_bind_msg)
268+
if sasl:
269+
d.addCallback(self._handle_sasl_bind_msg, sasl_ctx)
270+
else:
271+
d.addCallback(self._handle_bind_msg)
267272
return d
268273

274+
def _handle_sasl_bind_msg(self, msg, sasl_ctx):
275+
assert isinstance(msg, pureldap.LDAPBindResponse)
276+
assert msg.referral is None #TODO
277+
if sasl_ctx and msg.resultCode == ldaperrors.LDAPSaslBindInProgress.resultCode:
278+
token_in = msg.serverSaslCreds.value
279+
return self.bind(token_in, sasl=True, sasl_ctx=sasl_ctx)
280+
elif msg.resultCode!=ldaperrors.Success.resultCode:
281+
raise ldaperrors.get(msg.resultCode, msg.errorMessage)
282+
return self
283+
269284
def _handle_bind_msg(self, msg):
270285
assert isinstance(msg, pureldap.LDAPBindResponse)
271286
assert msg.referral is None #TODO

ldaptor/protocols/pureldap.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -587,9 +587,9 @@ class LDAPMatchingRuleAssertion(BERSequence):
587587
dnAttributes=None
588588

589589
def fromBER(klass, tag, content, berdecoder=None):
590-
matchingRule = None
591-
atype = None
592-
matchValue = None
590+
matchingRule = None
591+
atype = None
592+
matchValue = None
593593
dnAttributes = None
594594
l = berDecodeMultiple(content, LDAPBERDecoderContext_MatchingRuleAssertion(fallback=berdecoder, inherit=berdecoder))
595595
assert 1 <= len(l) <= 4
@@ -1362,13 +1362,13 @@ def __init__(self, resultCode=None, matchedDN=None, errorMessage=None,
13621362
referral=None, serverSaslCreds=None,
13631363
responseName=None, response=None,
13641364
tag=None):
1365-
LDAPExtendedResponse.__init__(self,
1366-
resultCode=resultCode,
1367-
matchedDN=matchedDN,
1365+
LDAPExtendedResponse.__init__(self,
1366+
resultCode=resultCode,
1367+
matchedDN=matchedDN,
13681368
errorMessage=errorMessage,
1369-
referral=referral,
1369+
referral=referral,
13701370
serverSaslCreds=serverSaslCreds,
1371-
responseName=responseName,
1371+
responseName=responseName,
13721372
response=response,
13731373
tag=tag)
13741374

ldaptor/usage.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ class Options_bind:
9595
"use Distinguished Name to bind to the directory"),
9696
('bind-auth-fd', None, None,
9797
"read bind password from filedescriptor"),
98-
)
98+
('bind-sasl-mech', None, None,
99+
"SASL Mechanism to use for binding to the directory"),
100+
)
99101

100102
def postOptions_bind_auth_fd_numeric(self):
101103
val=self.opts['bind-auth-fd']
@@ -106,6 +108,14 @@ def postOptions_bind_auth_fd_numeric(self):
106108
raise usage.UsageError, "%s value must be numeric" % 'bind-auth-fd'
107109
self.opts['bind-auth-fd'] = val
108110

111+
def postOptions_bind_sasl_mech(self):
112+
val = self.opts['bind-sasl-mech']
113+
MECHANISM = ('GSSAPI',)
114+
if val is not None:
115+
if val.upper() not in ('GSSAPI',):
116+
raise usage.UsageError, "%s : unknown SASL mechanism" % 'bind-sasl-mech'
117+
118+
109119
class Options_bind_mandatory(Options_bind):
110120
def postOptions_bind_mandatory(self):
111121
if not self.opts['binddn']:

0 commit comments

Comments
 (0)