From e526fb51f1fe7632a77295ba35d6bd6d2b820360 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 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Warning: Usage of insecure DNS queries is explicitly forbidden in RFC 4120[3] and may result in the risk of man-in-the-middle attack. [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 [3] https://datatracker.ietf.org/doc/html/rfc4120 --- README.rst | 33 +++++++++++++++++++ src/requests_gssapi/compat.py | 20 +++++++++++- src/requests_gssapi/gssapi_.py | 58 +++++++++++++++++++++++++++++++++- 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 6c787ca..da45737 100644 --- a/README.rst +++ b/README.rst @@ -229,6 +229,39 @@ 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 call ``dns_canonicalize_hostname(True)`` on an ``HTTPSPNEGOAuth`` +object. Optionally, if ``use_reverse_dns(True)`` is called, an additional +reverse DNS lookup will be used to obtain the canonical name. + + + >>> import requests + >>> from requests_gssapi import HTTPSPNEGOAuth + >>> gssapi_auth = HTTPSPNEGOAuth() + >>> gssapi_auth.dns_canonicalize_hostname(True) + >>> gssapi_auth.use_reverse_dns(True) + >>> r = requests.get("http://example.org", auth=gssapi_auth) + ... + +.. warning::: + Using an insecure DNS queries for principal name canonicalization can + result in risc of a man-in-the-middle attack. Strictly speaking such + queries are in violation of RFC 4120. Alas misconfigured realms exist + and client libraries like MIT Kerberos provide means to canonicalize + principal names via DNS queries. Be very careful when using thi option. + +.. seealso::: + `RFC 4120 ` + `RFC 6808 ` + `Kerberos configuration known issues, Kerberos authentication and DNS CNAMEs ` + `krb5.conf ` + 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..342dce1 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 @@ -128,6 +129,47 @@ 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 = False + self._use_reverse_dns = False + + def dns_canonicalize_hostname(self, value=None): + """ + Enables canonical hostname resolution via CNAME records. + + >>> import requests + >>> from requests_gssapi import HTTPSPNEGOAuth + >>> gssapi_auth = HTTPSPNEGOAuth() + >>> gssapi_auth.dns_canonicalize_hostname(True) + >>> gssapi_auth.use_reverse_dns(True) + >>> r = requests.get("http://example.org", auth=gssapi_auth) + + .. warning::: + Using an insecure DNS queries for principal name + canonicalization can result in risc of a man-in-the-middle + attack. Strictly speaking such queries are in violation of + RFC 4120. Alas misconfigured realms exist and client libraries + like MIT Kerberos provide means to canonicalize principal + names via DNS queries. Be very careful when using thi option. + + .. seealso::: + `RFC 4120 ` + `RFC 6808 ` + """ + if isinstance(value, bool): + self._dns_canonicalize_hostname = value + return self._dns_canonicalize_hostname + + def use_reverse_dns(self, value=None): + """ + Use rev-DNS query to resolve canonical host name when DNS + canonicalization is enabled. + + .. seealso:: + See `dns_canonicalize_hostname` for further details and warnings. + """ + if isinstance(value, bool): + self._use_reverse_dns = value + return self._use_reverse_dns def generate_request_header(self, response, host, is_preemptive=False): """ @@ -144,12 +186,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(