Skip to content

Commit e526fb5

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. Warning: Usage of insecure DNS queries is explicitly forbidden in RFC 4120[3] and may result in the risk of man-in-the-middle attack. [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 [3] https://datatracker.ietf.org/doc/html/rfc4120
1 parent 93660c3 commit e526fb5

File tree

3 files changed

+109
-2
lines changed

3 files changed

+109
-2
lines changed

README.rst

+33
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,39 @@ 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
237+
the canonical name of the host, different hostname needs to be used for
238+
an HTTP request and differnt for authentication. To enable canonical name
239+
resolution call ``dns_canonicalize_hostname(True)`` on an ``HTTPSPNEGOAuth``
240+
object. Optionally, if ``use_reverse_dns(True)`` is called, an additional
241+
reverse DNS lookup will be used to obtain the canonical name.
242+
243+
244+
>>> import requests
245+
>>> from requests_gssapi import HTTPSPNEGOAuth
246+
>>> gssapi_auth = HTTPSPNEGOAuth()
247+
>>> gssapi_auth.dns_canonicalize_hostname(True)
248+
>>> gssapi_auth.use_reverse_dns(True)
249+
>>> r = requests.get("http://example.org", auth=gssapi_auth)
250+
...
251+
252+
.. warning:::
253+
Using an insecure DNS queries for principal name canonicalization can
254+
result in risc of a man-in-the-middle attack. Strictly speaking such
255+
queries are in violation of RFC 4120. Alas misconfigured realms exist
256+
and client libraries like MIT Kerberos provide means to canonicalize
257+
principal names via DNS queries. Be very careful when using thi option.
258+
259+
.. seealso:::
260+
`RFC 4120 <https://datatracker.ietf.org/doc/html/rfc4120>`
261+
`RFC 6808 <https://datatracker.ietf.org/doc/html/rfc6806>`
262+
`Kerberos configuration known issues, Kerberos authentication and DNS CNAMEs <https://learn.microsoft.com/en-us/previous-versions/office/sharepoint-server-2010/gg502606(v=office.14)?redirectedfrom=MSDN#kerberos-authentication-and-dns-cnames>`
263+
`krb5.conf <https://web.mit.edu/kerberos/krb5-1.21/doc/admin/conf_files/krb5_conf.html>`
264+
232265
Logging
233266
-------
234267

src/requests_gssapi/compat.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Compatibility library for older versions of python and requests_kerberos
33
"""
44

5+
import socket
56
import sys
67

78
import gssapi
@@ -32,6 +33,8 @@ def __init__(
3233
principal=None,
3334
hostname_override=None,
3435
sanitize_mutual_error_response=True,
36+
dns_canonicalize_hostname=False,
37+
use_reverse_dns=False
3538
):
3639
# put these here for later
3740
self.principal = principal
@@ -46,12 +49,27 @@ def __init__(
4649
opportunistic_auth=force_preemptive,
4750
creds=None,
4851
sanitize_mutual_error_response=sanitize_mutual_error_response,
52+
dns_canonicalize_hostname=dns_canonicalize_hostname,
53+
use_reverse_dns=use_reverse_dns
4954
)
5055

5156
def generate_request_header(self, response, host, is_preemptive=False):
5257
# This method needs to be shimmed because `host` isn't exposed to
5358
# __init__() and we need to derive things from it. Also, __init__()
5459
# can't fail, in the strictest compatability sense.
60+
canonhost = host
61+
if self.dns_canonicalize_hostname:
62+
try:
63+
ai = socket.getaddrinfo(host, 0, flags=socket.AI_CANONNAME)
64+
canonhost = ai[0][3]
65+
66+
if self.use_reverse_dns:
67+
ni = socket.getnameinfo(ai[0][4], socket.NI_NAMEREQD)
68+
canonhost = ni[0]
69+
70+
except socket.gaierror as e:
71+
if e.errno == socket.EAI_MEMORY:
72+
raise e
5573
try:
5674
if self.principal is not None:
5775
gss_stage = "acquiring credentials"
@@ -64,7 +82,7 @@ def generate_request_header(self, response, host, is_preemptive=False):
6482
# name-based HTTP hosting)
6583
if self.service is not None:
6684
gss_stage = "initiating context"
67-
kerb_host = host
85+
kerb_host = canonhost
6886
if self.hostname_override:
6987
kerb_host = self.hostname_override
7088

src/requests_gssapi/gssapi_.py

+57-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
import re
3+
import socket
34
from base64 import b64decode, b64encode
45

56
import gssapi
@@ -128,6 +129,47 @@ def __init__(
128129
self.creds = creds
129130
self.mech = mech if mech else SPNEGO
130131
self.sanitize_mutual_error_response = sanitize_mutual_error_response
132+
self._dns_canonicalize_hostname = False
133+
self._use_reverse_dns = False
134+
135+
def dns_canonicalize_hostname(self, value=None):
136+
"""
137+
Enables canonical hostname resolution via CNAME records.
138+
139+
>>> import requests
140+
>>> from requests_gssapi import HTTPSPNEGOAuth
141+
>>> gssapi_auth = HTTPSPNEGOAuth()
142+
>>> gssapi_auth.dns_canonicalize_hostname(True)
143+
>>> gssapi_auth.use_reverse_dns(True)
144+
>>> r = requests.get("http://example.org", auth=gssapi_auth)
145+
146+
.. warning:::
147+
Using an insecure DNS queries for principal name
148+
canonicalization can result in risc of a man-in-the-middle
149+
attack. Strictly speaking such queries are in violation of
150+
RFC 4120. Alas misconfigured realms exist and client libraries
151+
like MIT Kerberos provide means to canonicalize principal
152+
names via DNS queries. Be very careful when using thi option.
153+
154+
.. seealso:::
155+
`RFC 4120 <https://datatracker.ietf.org/doc/html/rfc4120>`
156+
`RFC 6808 <https://datatracker.ietf.org/doc/html/rfc6806>`
157+
"""
158+
if isinstance(value, bool):
159+
self._dns_canonicalize_hostname = value
160+
return self._dns_canonicalize_hostname
161+
162+
def use_reverse_dns(self, value=None):
163+
"""
164+
Use rev-DNS query to resolve canonical host name when DNS
165+
canonicalization is enabled.
166+
167+
.. seealso::
168+
See `dns_canonicalize_hostname` for further details and warnings.
169+
"""
170+
if isinstance(value, bool):
171+
self._use_reverse_dns = value
172+
return self._use_reverse_dns
131173

132174
def generate_request_header(self, response, host, is_preemptive=False):
133175
"""
@@ -144,12 +186,26 @@ def generate_request_header(self, response, host, is_preemptive=False):
144186
if self.mutual_authentication != DISABLED:
145187
gssflags.append(gssapi.RequirementFlag.mutual_authentication)
146188

189+
canonhost = host
190+
if self._dns_canonicalize_hostname and type(self.target_name) != gssapi.Name:
191+
try:
192+
ai = socket.getaddrinfo(host, 0, flags=socket.AI_CANONNAME)
193+
canonhost = ai[0][3]
194+
195+
if self._use_reverse_dns:
196+
ni = socket.getnameinfo(ai[0][4], socket.NI_NAMEREQD)
197+
canonhost = ni[0]
198+
199+
except socket.gaierror as e:
200+
if e.errno == socket.EAI_MEMORY:
201+
raise e
202+
147203
try:
148204
gss_stage = "initiating context"
149205
name = self.target_name
150206
if type(name) != gssapi.Name:
151207
if "@" not in name:
152-
name = "%s@%s" % (name, host)
208+
name = "%s@%s" % (name, canonhost)
153209

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

0 commit comments

Comments
 (0)