From dfbc8d5f782e12e5b61d8eca929a8bfdf65e148b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Stelmach?= Date: Thu, 14 Dec 2023 15:05:26 +0100 Subject: [PATCH] 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 --- README.rst | 17 +++++++++++++++++ requests_gssapi/compat.py | 23 ++++++++++++++++++++--- requests_gssapi/gssapi_.py | 22 ++++++++++++++++++++-- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 6c787ca..905cb13 100644 --- a/README.rst +++ b/README.rst @@ -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 ------- diff --git a/requests_gssapi/compat.py b/requests_gssapi/compat.py index f59f08d..98cec58 100644 --- a/requests_gssapi/compat.py +++ b/requests_gssapi/compat.py @@ -1,6 +1,7 @@ """ Compatibility library for older versions of python and requests_kerberos """ +import socket import sys import gssapi @@ -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 @@ -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" @@ -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 diff --git a/requests_gssapi/gssapi_.py b/requests_gssapi/gssapi_.py index bdbb578..dd6c398 100644 --- a/requests_gssapi/gssapi_.py +++ b/requests_gssapi/gssapi_.py @@ -1,5 +1,6 @@ import re import logging +import socket from base64 import b64encode, b64decode @@ -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 @@ -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): """ @@ -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(