Skip to content

Commit

Permalink
Implement DNS hostname canonicalization
Browse files Browse the repository at this point in the history
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
  • Loading branch information
steelman committed Apr 3, 2024
1 parent 93660c3 commit 47aee87
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 2 deletions.
17 changes: 17 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,23 @@ To enable delegation of credentials to a server that requests delegation, pass
Be careful to only allow delegation to servers you trust as they will be able
to impersonate you using the delegated credentials.

Hostname canonicalization
-------------------------

When one or more services run on a single host and CNAME records are employed
to point at the host's A or AAAA records, and there is an SPN only for the canonical
name of the host, different hostname needs to be used for an HTTP request
and differnt for authentication. To enable canonical name resolution pass
``dns_canonicalize_hostname=True`` to ``HTTPSPNEGOAuth``. Optionally,
if ``use_reverse_dns=True`` is passed, an additional reverse DNS lookup
will be used to obtain the canonical name.

>>> import requests
>>> from requests_gssapi import HTTPSPNEGOAuth
>>> gssapi_auth = HTTPSPNEGOAuth(dns_canonicalize_hostname=True, use_reverse_dns=True)
>>> r = requests.get("http://example.org", auth=gssapi_auth)
...

Logging
-------

Expand Down
20 changes: 19 additions & 1 deletion src/requests_gssapi/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Compatibility library for older versions of python and requests_kerberos
"""

import socket
import sys

import gssapi
Expand Down Expand Up @@ -32,6 +33,8 @@ def __init__(
principal=None,
hostname_override=None,
sanitize_mutual_error_response=True,
dns_canonicalize_hostname=False,
use_reverse_dns=False
):
# put these here for later
self.principal = principal
Expand All @@ -46,12 +49,27 @@ def __init__(
opportunistic_auth=force_preemptive,
creds=None,
sanitize_mutual_error_response=sanitize_mutual_error_response,
dns_canonicalize_hostname=dns_canonicalize_hostname,
use_reverse_dns=use_reverse_dns
)

def generate_request_header(self, response, host, is_preemptive=False):
# This method needs to be shimmed because `host` isn't exposed to
# __init__() and we need to derive things from it. Also, __init__()
# can't fail, in the strictest compatability sense.
canonhost = host
if self.dns_canonicalize_hostname:
try:
ai = socket.getaddrinfo(host, 0, flags=socket.AI_CANONNAME)
canonhost = ai[0][3]

if self.use_reverse_dns:
ni = socket.getnameinfo(ai[0][4], socket.NI_NAMEREQD)
canonhost = ni[0]

except socket.gaierror as e:
if e.errno == socket.EAI_MEMORY:
raise e
try:
if self.principal is not None:
gss_stage = "acquiring credentials"
Expand All @@ -64,7 +82,7 @@ def generate_request_header(self, response, host, is_preemptive=False):
# name-based HTTP hosting)
if self.service is not None:
gss_stage = "initiating context"
kerb_host = host
kerb_host = canonhost
if self.hostname_override:
kerb_host = self.hostname_override

Expand Down
21 changes: 20 additions & 1 deletion src/requests_gssapi/gssapi_.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import re
import socket
from base64 import b64decode, b64encode

import gssapi
Expand Down Expand Up @@ -118,6 +119,8 @@ def __init__(
creds=None,
mech=SPNEGO,
sanitize_mutual_error_response=True,
dns_canonicalize_hostname=False,
use_reverse_dns=False
):
self.context = {}
self.pos = None
Expand All @@ -128,6 +131,8 @@ def __init__(
self.creds = creds
self.mech = mech if mech else SPNEGO
self.sanitize_mutual_error_response = sanitize_mutual_error_response
self.dns_canonicalize_hostname = dns_canonicalize_hostname
self.use_reverse_dns = use_reverse_dns

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

canonhost = host
if self.dns_canonicalize_hostname and type(self.target_name) != gssapi.Name:
try:
ai = socket.getaddrinfo(host, 0, flags=socket.AI_CANONNAME)
canonhost = ai[0][3]

if self.use_reverse_dns:
ni = socket.getnameinfo(ai[0][4], socket.NI_NAMEREQD)
canonhost = ni[0]

except socket.gaierror as e:
if e.errno == socket.EAI_MEMORY:
raise e

try:
gss_stage = "initiating context"
name = self.target_name
if type(name) != gssapi.Name:
if "@" not in name:
name = "%s@%s" % (name, host)
name = "%s@%s" % (name, canonhost)

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

0 comments on commit 47aee87

Please sign in to comment.