Skip to content

Commit 58ca70f

Browse files
simo5Gemini
andcommitted
Support GSSAPI channel bindings
This change adds support for GSSAPI channel bindings, which helps to protect against man-in-the-middle relay attacks by tying the authentication to the underlying secure channel. A `channel_bindings` parameter is added to `HTTPSPNEGOAuth`. When set to 'tls- server-end-point', the server's TLS certificate is retrieved from the socket, hashed, and used to create the GSSAPI channel bindings. This feature requires the `cryptography` library as an optional dependency. If it's not available, channel bindings cannot be used and a warning is logged. Co-authored-by: Gemini <[email protected]> Signed-off-by: Simo Sorce <[email protected]>
1 parent 93660c3 commit 58ca70f

File tree

2 files changed

+99
-9
lines changed

2 files changed

+99
-9
lines changed

src/requests_gssapi/gssapi_.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ class HTTPSPNEGOAuth(AuthBase):
107107
`sanitize_mutual_error_response` controls whether we should clean up
108108
server responses. See the `SanitizedResponse` class.
109109
110+
`channel_bindings` can be used to pass channel bindings to GSSAPI.
111+
The only accepted value is 'tls-server-end-point' which uses the TLS
112+
server's certificate for the channel bindings. Default is `None`.
113+
110114
"""
111115

112116
def __init__(
@@ -118,6 +122,7 @@ def __init__(
118122
creds=None,
119123
mech=SPNEGO,
120124
sanitize_mutual_error_response=True,
125+
channel_bindings=None,
121126
):
122127
self.context = {}
123128
self.pos = None
@@ -128,6 +133,9 @@ def __init__(
128133
self.creds = creds
129134
self.mech = mech if mech else SPNEGO
130135
self.sanitize_mutual_error_response = sanitize_mutual_error_response
136+
if channel_bindings not in (None, "tls-server-end-point"):
137+
raise ValueError("channel_bindings must be None or 'tls-server-end-point'")
138+
self.channel_bindings = channel_bindings
131139

132140
def generate_request_header(self, response, host, is_preemptive=False):
133141
"""
@@ -144,6 +152,37 @@ def generate_request_header(self, response, host, is_preemptive=False):
144152
if self.mutual_authentication != DISABLED:
145153
gssflags.append(gssapi.RequirementFlag.mutual_authentication)
146154

155+
gss_cb = None
156+
if self.channel_bindings == "tls-server-end-point":
157+
if is_preemptive:
158+
log.warning("channel_bindings were requested, but are unavailable for opportunistic authentication")
159+
# The 'connection' attribute on raw is a public urllib3 API
160+
# and can be None if the connection has been released.
161+
elif getattr(response.raw, "connection", None) and getattr(response.raw.connection, "sock", None):
162+
try:
163+
# Defer import so it's not a hard dependency.
164+
from cryptography import x509
165+
166+
sock = response.raw.connection.sock
167+
168+
der_cert = sock.getpeercert(binary_form=True)
169+
cert = x509.load_der_x509_certificate(der_cert)
170+
hash = cert.signature_hash_algorithm
171+
cert_hash = cert.fingerprint(hash)
172+
173+
app_data = b"tls-server-end-point:" + cert_hash
174+
gss_cb = gssapi.ChannelBindings(application_data=app_data)
175+
log.debug("generate_request_header(): Successfully retrieved channel bindings")
176+
except ImportError:
177+
log.warning("Could not import cryptography, python-cryptography is required for this feature.")
178+
except Exception:
179+
log.warning(
180+
"Failed to get channel bindings from socket",
181+
exc_info=True,
182+
)
183+
else:
184+
log.warning("channel_bindings were requested, but a socket could not be retrieved from the response")
185+
147186
try:
148187
gss_stage = "initiating context"
149188
name = self.target_name
@@ -153,7 +192,12 @@ def generate_request_header(self, response, host, is_preemptive=False):
153192

154193
name = gssapi.Name(name, gssapi.NameType.hostbased_service)
155194
self.context[host] = gssapi.SecurityContext(
156-
usage="initiate", flags=gssflags, name=name, creds=self.creds, mech=self.mech
195+
usage="initiate",
196+
flags=gssflags,
197+
name=name,
198+
creds=self.creds,
199+
mech=self.mech,
200+
channel_bindings=gss_cb,
157201
)
158202

159203
gss_stage = "stepping context"

tests/test_requests_gssapi.py

100644100755
Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,12 @@ def test_generate_request_header(self):
9898
auth = requests_gssapi.HTTPKerberosAuth()
9999
self.assertEqual(auth.generate_request_header(response, host), b64_negotiate_response)
100100
fake_init.assert_called_with(
101-
name=gssapi_sname("[email protected]"), creds=None, mech=SPNEGO, flags=gssflags, usage="initiate"
101+
name=gssapi_sname("[email protected]"),
102+
creds=None,
103+
mech=SPNEGO,
104+
flags=gssflags,
105+
usage="initiate",
106+
channel_bindings=None,
102107
)
103108
fake_resp.assert_called_with(b"token")
104109

@@ -113,7 +118,12 @@ def test_generate_request_header_init_error(self):
113118
requests_gssapi.exceptions.SPNEGOExchangeError, auth.generate_request_header, response, host
114119
)
115120
fake_init.assert_called_with(
116-
name=gssapi_sname("[email protected]"), usage="initiate", flags=gssflags, creds=None, mech=SPNEGO
121+
name=gssapi_sname("[email protected]"),
122+
usage="initiate",
123+
flags=gssflags,
124+
creds=None,
125+
mech=SPNEGO,
126+
channel_bindings=None,
117127
)
118128

