Skip to content

Commit 47aee87

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 93660c3 commit 47aee87

File tree

3 files changed

+56
-2
lines changed

3 files changed

+56
-2
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

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

+20-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
@@ -118,6 +119,8 @@ def __init__(
118119
creds=None,
119120
mech=SPNEGO,
120121
sanitize_mutual_error_response=True,
122+
dns_canonicalize_hostname=False,
123+
use_reverse_dns=False
121124
):
122125
self.context = {}
123126
self.pos = None
@@ -128,6 +131,8 @@ def __init__(
128131
self.creds = creds
129132
self.mech = mech if mech else SPNEGO
130133
self.sanitize_mutual_error_response = sanitize_mutual_error_response
134+
self.dns_canonicalize_hostname = dns_canonicalize_hostname
135+
self.use_reverse_dns = use_reverse_dns
131136

132137
def generate_request_header(self, response, host, is_preemptive=False):
133138
"""
@@ -144,12 +149,26 @@ def generate_request_header(self, response, host, is_preemptive=False):
144149
if self.mutual_authentication != DISABLED:
145150
gssflags.append(gssapi.RequirementFlag.mutual_authentication)
146151

152+
canonhost = host
153+
if self.dns_canonicalize_hostname and type(self.target_name) != gssapi.Name:
154+
try:
155+
ai = socket.getaddrinfo(host, 0, flags=socket.AI_CANONNAME)
156+
canonhost = ai[0][3]
157+
158+
if self.use_reverse_dns:
159+
ni = socket.getnameinfo(ai[0][4], socket.NI_NAMEREQD)
160+
canonhost = ni[0]
161+
162+
except socket.gaierror as e:
163+
if e.errno == socket.EAI_MEMORY:
164+
raise e
165+
147166
try:
148167
gss_stage = "initiating context"
149168
name = self.target_name
150169
if type(name) != gssapi.Name:
151170
if "@" not in name:
152-
name = "%s@%s" % (name, host)
171+
name = "%s@%s" % (name, canonhost)
153172

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

0 commit comments

Comments
 (0)