From 47aee876af9bfe4cbd5fc0cb7fb7b85620e10648 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 +++++++++++++++++ src/requests_gssapi/compat.py | 20 +++++++++++++++++++- src/requests_gssapi/gssapi_.py | 21 ++++++++++++++++++++- 3 files changed, 56 insertions(+), 2 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/src/requests_gssapi/compat.py b/src/requests_gssapi/compat.py index 0ae4ca8..99d93c7 100644 --- a/src/requests_gssapi/compat.py +++ b/src/requests_gssapi/compat.py @@ -2,6 +2,7 @@ Compatibility library for older versions of python and requests_kerberos """ +import socket import sys import gssapi @@ -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 @@ -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" @@ -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 diff --git a/src/requests_gssapi/gssapi_.py b/src/requests_gssapi/gssapi_.py index 0d3c9f6..5f36eaf 100644 --- a/src/requests_gssapi/gssapi_.py +++ b/src/requests_gssapi/gssapi_.py @@ -1,5 +1,6 @@ import logging import re +import socket from base64 import b64decode, b64encode import gssapi @@ -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 @@ -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): """ @@ -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(