119129
def test_generate_request_header_step_error(self):
@@ -127,7 +137,12 @@ def test_generate_request_header_step_error(self):
127137
requests_gssapi.exceptions.SPNEGOExchangeError, auth.generate_request_header, response, host
128138
)
129139
fake_init.assert_called_with(
130-
name=gssapi_sname("[email protected]"), usage="initiate", flags=gssflags, creds=None, mech=SPNEGO
140+
name=gssapi_sname("[email protected]"),
141+
usage="initiate",
142+
flags=gssflags,
143+
creds=None,
144+
mech=SPNEGO,
145+
channel_bindings=None,
131146
)
132147
fail_resp.assert_called_with(b"token")
133148

@@ -162,7 +177,12 @@ def test_authenticate_user(self):
162177
connection.send.assert_called_with(request)
163178
raw.release_conn.assert_called_with()
164179
fake_init.assert_called_with(
165-
name=gssapi_sname("[email protected]"), flags=gssflags, usage="initiate", creds=None, mech=SPNEGO
180+
name=gssapi_sname("[email protected]"),
181+
flags=gssflags,
182+
usage="initiate",
183+
creds=None,
184+
mech=SPNEGO,
185+
channel_bindings=None,
166186
)
167187
fake_resp.assert_called_with(b"token")
168188

@@ -197,7 +217,12 @@ def test_handle_401(self):
197217
connection.send.assert_called_with(request)
198218
raw.release_conn.assert_called_with()
199219
fake_init.assert_called_with(
200-
name=gssapi_sname("[email protected]"), creds=None, mech=SPNEGO, flags=gssflags, usage="initiate"
220+
name=gssapi_sname("[email protected]"),
221+
creds=None,
222+
mech=SPNEGO,
223+
flags=gssflags,
224+
usage="initiate",
225+
channel_bindings=None,
201226
)
202227
fake_resp.assert_called_with(b"token")
203228

@@ -402,7 +427,12 @@ def test_handle_response_401(self):
402427
connection.send.assert_called_with(request)
403428
raw.release_conn.assert_called_with()
404429
fake_init.assert_called_with(
405-
name=gssapi_sname("[email protected]"), usage="initiate", flags=gssflags, creds=None, mech=SPNEGO
430+
name=gssapi_sname("[email protected]"),
431+
usage="initiate",
432+
flags=gssflags,
433+
creds=None,
434+
mech=SPNEGO,
435+
channel_bindings=None,
406436
)
407437
fake_resp.assert_called_with(b"token")
408438

@@ -443,7 +473,12 @@ def connection_send(self, *args, **kwargs):
443473
connection.send.assert_called_with(request)
444474
raw.release_conn.assert_called_with()
445475
fake_init.assert_called_with(
446-
name=gssapi_sname("[email protected]"), usage="initiate", flags=gssflags, creds=None, mech=SPNEGO
476+
name=gssapi_sname("[email protected]"),
477+
usage="initiate",
478+
flags=gssflags,
479+
creds=None,
480+
mech=SPNEGO,
481+
channel_bindings=None,
447482
)
448483
fake_resp.assert_called_with(b"token")
449484

@@ -456,7 +491,12 @@ def test_generate_request_header_custom_service(self):
456491
auth = requests_gssapi.HTTPKerberosAuth(service="barfoo")
457492
auth.generate_request_header(response, host),
458493
fake_init.assert_called_with(
459-
name=gssapi_sname("[email protected]"), usage="initiate", flags=gssflags, creds=None, mech=SPNEGO
494+
name=gssapi_sname("[email protected]"),
495+
usage="initiate",
496+
flags=gssflags,
497+
creds=None,
498+
mech=SPNEGO,
499+
channel_bindings=None,
460500
)
461501
fake_resp.assert_called_with(b"token")
462502

@@ -496,6 +536,7 @@ def test_delegation(self):
496536
flags=gssdelegflags,
497537
creds=None,
498538
mech=SPNEGO,
539+
channel_bindings=None,
499540
)
500541
fake_resp.assert_called_with(b"token")
501542

@@ -522,6 +563,7 @@ def test_principal_override(self):
522563
flags=gssflags,
523564
creds=b"fake creds",
524565
mech=SPNEGO,
566+
channel_bindings=None,
525567
)
526568

527569
def test_realm_override(self):
@@ -538,6 +580,7 @@ def test_realm_override(self):
538580
flags=gssflags,
539581
creds=None,
540582
mech=SPNEGO,
583+
channel_bindings=None,
541584
)
542585
fake_resp.assert_called_with(b"token")
543586

@@ -569,6 +612,7 @@ def test_explicit_creds(self):
569612
flags=gssflags,
570613
creds=b"fake creds",
571614
mech=SPNEGO,
615+
channel_bindings=None,
572616
)
573617
fake_resp.assert_called_with(b"token")
574618

@@ -589,6 +633,7 @@ def test_explicit_mech(self):
589633
flags=gssflags,
590634
creds=None,
591635
mech=b"fake mech",
636+
channel_bindings=None,
592637
)
593638
fake_resp.assert_called_with(b"token")
594639

@@ -606,6 +651,7 @@ def test_target_name(self):
606651
flags=gssflags,
607652
creds=None,
608653
mech=SPNEGO,
654+
channel_bindings=None,
609655
)
610656
fake_resp.assert_called_with(b"token")
611657

0 commit comments

Comments
 (0)