Skip to content

Commit 19ddfc7

Browse files
steelmanŁukasz Stelmach
authored andcommitted
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 ae1acf5 commit 19ddfc7

File tree

3 files changed

+109
-2
lines changed

3 files changed

+109
-2
lines changed

README.rst

Lines changed: 33 additions & 0 deletions
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 this 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

Lines changed: 19 additions & 1 deletion
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

Lines changed: 57 additions & 1 deletion
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
@@ -136,6 +137,47 @@ def __init__(
136137
if channel_bindings not in (None, "tls-server-end-point"):
137138
raise ValueError("channel_bindings must be None or 'tls-server-end-point'")
138139
self.channel_bindings = channel_bindings
140+
self._dns_canonicalize_hostname = False
141+
self._use_reverse_dns = False
142+
143+
def dns_canonicalize_hostname(self, value=None):
144+
"""
145+
Enables canonical hostname resolution via CNAME records.
146+
147+
>>> import requests
148+
>>> from requests_gssapi import HTTPSPNEGOAuth
149+
>>> gssapi_auth = HTTPSPNEGOAuth()
150+
>>> gssapi_auth.dns_canonicalize_hostname(True)
151+
>>> gssapi_auth.use_reverse_dns(True)
152+
>>> r = requests.get("http://example.org", auth=gssapi_auth)
153+
154+
.. warning:::
155+
Using an insecure DNS queries for principal name
156+
canonicalization can result in risc of a man-in-the-middle
157+
attack. Strictly speaking such queries are in violation of
158+
RFC 4120. Alas misconfigured realms exist and client libraries
159+
like MIT Kerberos provide means to canonicalize principal
160+
names via DNS queries. Be very careful when using thi option.
161+
162+
.. seealso:::
163+
`RFC 4120 <https://datatracker.ietf.org/doc/html/rfc4120>`
164+
`RFC 6808 <https://datatracker.ietf.org/doc/html/rfc6806>`
165+
"""
166+
if isinstance(value, bool):
167+
self._dns_canonicalize_hostname = value
168+
return self._dns_canonicalize_hostname
169+
170+
def use_reverse_dns(self, value=None):
171+
"""
172+
Use rev-DNS query to resolve canonical host name when DNS
173+
canonicalization is enabled.
174+
175+
.. seealso::
176+
See `dns_canonicalize_hostname` for further details and warnings.
177+
"""
178+
if isinstance(value, bool):
179+
self._use_reverse_dns = value
180+
return self._use_reverse_dns
139181

140182
def generate_request_header(self, response, host, is_preemptive=False):
141183
"""
@@ -179,12 +221,26 @@ def generate_request_header(self, response, host, is_preemptive=False):
179221
"channel_bindings were requested, but a socket could not be retrieved from the response"
180222
)
181223

224+
canonhost = host
225+
if self._dns_canonicalize_hostname and type(self.target_name) != gssapi.Name:
226+
try:
227+
ai = socket.getaddrinfo(host, 0, flags=socket.AI_CANONNAME)
228+
canonhost = ai[0][3]
229+
230+
if self._use_reverse_dns:
231+
ni = socket.getnameinfo(ai[0][4], socket.NI_NAMEREQD)
232+
canonhost = ni[0]
233+
234+
except socket.gaierror as e:
235+
if e.errno == socket.EAI_MEMORY:
236+
raise e
237+
182238
try:
183239
gss_stage = "initiating context"
184240
name = self.target_name
185241
if type(name) != gssapi.Name:
186242
if "@" not in name:
187-
name = "%s@%s" % (name, host)
243+
name = "%s@%s" % (name, canonhost)
188244

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

0 commit comments

Comments
 (0)