From 8b906ad9fa6bf3c4581dbd54da3cf0e8dd9f101b Mon Sep 17 00:00:00 2001 From: Robbie Harwood Date: Thu, 16 May 2019 15:19:51 -0400 Subject: [PATCH 1/2] Add information about MutualAuthenticationError to README.rst Signed-off-by: Robbie Harwood Resolves: #15 --- README.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.rst b/README.rst index 8a61fb8..712e9ad 100644 --- a/README.rst +++ b/README.rst @@ -32,6 +32,26 @@ the 401 response. Mutual Authentication --------------------- +Mutual authentication is a poorly-named feature of the GSSAPI which doesn't +provide any additional security benefit to most possible uses of +requests_gssapi. Practically speaking, in most mechanism implementations +(including krb5), it requires another round-trip between the client and server +during the authentication handshake. Many clients and servers do not properly +handle the authentication handshake taking more than one round-trip. If you +encounter a MutualAuthenticationError, this is probably why. + +So long as you're running over a TLS link whose security guarantees you trust, +there's no benefit to mutual authentication. If you don't trust the link at +all, mutual authentication won't help (since it's not tamper-proof, and GSSAPI +isn't being used post-authentication. There's some middle ground between the +two where it helps a small amount (e.g., passive adversary over +encrypted-but-unverified channel), but for Negotiate (what we're doing here), +it's not generally helpful. + +For a more technical explanation of what mutual authentication actually +guarantees, I refer you to rfc2743 (GSSAPIv2), rfc4120 (krb5 in GSSAPI), +rfc4178 (SPNEGO), and rfc4559 (HTTP Negotiate). + REQUIRED ^^^^^^^^ From 60e10699d7eadccf7aa66d7fd04dcf90a76b0248 Mon Sep 17 00:00:00 2001 From: Robbie Harwood Date: Thu, 16 May 2019 15:39:59 -0400 Subject: [PATCH 2/2] Disable mutual authentication by default Signed-off-by: Robbie Harwood --- README.rst | 59 +++++++++++++++++++------------------- requests_gssapi/compat.py | 4 +-- requests_gssapi/gssapi_.py | 12 ++++---- test_requests_gssapi.py | 24 ++++++++++------ 4 files changed, 55 insertions(+), 44 deletions(-) diff --git a/README.rst b/README.rst index 712e9ad..79cb079 100644 --- a/README.rst +++ b/README.rst @@ -52,57 +52,58 @@ For a more technical explanation of what mutual authentication actually guarantees, I refer you to rfc2743 (GSSAPIv2), rfc4120 (krb5 in GSSAPI), rfc4178 (SPNEGO), and rfc4559 (HTTP Negotiate). -REQUIRED + +DISABLED ^^^^^^^^ -By default, ``HTTPSPNEGOAuth`` will require mutual authentication from the -server, and if a server emits a non-error response which cannot be -authenticated, a ``requests_gssapi.errors.MutualAuthenticationError`` will -be raised. If a server emits an error which cannot be authenticated, it will -be returned to the user but with its contents and headers stripped. If the -response content is more important than the need for mutual auth on errors, -(eg, for certain WinRM calls) the stripping behavior can be suppressed by -setting ``sanitize_mutual_error_response=False``: +By default, there's no need to explicitly disable mutual authentication. +However, for compatability with older versions of request_gssapi or +requests_kerberos, you can explicitly request it not be attempted: .. code-block:: python >>> import requests - >>> from requests_gssapi import HTTPSPNEGOAuth, REQUIRED - >>> gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=REQUIRED, sanitize_mutual_error_response=False) - >>> r = requests.get("https://windows.example.org/wsman", auth=gssapi_auth) + >>> from requests_gssapi import HTTPSPNEGOAuth, DISABLED + >>> gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=DISABLED) + >>> r = requests.get("https://example.org", auth=gssapi_auth) ... - -OPTIONAL +REQUIRED ^^^^^^^^ -If you'd prefer to not require mutual authentication, you can set your -preference when constructing your ``HTTPSPNEGOAuth`` object: +This was historically the default, but no longer is. If requested, +``HTTPSPNEGOAuth`` will require mutual authentication from the server, and if +a server emits a non-error response which cannot be authenticated, a +``requests_gssapi.errors.MutualAuthenticationError`` will be raised. (See +above for what this means.) If a server emits an error which cannot be +authenticated, it will be returned to the user but with its contents and +headers stripped. If the response content is more important than the need for +mutual auth on errors, (eg, for certain WinRM calls) the stripping behavior +can be suppressed by setting ``sanitize_mutual_error_response=False``: .. code-block:: python >>> import requests - >>> from requests_gssapi import HTTPSPNEGOAuth, OPTIONAL - >>> gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=OPTIONAL) - >>> r = requests.get("http://example.org", auth=gssapi_auth) + >>> from requests_gssapi import HTTPSPNEGOAuth, REQUIRED + >>> gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=REQUIRED, sanitize_mutual_error_response=False) + >>> r = requests.get("https://windows.example.org/wsman", auth=gssapi_auth) ... -This will cause ``requests_gssapi`` to attempt mutual authentication if the -server advertises that it supports it, and cause a failure if authentication -fails, but not if the server does not support it at all. - -DISABLED +OPTIONAL ^^^^^^^^ -While we don't recommend it, if you'd prefer to never attempt mutual -authentication, you can do that as well: +This will cause ``requests_gssapi`` to attempt mutual authentication if the +server advertises that it supports it, and cause a failure if authentication +fails, but not if the server does not support it at all. This is probably not +what you want: link tampering will either cause hard failures, or silently +cause it to not happen at all. It is retained for compatability. .. code-block:: python >>> import requests - >>> from requests_gssapi import HTTPSPNEGOAuth, DISABLED - >>> gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=DISABLED) - >>> r = requests.get("http://example.org", auth=gssapi_auth) + >>> from requests_gssapi import HTTPSPNEGOAuth, OPTIONAL + >>> gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=OPTIONAL) + >>> r = requests.get("https://example.org", auth=gssapi_auth) ... Opportunistic Authentication diff --git a/requests_gssapi/compat.py b/requests_gssapi/compat.py index 99eb7b7..9373331 100644 --- a/requests_gssapi/compat.py +++ b/requests_gssapi/compat.py @@ -5,7 +5,7 @@ import gssapi -from .gssapi_ import REQUIRED, HTTPSPNEGOAuth, SPNEGOExchangeError, log +from .gssapi_ import DISABLED, HTTPSPNEGOAuth, SPNEGOExchangeError, log # python 2.7 introduced a NullHandler which we want to use, but to support # older versions, we implement our own if needed. @@ -21,7 +21,7 @@ def emit(self, record): class HTTPKerberosAuth(HTTPSPNEGOAuth): """Deprecated compat shim; see HTTPSPNEGOAuth instead.""" - def __init__(self, mutual_authentication=REQUIRED, service="HTTP", + def __init__(self, mutual_authentication=DISABLED, service="HTTP", delegate=False, force_preemptive=False, principal=None, hostname_override=None, sanitize_mutual_error_response=True): # put these here for later diff --git a/requests_gssapi/gssapi_.py b/requests_gssapi/gssapi_.py index 69ba58a..2a6c55a 100644 --- a/requests_gssapi/gssapi_.py +++ b/requests_gssapi/gssapi_.py @@ -84,8 +84,8 @@ class HTTPSPNEGOAuth(AuthBase): """Attaches HTTP GSSAPI Authentication to the given Request object. `mutual_authentication` controls whether GSSAPI should attempt mutual - authentication. It may be `REQUIRED` (default), `OPTIONAL`, or - `DISABLED`. + authentication. It may be `REQUIRED`, `OPTIONAL`, or `DISABLED` + (default). `target_name` specifies the remote principal name. It may be either a GSSAPI name type or a string (default: "HTTP" at the DNS host). @@ -101,8 +101,9 @@ class HTTPSPNEGOAuth(AuthBase): `sanitize_mutual_error_response` controls whether we should clean up server responses. See the `SanitizedResponse` class. + """ - def __init__(self, mutual_authentication=REQUIRED, target_name="HTTP", + def __init__(self, mutual_authentication=DISABLED, target_name="HTTP", delegate=False, opportunistic_auth=False, creds=None, sanitize_mutual_error_response=True): self.context = {} @@ -123,10 +124,11 @@ def generate_request_header(self, response, host, is_preemptive=False): """ - gssflags = [gssapi.RequirementFlag.mutual_authentication, - gssapi.RequirementFlag.out_of_sequence_detection] + gssflags = [gssapi.RequirementFlag.out_of_sequence_detection] if self.delegate: gssflags.append(gssapi.RequirementFlag.delegate_to_peer) + if self.mutual_authentication != DISABLED: + gssflags.append(gssapi.RequirementFlag.mutual_authentication) try: gss_stage = "initiating context" diff --git a/test_requests_gssapi.py b/test_requests_gssapi.py index cd4adcd..9014894 100644 --- a/test_requests_gssapi.py +++ b/test_requests_gssapi.py @@ -13,6 +13,8 @@ import requests_gssapi import unittest +from requests_gssapi import REQUIRED + # Note: we're not using the @mock.patch decorator: # > My only word of warning is that in the past, the patch decorator hides # > tests when using the standard unittest library. @@ -26,8 +28,8 @@ # construction, so construct a *really* fake one fail_resp = Mock(side_effect=gssapi.exceptions.GSSError(0, 0)) -gssflags = [gssapi.RequirementFlag.mutual_authentication, - gssapi.RequirementFlag.out_of_sequence_detection] +gssflags = [gssapi.RequirementFlag.out_of_sequence_detection] +mutflags = gssflags + [gssapi.RequirementFlag.mutual_authentication] gssdelegflags = gssflags + [gssapi.RequirementFlag.delegate_to_peer] # The base64 behavior we want is that encoding produces a string, but decoding @@ -237,7 +239,8 @@ def test_handle_other(self): 'www-authenticate': b64_negotiate_server, 'authorization': b64_negotiate_response} - auth = requests_gssapi.HTTPKerberosAuth() + auth = requests_gssapi.HTTPKerberosAuth( + mutual_authentication=REQUIRED) auth.context = {"www.example.org": gssapi.SecurityContext} r = auth.handle_other(response_ok) @@ -255,7 +258,8 @@ def test_handle_response_200(self): 'www-authenticate': b64_negotiate_server, 'authorization': b64_negotiate_response} - auth = requests_gssapi.HTTPKerberosAuth() + auth = requests_gssapi.HTTPKerberosAuth( + mutual_authentication=REQUIRED) auth.context = {"www.example.org": gssapi.SecurityContext} r = auth.handle_response(response_ok) @@ -272,7 +276,8 @@ def test_handle_response_200_mutual_auth_required_failure(self): response_ok.status_code = 200 response_ok.headers = {} - auth = requests_gssapi.HTTPKerberosAuth() + auth = requests_gssapi.HTTPKerberosAuth( + mutual_authentication=REQUIRED) auth.context = {"www.example.org": "CTX"} self.assertRaises(requests_gssapi.MutualAuthenticationError, @@ -290,7 +295,8 @@ def test_handle_response_200_mutual_auth_required_failure_2(self): 'www-authenticate': b64_negotiate_server, 'authorization': b64_negotiate_response} - auth = requests_gssapi.HTTPKerberosAuth() + auth = requests_gssapi.HTTPKerberosAuth( + mutual_authentication=REQUIRED) auth.context = {"www.example.org": gssapi.SecurityContext} self.assertRaises(requests_gssapi.MutualAuthenticationError, @@ -348,7 +354,8 @@ def test_handle_response_500_mutual_auth_required_failure(self): response_500.raw = "RAW" response_500.cookies = "COOKIES" - auth = requests_gssapi.HTTPKerberosAuth() + auth = requests_gssapi.HTTPKerberosAuth( + mutual_authentication=REQUIRED) auth.context = {"www.example.org": "CTX"} r = auth.handle_response(response_500) @@ -524,7 +531,8 @@ def test_delegation(self): response.connection = connection response._content = "" response.raw = raw - auth = requests_gssapi.HTTPKerberosAuth(1, "HTTP", True) + auth = requests_gssapi.HTTPKerberosAuth(service="HTTP", + delegate=True) r = auth.authenticate_user(response) self.assertTrue(response in r.history)