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 Dec 14, 2023
1 parent 84e052b commit dfbc8d5
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 5 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
23 changes: 20 additions & 3 deletions requests_gssapi/compat.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Compatibility library for older versions of python and requests_kerberos
"""
import socket
import sys

import gssapi
Expand All @@ -23,7 +24,8 @@ class HTTPKerberosAuth(HTTPSPNEGOAuth):
"""Deprecated compat shim; see HTTPSPNEGOAuth instead."""
def __init__(self, mutual_authentication=DISABLED, service="HTTP",
delegate=False, force_preemptive=False, principal=None,
hostname_override=None, sanitize_mutual_error_response=True):
hostname_override=None, sanitize_mutual_error_response=True,
dns_canonicalize_hostname=False, use_reverse_dns=False):
# put these here for later
self.principal = principal
self.service = service
Expand All @@ -36,12 +38,27 @@ def __init__(self, mutual_authentication=DISABLED, service="HTTP",
delegate=delegate,
opportunistic_auth=force_preemptive,
creds=None,
sanitize_mutual_error_response=sanitize_mutual_error_response)
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 @@ -55,7 +72,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
22 changes: 20 additions & 2 deletions requests_gssapi/gssapi_.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import re
import logging
import socket

from base64 import b64encode, b64decode

Expand Down Expand Up @@ -112,7 +113,8 @@ class HTTPSPNEGOAuth(AuthBase):
"""
def __init__(self, mutual_authentication=DISABLED, target_name="HTTP",
delegate=False, opportunistic_auth=False, creds=None,
mech=SPNEGO, sanitize_mutual_error_response=True):
mech=SPNEGO, sanitize_mutual_error_response=True,
dns_canonicalize_hostname=False, use_reverse_dns=False):
self.context = {}
self.pos = None
self.mutual_authentication = mutual_authentication
Expand All @@ -122,6 +124,8 @@ def __init__(self, mutual_authentication=DISABLED, target_name="HTTP",
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 @@ -138,12 +142,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 dfbc8d5

Please sign in to comment.