Skip to content

Commit dfbc8d5

Browse files
committed
Implement DNS hostname canonicalization
Optionally resolve hostname via CNAME recrord to its canonical form (A or AAAA record). Optionally use reverse DNS query. Such code is necessary on Windows platforms where SSPI (unlike MIT Kerberos[1]) does not implement such operation and it is applications' responsibility[2] to take care of CNAME resolution. However, the code seems universal enough to put it into the library rather than in every single program using requests_gssapi. [1] https://github.com/krb5/krb5/blob/ec71ac1cabbb3926f8ffaf71e1ad007e4e56e0e5/src/lib/krb5/os/sn2princ.c#L99 [2] https://learn.microsoft.com/en-us/previous-versions/office/sharepoint-server-2010/gg502606(v=office.14)?redirectedfrom=MSDN#kerberos-authentication-and-dns-cnames
1 parent 84e052b commit dfbc8d5

File tree

3 files changed

+57
-5
lines changed

3 files changed

+57
-5
lines changed

README.rst

+17
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,23 @@ To enable delegation of credentials to a server that requests delegation, pass
229229
Be careful to only allow delegation to servers you trust as they will be able
230230
to impersonate you using the delegated credentials.
231231

232+
Hostname canonicalization
233+
-------------------------
234+
235+
When one or more services run on a single host and CNAME records are employed
236+
to point at the host's A or AAAA records, and there is an SPN only for the canonical
237+
name of the host, different hostname needs to be used for an HTTP request
238+
and differnt for authentication. To enable canonical name resolution pass
239+
``dns_canonicalize_hostname=True`` to ``HTTPSPNEGOAuth``. Optionally,
240+
if ``use_reverse_dns=True`` is passed, an additional reverse DNS lookup
241+
will be used to obtain the canonical name.
242+
243+
>>> import requests
244+
>>> from requests_gssapi import HTTPSPNEGOAuth
245+
>>> gssapi_auth = HTTPSPNEGOAuth(dns_canonicalize_hostname=True, use_reverse_dns=True)
246+
>>> r = requests.get("http://example.org", auth=gssapi_auth)
247+
...
248+
232249
Logging
233250
-------
234251

requests_gssapi/compat.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Compatibility library for older versions of python and requests_kerberos
33
"""
4+
import socket
45
import sys
56

67
import gssapi
@@ -23,7 +24,8 @@ class HTTPKerberosAuth(HTTPSPNEGOAuth):
2324
"""Deprecated compat shim; see HTTPSPNEGOAuth instead."""
2425
def __init__(self, mutual_authentication=DISABLED, service="HTTP",
2526
delegate=False, force_preemptive=False, principal=None,
26-
hostname_override=None, sanitize_mutual_error_response=True):
27+
hostname_override=None, sanitize_mutual_error_response=True,
28+
dns_canonicalize_hostname=False, use_reverse_dns=False):
2729
# put these here for later
2830
self.principal = principal
2931
self.service = service
@@ -36,12 +38,27 @@ def __init__(self, mutual_authentication=DISABLED, service="HTTP",
3638
delegate=delegate,
3739
opportunistic_auth=force_preemptive,
3840
creds=None,
39-
sanitize_mutual_error_response=sanitize_mutual_error_response)
41+
sanitize_mutual_error_response=sanitize_mutual_error_response,
42+
dns_canonicalize_hostname=dns_canonicalize_hostname,
43+
use_reverse_dns=use_reverse_dns)
4044

4145
def generate_request_header(self, response, host, is_preemptive=False):
4246
# This method needs to be shimmed because `host` isn't exposed to
4347
# __init__() and we need to derive things from it. Also, __init__()
4448
# can't fail, in the strictest compatability sense.
49+
canonhost = host
50+
if self.dns_canonicalize_hostname:
51+
try:
52+
ai = socket.getaddrinfo(host, 0, flags=socket.AI_CANONNAME)
53+
canonhost = ai[0][3]
54+
55+
if self.use_reverse_dns:
56+
ni = socket.getnameinfo(ai[0][4], socket.NI_NAMEREQD)
57+
canonhost = ni[0]
58+
59+
except socket.gaierror as e:
60+
if e.errno == socket.EAI_MEMORY:
61+
raise e
4562
try:
4663
if self.principal is not None:
4764
gss_stage = "acquiring credentials"
@@ -55,7 +72,7 @@ def generate_request_header(self, response, host, is_preemptive=False):
5572
# name-based HTTP hosting)
5673
if self.service is not None:
5774
gss_stage = "initiating context"
58-
kerb_host = host
75+
kerb_host = canonhost
5976
if self.hostname_override:
6077
kerb_host = self.hostname_override
6178

requests_gssapi/gssapi_.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import re
22
import logging
3+
import socket
34

45
from base64 import b64encode, b64decode
56

@@ -112,7 +113,8 @@ class HTTPSPNEGOAuth(AuthBase):
112113
"""
113114
def __init__(self, mutual_authentication=DISABLED, target_name="HTTP",
114115
delegate=False, opportunistic_auth=False, creds=None,
115-
mech=SPNEGO, sanitize_mutual_error_response=True):
116+
mech=SPNEGO, sanitize_mutual_error_response=True,
117+
dns_canonicalize_hostname=False, use_reverse_dns=False):
116118
self.context = {}
117119
self.pos = None
118120
self.mutual_authentication = mutual_authentication
@@ -122,6 +124,8 @@ def __init__(self, mutual_authentication=DISABLED, target_name="HTTP",
122124
self.creds = creds
123125
self.mech = mech if mech else SPNEGO
124126
self.sanitize_mutual_error_response = sanitize_mutual_error_response
127+
self.dns_canonicalize_hostname = dns_canonicalize_hostname
128+
self.use_reverse_dns = use_reverse_dns
125129

126130
def generate_request_header(self, response, host, is_preemptive=False):
127131
"""
@@ -138,12 +142,26 @@ def generate_request_header(self, response, host, is_preemptive=False):
138142
if self.mutual_authentication != DISABLED:
139143
gssflags.append(gssapi.RequirementFlag.mutual_authentication)
140144

145+
canonhost = host
146+
if self.dns_canonicalize_hostname and type(self.target_name) != gssapi.Name:
147+
try:
148+
ai = socket.getaddrinfo(host, 0, flags=socket.AI_CANONNAME)
149+
canonhost = ai[0][3]
150+
151+
if self.use_reverse_dns:
152+
ni = socket.getnameinfo(ai[0][4], socket.NI_NAMEREQD)
153+
canonhost = ni[0]
154+
155+
except socket.gaierror as e:
156+
if e.errno == socket.EAI_MEMORY:
157+
raise e
158+
141159
try:
142160
gss_stage = "initiating context"
143161
name = self.target_name
144162
if type(name) != gssapi.Name:
145163
if '@' not in name:
146-
name = "%s@%s" % (name, host)
164+
name = "%s@%s" % (name, canonhost)
147165

148166
name = gssapi.Name(name, gssapi.NameType.hostbased_service)
149167
self.context[host] = gssapi.SecurityContext(

0 commit comments

Comments
 (0)