├── tests ├── __init__.py ├── test_functional_kerberos.py └── test_requests_kerberos.py ├── setup.cfg ├── AUTHORS ├── requirements-test.txt ├── .gitignore ├── MANIFEST.in ├── requirements.txt ├── requests_kerberos ├── exceptions.py ├── compat.py ├── __init__.py └── kerberos_.py ├── LICENSE ├── .travis.yml ├── setup.py ├── HISTORY.rst ├── .travis.sh └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Michael Komitee 2 | Jose Castro Leon 3 | David Pursehouse 4 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | mock 2 | pytest<=3.2.5 3 | pytest-cov 4 | coveralls 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | env/ 4 | build/ 5 | dist/ 6 | requests_kerberos.egg-info/ 7 | 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | include README.rst 3 | include LICENSE 4 | include HISTORY.rst 5 | include AUTHORS 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=1.1.0 2 | winkerberos >= 0.5.0; sys.platform == 'win32' 3 | pykerberos >= 1.1.8, < 2.0.0; sys.platform != 'win32' 4 | cryptography>=1.3 5 | cryptography>=1.3; python_version!="3.3" 6 | cryptography>=1.3, <2; python_version=="3.3" 7 | -------------------------------------------------------------------------------- /requests_kerberos/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | requests_kerberos.exceptions 3 | ~~~~~~~~~~~~~~~~~~~ 4 | 5 | This module contains the set of exceptions. 6 | 7 | """ 8 | from requests.exceptions import RequestException 9 | 10 | 11 | class MutualAuthenticationError(RequestException): 12 | """Mutual Authentication Error""" 13 | 14 | class KerberosExchangeError(RequestException): 15 | """Kerberos Exchange Failed Error""" 16 | -------------------------------------------------------------------------------- /requests_kerberos/compat.py: -------------------------------------------------------------------------------- 1 | """ 2 | Compatibility library for older versions of python 3 | """ 4 | import sys 5 | 6 | # python 2.7 introduced a NullHandler which we want to use, but to support 7 | # older versions, we implement our own if needed. 8 | if sys.version_info[:2] > (2, 6): 9 | from logging import NullHandler 10 | else: 11 | from logging import Handler 12 | class NullHandler(Handler): 13 | def emit(self, record): 14 | pass 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2012 Kenneth Reitz 4 | 5 | Permission to use, copy, modify and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS-IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: python 4 | 5 | services: 6 | - docker 7 | 8 | os: linux 9 | dist: trusty 10 | 11 | matrix: 12 | include: 13 | - env: PYENV=2.7.14 IMAGE=python:2.7.14-slim-stretch 14 | - env: PYENV=3.3.7 IMAGE=ubuntu:16.04 15 | - env: PYENV=3.4.7 IMAGE=ubuntu:16.04 16 | - env: PYENV=3.5.4 IMAGE=ubuntu:16.04 17 | - env: PYENV=3.6.3 IMAGE=python:3.6.3-slim-stretch 18 | 19 | install: 20 | - pip install coveralls # Need to have coveralls installed locally for after_success to run 21 | 22 | script: 23 | - > 24 | docker run 25 | -v $(pwd):$(pwd) 26 | -w $(pwd) 27 | -e PYENV=$PYENV 28 | -e IMAGE=$IMAGE 29 | -e KERBEROS_USERNAME=administrator 30 | -e KERBEROS_PASSWORD=Password01 31 | -e KERBEROS_REALM=example.com 32 | $IMAGE 33 | /bin/bash .travis.sh 34 | 35 | after_success: 36 | - coveralls 37 | -------------------------------------------------------------------------------- /requests_kerberos/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | requests Kerberos/GSSAPI authentication library 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | Requests is an HTTP library, written in Python, for human beings. This library 6 | adds optional Kerberos/GSSAPI authentication support and supports mutual 7 | authentication. Basic GET usage: 8 | 9 | >>> import requests 10 | >>> from requests_kerberos import HTTPKerberosAuth 11 | >>> r = requests.get("http://example.org", auth=HTTPKerberosAuth()) 12 | 13 | The entire `requests.api` should be supported. 14 | """ 15 | import logging 16 | 17 | from .kerberos_ import HTTPKerberosAuth, REQUIRED, OPTIONAL, DISABLED 18 | from .exceptions import MutualAuthenticationError 19 | from .compat import NullHandler 20 | 21 | logging.getLogger(__name__).addHandler(NullHandler()) 22 | 23 | __all__ = ('HTTPKerberosAuth', 'MutualAuthenticationError', 'REQUIRED', 24 | 'OPTIONAL', 'DISABLED') 25 | __version__ = '0.13.0.dev0' 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | import os 4 | import re 5 | from setuptools import setup 6 | 7 | path = os.path.dirname(__file__) 8 | desc_fd = os.path.join(path, 'README.rst') 9 | hist_fd = os.path.join(path, 'HISTORY.rst') 10 | 11 | long_desc = '' 12 | short_desc = 'A Kerberos authentication handler for python-requests' 13 | 14 | if os.path.isfile(desc_fd): 15 | with open(desc_fd) as fd: 16 | long_desc = fd.read() 17 | 18 | if os.path.isfile(hist_fd): 19 | with open(hist_fd) as fd: 20 | long_desc = '\n\n'.join([long_desc, fd.read()]) 21 | 22 | 23 | def get_version(): 24 | """ 25 | Simple function to extract the current version using regular expressions. 26 | """ 27 | reg = re.compile(r'__version__ = [\'"]([^\'"]*)[\'"]') 28 | with open('requests_kerberos/__init__.py') as fd: 29 | matches = list(filter(lambda x: x, map(reg.match, fd))) 30 | 31 | if not matches: 32 | raise RuntimeError( 33 | 'Could not find the version information for requests_kerberos' 34 | ) 35 | 36 | return matches[0].group(1) 37 | 38 | 39 | setup( 40 | name='requests-kerberos', 41 | description=short_desc, 42 | long_description=long_desc, 43 | author='Ian Cordasco, Cory Benfield, Michael Komitee', 44 | author_email='graffatcolmingov@gmail.com', 45 | url='https://github.com/requests/requests-kerberos', 46 | packages=['requests_kerberos'], 47 | package_data={'': ['LICENSE', 'AUTHORS']}, 48 | include_package_data=True, 49 | version=get_version(), 50 | install_requires=[ 51 | 'requests>=1.1.0', 52 | 'cryptography>=1.3;python_version!="3.3"', 53 | 'cryptography>=1.3,<2;python_version=="3.3"' 54 | ], 55 | extras_require={ 56 | ':sys_platform=="win32"': ['winkerberos>=0.5.0'], 57 | ':sys_platform!="win32"': ['pykerberos>=1.1.8,<2.0.0'], 58 | }, 59 | test_suite='test_requests_kerberos', 60 | tests_require=['mock'], 61 | ) 62 | -------------------------------------------------------------------------------- /tests/test_functional_kerberos.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import os 3 | import unittest 4 | 5 | from requests_kerberos import HTTPKerberosAuth, REQUIRED 6 | 7 | 8 | class KerberosFunctionalTestCase(unittest.TestCase): 9 | """ 10 | This test is designed to run functional tests against a live website 11 | secured with Kerberos authentication. See .travis.sh for the script that 12 | is used to setup a Kerberos realm and Apache site. 13 | 14 | For this test to run the 2 environment variables need to be set 15 | KERBEROS_PRINCIPAL: The principal to authenticate with (user@REALM.COM) 16 | Before running this test you need to ensure you have gotten a valid 17 | ticket for the user in that realm using kinit. 18 | KERBEROS_URL: The URL (http://host.realm.com) to authenticate with 19 | This need to be set up before hand 20 | """ 21 | 22 | def setUp(self): 23 | """Setup.""" 24 | self.principal = os.environ.get('KERBEROS_PRINCIPAL', None) 25 | self.url = os.environ.get('KERBEROS_URL', None) 26 | 27 | # Skip the test if not set 28 | if self.principal is None: 29 | raise unittest.SkipTest("KERBEROS_PRINCIPAL is not set, skipping functional tests") 30 | if self.url is None: 31 | raise unittest.SkipTest("KERBEROS_URL is not set, skipping functional tests") 32 | 33 | def test_successful_http_call(self): 34 | session = requests.Session() 35 | if self.url.startswith("https://"): 36 | session.verify = False 37 | 38 | session.auth = HTTPKerberosAuth(mutual_authentication=REQUIRED, principal=self.principal) 39 | request = requests.Request('GET', self.url) 40 | prepared_request = session.prepare_request(request) 41 | 42 | response = session.send(prepared_request) 43 | 44 | assert response.status_code == 200, "HTTP response with kerberos auth did not return a 200 error code" 45 | 46 | if __name__ == '__main__': 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | History 2 | ======= 3 | 4 | 0.12.0: 2017-12-20 5 | ------------------------ 6 | 7 | - Add support for channel binding tokens (assumes pykerberos support >= 1.2.1) 8 | - Add support for kerberos message encryption (assumes pykerberos support >= 1.2.1) 9 | - Misc CI/test fixes 10 | 11 | 0.11.0: 2016-11-02 12 | ------------------ 13 | 14 | - Switch dependency on Windows from kerberos-sspi/pywin32 to WinKerberos. 15 | This brings Custom Principal support to Windows users. 16 | 17 | 0.10.0: 2016-05-18 18 | ------------------ 19 | 20 | - Make it possible to receive errors without having their contents and headers 21 | stripped. 22 | - Resolve a bug caused by passing the ``principal`` keyword argument to 23 | kerberos-sspi on Windows. 24 | 25 | 0.9.0: 2016-05-06 26 | ----------------- 27 | 28 | - Support for principal, hostname, and realm override. 29 | 30 | - Added support for mutual auth. 31 | 32 | 0.8.0: 2016-01-07 33 | ----------------- 34 | 35 | - Support for Kerberos delegation. 36 | 37 | - Fixed problems declaring kerberos-sspi on Windows installs. 38 | 39 | 0.7.0: 2015-05-04 40 | ----------------- 41 | 42 | - Added Windows native authentication support by adding kerberos-sspi as an 43 | alternative backend. 44 | 45 | - Prevent infinite recursion when a server returns 401 to an authorization 46 | attempt. 47 | 48 | - Reduce the logging during successful responses. 49 | 50 | 0.6.1: 2014-11-14 51 | ----------------- 52 | 53 | - Fix HTTPKerberosAuth not to treat non-file as a file 54 | 55 | - Prevent infinite recursion when GSSErrors occurs 56 | 57 | 0.6: 2014-11-04 58 | --------------- 59 | 60 | - Handle mutual authentication (see pull request 36_) 61 | 62 | All users should upgrade immediately. This has been reported to 63 | oss-security_ and we are awaiting a proper CVE identifier. 64 | 65 | **Update**: We were issued CVE-2014-8650 66 | 67 | - Distribute as a wheel. 68 | 69 | .. _36: https://github.com/requests/requests-kerberos/pull/36 70 | .. _oss-security: http://www.openwall.com/lists/oss-security/ 71 | 72 | 0.5: 2014-05-14 73 | --------------- 74 | 75 | - Allow non-HTTP service principals with HTTPKerberosAuth using a new optional 76 | argument ``service``. 77 | 78 | - Fix bug in ``setup.py`` on distributions where the ``compiler`` module is 79 | not available. 80 | 81 | - Add test dependencies to ``setup.py`` so ``python setup.py test`` will work. 82 | 83 | 0.4: 2013-10-26 84 | --------------- 85 | 86 | - Minor updates in the README 87 | - Change requirements to depend on requests above 1.1.0 88 | 89 | 0.3: 2013-06-02 90 | --------------- 91 | 92 | - Work with servers operating on non-standard ports 93 | 94 | 0.2: 2013-03-26 95 | --------------- 96 | 97 | - Not documented 98 | 99 | 0.1: Never released 100 | ------------------- 101 | 102 | - Initial Release 103 | -------------------------------------------------------------------------------- /.travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | IP_ADDRESS=$(hostname -I) 6 | HOSTNAME=$(cat /etc/hostname) 7 | PY_MAJOR=${PYENV:0:1} 8 | 9 | export KERBEROS_HOSTNAME=$HOSTNAME.$KERBEROS_REALM 10 | export DEBIAN_FRONTEND=noninteractive 11 | 12 | echo "Configure the hosts file for Kerberos to work in a container" 13 | cp /etc/hosts ~/hosts.new 14 | sed -i "/.*$HOSTNAME/c\\$IP_ADDRESS\t$KERBEROS_HOSTNAME" ~/hosts.new 15 | cp -f ~/hosts.new /etc/hosts 16 | 17 | echo "Setting up Kerberos config file at /etc/krb5.conf" 18 | cat > /etc/krb5.conf << EOL 19 | [libdefaults] 20 | default_realm = ${KERBEROS_REALM^^} 21 | dns_lookup_realm = false 22 | dns_lookup_kdc = false 23 | 24 | [realms] 25 | ${KERBEROS_REALM^^} = { 26 | kdc = $KERBEROS_HOSTNAME 27 | admin_server = $KERBEROS_HOSTNAME 28 | } 29 | 30 | [domain_realm] 31 | .$KERBEROS_REALM = ${KERBEROS_REALM^^} 32 | 33 | [logging] 34 | kdc = FILE:/var/log/krb5kdc.log 35 | admin_server = FILE:/var/log/kadmin.log 36 | default = FILE:/var/log/krb5lib.log 37 | EOL 38 | 39 | echo "Setting up kerberos ACL configuration at /etc/krb5kdc/kadm5.acl" 40 | mkdir /etc/krb5kdc 41 | echo -e "*/*@${KERBEROS_REALM^^}\t*" > /etc/krb5kdc/kadm5.acl 42 | 43 | echo "Installing all the packages required in this test" 44 | apt-get update 45 | apt-get \ 46 | -y \ 47 | -qq \ 48 | install \ 49 | krb5-{user,kdc,admin-server,multidev} \ 50 | libkrb5-dev \ 51 | wget \ 52 | curl \ 53 | apache2 \ 54 | libapache2-mod-auth-gssapi \ 55 | python-dev \ 56 | libffi-dev \ 57 | build-essential \ 58 | libssl-dev \ 59 | zlib1g-dev \ 60 | libbz2-dev 61 | 62 | echo "Creating KDC database" 63 | # krb5_newrealm returns non-0 return code as it is running in a container, ignore it for this command only 64 | set +e 65 | printf "$KERBEROS_PASSWORD\n$KERBEROS_PASSWORD" | krb5_newrealm 66 | set -e 67 | 68 | echo "Creating principals for tests" 69 | kadmin.local -q "addprinc -pw $KERBEROS_PASSWORD $KERBEROS_USERNAME" 70 | 71 | echo "Adding HTTP principal for Kerberos and create keytab" 72 | kadmin.local -q "addprinc -randkey HTTP/$KERBEROS_HOSTNAME" 73 | kadmin.local -q "ktadd -k /etc/krb5.keytab HTTP/$KERBEROS_HOSTNAME" 74 | chmod 777 /etc/krb5.keytab 75 | 76 | echo "Restarting Kerberos KDS service" 77 | service krb5-kdc restart 78 | 79 | echo "Add ServerName to Apache config" 80 | grep -q -F "ServerName $KERBEROS_HOSTNAME" /etc/apache2/apache2.conf || echo "ServerName $KERBEROS_HOSTNAME" >> /etc/apache2/apache2.conf 81 | 82 | echo "Deleting default virtual host file" 83 | rm /etc/apache2/sites-enabled/000-default.conf 84 | rm /etc/apache2/sites-available/000-default.conf 85 | rm /etc/apache2/sites-available/default-ssl.conf 86 | 87 | echo "Create website directory structure and pages" 88 | mkdir -p /var/www/example.com/public_html 89 | chmod -R 755 /var/www 90 | echo "Titlebody mesage" > /var/www/example.com/public_html/index.html 91 | 92 | echo "Create self signed certificate for HTTPS endpoint" 93 | mkdir /etc/apache2/ssl 94 | openssl req \ 95 | -x509 \ 96 | -nodes \ 97 | -days 365 \ 98 | -newkey rsa:2048 \ 99 | -keyout /etc/apache2/ssl/https.key \ 100 | -out /etc/apache2/ssl/https.crt \ 101 | -subj "/CN=$KERBEROS_HOSTNAME/o=Testing LTS./C=US" 102 | 103 | echo "Create virtual host files" 104 | cat > /etc/apache2/sites-available/example.com.conf << EOL 105 | 106 | ServerName $KERBEROS_HOSTNAME 107 | ServerAlias $KERBEROS_HOSTNAME 108 | DocumentRoot /var/www/example.com/public_html 109 | ErrorLog ${APACHE_LOG_DIR}/error.log 110 | CustomLog ${APACHE_LOG_DIR}/access.log combined 111 | 112 | AuthType GSSAPI 113 | AuthName "GSSAPI Single Sign On Login" 114 | Require user $KERBEROS_USERNAME@${KERBEROS_REALM^^} 115 | GssapiCredStore keytab:/etc/krb5.keytab 116 | 117 | 118 | 119 | ServerName $KERBEROS_HOSTNAME 120 | ServerAlias $KERBEROS_HOSTNAME 121 | DocumentRoot /var/www/example.com/public_html 122 | ErrorLog ${APACHE_LOG_DIR}/error.log 123 | CustomLog ${APACHE_LOG_DIR}/access.log combined 124 | SSLEngine on 125 | SSLCertificateFile /etc/apache2/ssl/https.crt 126 | SSLCertificateKeyFile /etc/apache2/ssl/https.key 127 | 128 | AuthType GSSAPI 129 | AuthName "GSSAPI Single Sign On Login" 130 | Require user $KERBEROS_USERNAME@${KERBEROS_REALM^^} 131 | GssapiCredStore keytab:/etc/krb5.keytab 132 | 133 | 134 | EOL 135 | 136 | echo "Enabling virtual host site" 137 | a2enmod ssl 138 | a2ensite example.com.conf 139 | service apache2 restart 140 | 141 | echo "Getting ticket for Kerberos user" 142 | echo -n "$KERBEROS_PASSWORD" | kinit "$KERBEROS_USERNAME@${KERBEROS_REALM^^}" 143 | 144 | echo "Try out the HTTP connection with curl" 145 | CURL_OUTPUT=$(curl --negotiate -u : "http://$KERBEROS_HOSTNAME") 146 | 147 | if [ "$CURL_OUTPUT" != "Titlebody mesage" ]; then 148 | echo -e "ERROR: Did not get success message, cannot continue with actual tests:\nActual Output:\n$CURL_OUTPUT" 149 | exit 1 150 | else 151 | echo -e "SUCCESS: Apache site built and set for Kerberos auth\nActual Output:\n$CURL_OUTPUT" 152 | fi 153 | 154 | echo "Try out the HTTPS connection with curl" 155 | CURL_OUTPUT=$(curl --negotiate -u : "https://$KERBEROS_HOSTNAME" --insecure) 156 | 157 | if [ "$CURL_OUTPUT" != "Titlebody mesage" ]; then 158 | echo -e "ERROR: Did not get success message, cannot continue with actual tests:\nActual Output:\n$CURL_OUTPUT" 159 | exit 1 160 | else 161 | echo -e "SUCCESS: Apache site built and set for Kerberos auth\nActual Output:\n$CURL_OUTPUT" 162 | fi 163 | 164 | if [ "$IMAGE" == "ubuntu:16.04" ]; then 165 | echo "Downloading Python $PYENV" 166 | wget -q "https://www.python.org/ftp/python/$PYENV/Python-$PYENV.tgz" 167 | tar xzf "Python-$PYENV.tgz" 168 | cd "Python-$PYENV" 169 | 170 | echo "Configuring Python install" 171 | ./configure &> /dev/null 172 | 173 | echo "Running make install on Python" 174 | make install &> /dev/null 175 | cd .. 176 | rm -rf "Python-$PYENV" 177 | rm "Python-$PYENV.tgz" 178 | fi 179 | 180 | echo "Installing Pip" 181 | wget -q https://bootstrap.pypa.io/get-pip.py 182 | python$PY_MAJOR get-pip.py 183 | rm get-pip.py 184 | 185 | echo "Updating pip and installing library" 186 | pip$PY_MAJOR install -U pip setuptools 187 | pip$PY_MAJOR install . 188 | pip$PY_MAJOR install -r requirements-test.txt 189 | 190 | echo "Outputting build info before tests" 191 | echo "Python Version: $(python$PY_MAJOR --version 2>&1)" 192 | echo "Pip Version: $(pip$PY_MAJOR --version)" 193 | echo "Pip packages: $(pip$PY_MAJOR list)" 194 | 195 | echo "Running Python tests" 196 | export KERBEROS_PRINCIPAL="$KERBEROS_USERNAME@${KERBEROS_REALM^^}" 197 | export KERBEROS_URL="http://$KERBEROS_HOSTNAME" 198 | python$PY_MAJOR -m pytest -v --cov=requests_kerberos\ 199 | 200 | echo "Running Python test over HTTPS for basic CBT test" 201 | export KERBEROS_URL="https://$KERBEROS_HOSTNAME" 202 | python$PY_MAJOR -m pytest -v --cov=requests_kerberos\ 203 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | requests Kerberos/GSSAPI authentication library 2 | =============================================== 3 | 4 | .. image:: https://travis-ci.org/requests/requests-kerberos.svg?branch=master 5 | :target: https://travis-ci.org/requests/requests-kerberos 6 | 7 | .. image:: https://coveralls.io/repos/github/requests/requests-kerberos/badge.svg?branch=master 8 | :target: https://coveralls.io/github/requests/requests-kerberos?branch=master 9 | 10 | Requests is an HTTP library, written in Python, for human beings. This library 11 | adds optional Kerberos/GSSAPI authentication support and supports mutual 12 | authentication. Basic GET usage: 13 | 14 | 15 | .. code-block:: python 16 | 17 | >>> import requests 18 | >>> from requests_kerberos import HTTPKerberosAuth 19 | >>> r = requests.get("http://example.org", auth=HTTPKerberosAuth()) 20 | ... 21 | 22 | The entire ``requests.api`` should be supported. 23 | 24 | Authentication Failures 25 | ----------------------- 26 | 27 | Client authentication failures will be communicated to the caller by returning 28 | the 401 response. 29 | 30 | Mutual Authentication 31 | --------------------- 32 | 33 | REQUIRED 34 | ^^^^^^^^ 35 | 36 | By default, ``HTTPKerberosAuth`` will require mutual authentication from the 37 | server, and if a server emits a non-error response which cannot be 38 | authenticated, a ``requests_kerberos.errors.MutualAuthenticationError`` will 39 | be raised. If a server emits an error which cannot be authenticated, it will 40 | be returned to the user but with its contents and headers stripped. If the 41 | response content is more important than the need for mutual auth on errors, 42 | (eg, for certain WinRM calls) the stripping behavior can be suppressed by 43 | setting ``sanitize_mutual_error_response=False``: 44 | 45 | .. code-block:: python 46 | 47 | >>> import requests 48 | >>> from requests_kerberos import HTTPKerberosAuth, REQUIRED 49 | >>> kerberos_auth = HTTPKerberosAuth(mutual_authentication=REQUIRED, sanitize_mutual_error_response=False) 50 | >>> r = requests.get("https://windows.example.org/wsman", auth=kerberos_auth) 51 | ... 52 | 53 | 54 | OPTIONAL 55 | ^^^^^^^^ 56 | 57 | If you'd prefer to not require mutual authentication, you can set your 58 | preference when constructing your ``HTTPKerberosAuth`` object: 59 | 60 | .. code-block:: python 61 | 62 | >>> import requests 63 | >>> from requests_kerberos import HTTPKerberosAuth, OPTIONAL 64 | >>> kerberos_auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL) 65 | >>> r = requests.get("http://example.org", auth=kerberos_auth) 66 | ... 67 | 68 | This will cause ``requests_kerberos`` to attempt mutual authentication if the 69 | server advertises that it supports it, and cause a failure if authentication 70 | fails, but not if the server does not support it at all. 71 | 72 | DISABLED 73 | ^^^^^^^^ 74 | 75 | While we don't recommend it, if you'd prefer to never attempt mutual 76 | authentication, you can do that as well: 77 | 78 | .. code-block:: python 79 | 80 | >>> import requests 81 | >>> from requests_kerberos import HTTPKerberosAuth, DISABLED 82 | >>> kerberos_auth = HTTPKerberosAuth(mutual_authentication=DISABLED) 83 | >>> r = requests.get("http://example.org", auth=kerberos_auth) 84 | ... 85 | 86 | Preemptive Authentication 87 | ------------------------- 88 | 89 | ``HTTPKerberosAuth`` can be forced to preemptively initiate the Kerberos 90 | GSS exchange and present a Kerberos ticket on the initial request (and all 91 | subsequent). By default, authentication only occurs after a 92 | ``401 Unauthorized`` response containing a Kerberos or Negotiate challenge 93 | is received from the origin server. This can cause mutual authentication 94 | failures for hosts that use a persistent connection (eg, Windows/WinRM), as 95 | no Kerberos challenges are sent after the initial auth handshake. This 96 | behavior can be altered by setting ``force_preemptive=True``: 97 | 98 | .. code-block:: python 99 | 100 | >>> import requests 101 | >>> from requests_kerberos import HTTPKerberosAuth, REQUIRED 102 | >>> kerberos_auth = HTTPKerberosAuth(mutual_authentication=REQUIRED, force_preemptive=True) 103 | >>> r = requests.get("https://windows.example.org/wsman", auth=kerberos_auth) 104 | ... 105 | 106 | Hostname Override 107 | ----------------- 108 | 109 | If communicating with a host whose DNS name doesn't match its 110 | kerberos hostname (eg, behind a content switch or load balancer), 111 | the hostname used for the Kerberos GSS exchange can be overridden by 112 | setting the ``hostname_override`` arg: 113 | 114 | .. code-block:: python 115 | 116 | >>> import requests 117 | >>> from requests_kerberos import HTTPKerberosAuth, REQUIRED 118 | >>> kerberos_auth = HTTPKerberosAuth(hostname_override="internalhost.local") 119 | >>> r = requests.get("https://externalhost.example.org/", auth=kerberos_auth) 120 | ... 121 | 122 | If ``externalhost.example.org`` is a CNAME that can be resolved to 123 | ``internalhost.local`` via DNS, then you can have ``requests-kerberos`` 124 | use the resolved version of the hostname for Kerberos by specifying the 125 | ``canonicalize_hostname`` flag: 126 | 127 | .. code-block:: python 128 | 129 | >>> import requests 130 | >>> from requests_kerberos import HTTPKerberosAuth 131 | >>> kerberos_auth = HTTPKerberosAuth(canonicalize_hostname=True) 132 | >>> r = requests.get("https://externalhost.example.org/", auth=kerberos_auth) 133 | ... 134 | 135 | Canonicalizing hostnames in this way is the default behaviour for almost all browsers. 136 | We don't match this behaviour because it can weaken security in the case where 137 | mutual authentication is being used over unauthenticated HTTP to validate the identity 138 | of the server. 139 | 140 | Explicit Principal 141 | ------------------ 142 | 143 | ``HTTPKerberosAuth`` normally uses the default principal (ie, the user for 144 | whom you last ran ``kinit`` or ``kswitch``, or an SSO credential if 145 | applicable). However, an explicit principal can be specified, which will 146 | cause Kerberos to look for a matching credential cache for the named user. 147 | This feature depends on OS support for collection-type credential caches, 148 | as well as working principal support in PyKerberos (it is broken in many 149 | builds). An explicit principal can be specified with the ``principal`` arg: 150 | 151 | .. code-block:: python 152 | 153 | >>> import requests 154 | >>> from requests_kerberos import HTTPKerberosAuth, REQUIRED 155 | >>> kerberos_auth = HTTPKerberosAuth(principal="user@REALM") 156 | >>> r = requests.get("http://example.org", auth=kerberos_auth) 157 | ... 158 | 159 | On Windows, WinKerberos is used instead of PyKerberos. WinKerberos allows the 160 | use of arbitrary principals instead of a credential cache. Passwords can be 161 | specified by following the form ``user@realm:password`` for ``principal``. 162 | 163 | Delegation 164 | ---------- 165 | 166 | ``requests_kerberos`` supports credential delegation (``GSS_C_DELEG_FLAG``). 167 | To enable delegation of credentials to a server that requests delegation, pass 168 | ``delegate=True`` to ``HTTPKerberosAuth``: 169 | 170 | .. code-block:: python 171 | 172 | >>> import requests 173 | >>> from requests_kerberos import HTTPKerberosAuth 174 | >>> r = requests.get("http://example.org", auth=HTTPKerberosAuth(delegate=True)) 175 | ... 176 | 177 | Be careful to only allow delegation to servers you trust as they will be able 178 | to impersonate you using the delegated credentials. 179 | 180 | Logging 181 | ------- 182 | 183 | This library makes extensive use of Python's logging facilities. 184 | 185 | Log messages are logged to the ``requests_kerberos`` and 186 | ``requests_kerberos.kerberos_`` named loggers. 187 | 188 | If you are having difficulty we suggest you configure logging. Issues with the 189 | underlying kerberos libraries will be made apparent. Additionally, copious debug 190 | information is made available which may assist in troubleshooting if you 191 | increase your log level all the way up to debug. 192 | -------------------------------------------------------------------------------- /requests_kerberos/kerberos_.py: -------------------------------------------------------------------------------- 1 | try: 2 | import kerberos 3 | except ImportError: 4 | import winkerberos as kerberos 5 | import logging 6 | import socket 7 | import re 8 | import sys 9 | import warnings 10 | 11 | from cryptography import x509 12 | from cryptography.hazmat.backends import default_backend 13 | from cryptography.hazmat.primitives import hashes 14 | from cryptography.exceptions import UnsupportedAlgorithm 15 | 16 | from requests.auth import AuthBase 17 | from requests.models import Response 18 | from requests.compat import urlparse, StringIO 19 | from requests.structures import CaseInsensitiveDict 20 | from requests.cookies import cookiejar_from_dict 21 | from requests.packages.urllib3 import HTTPResponse 22 | 23 | from .exceptions import MutualAuthenticationError, KerberosExchangeError 24 | 25 | log = logging.getLogger(__name__) 26 | 27 | # Different types of mutual authentication: 28 | # with mutual_authentication set to REQUIRED, all responses will be 29 | # authenticated with the exception of errors. Errors will have their contents 30 | # and headers stripped. If a non-error response cannot be authenticated, a 31 | # MutualAuthenticationError exception will be raised. 32 | # with mutual_authentication set to OPTIONAL, mutual authentication will be 33 | # attempted if supported, and if supported and failed, a 34 | # MutualAuthenticationError exception will be raised. Responses which do not 35 | # support mutual authentication will be returned directly to the user. 36 | # with mutual_authentication set to DISABLED, mutual authentication will not be 37 | # attempted, even if supported. 38 | REQUIRED = 1 39 | OPTIONAL = 2 40 | DISABLED = 3 41 | 42 | 43 | class NoCertificateRetrievedWarning(Warning): 44 | pass 45 | 46 | class UnknownSignatureAlgorithmOID(Warning): 47 | pass 48 | 49 | 50 | class SanitizedResponse(Response): 51 | """The :class:`Response ` object, which contains a server's 52 | response to an HTTP request. 53 | 54 | This differs from `requests.models.Response` in that it's headers and 55 | content have been sanitized. This is only used for HTTP Error messages 56 | which do not support mutual authentication when mutual authentication is 57 | required.""" 58 | 59 | def __init__(self, response): 60 | super(SanitizedResponse, self).__init__() 61 | self.status_code = response.status_code 62 | self.encoding = response.encoding 63 | self.raw = response.raw 64 | self.reason = response.reason 65 | self.url = response.url 66 | self.request = response.request 67 | self.connection = response.connection 68 | self._content_consumed = True 69 | 70 | self._content = "" 71 | self.cookies = cookiejar_from_dict({}) 72 | self.headers = CaseInsensitiveDict() 73 | self.headers['content-length'] = '0' 74 | for header in ('date', 'server'): 75 | if header in response.headers: 76 | self.headers[header] = response.headers[header] 77 | 78 | 79 | def _negotiate_value(response): 80 | """Extracts the gssapi authentication token from the appropriate header""" 81 | if hasattr(_negotiate_value, 'regex'): 82 | regex = _negotiate_value.regex 83 | else: 84 | # There's no need to re-compile this EVERY time it is called. Compile 85 | # it once and you won't have the performance hit of the compilation. 86 | regex = re.compile('(?:.*,)*\s*Negotiate\s*([^,]*),?', re.I) 87 | _negotiate_value.regex = regex 88 | 89 | authreq = response.headers.get('www-authenticate', None) 90 | 91 | if authreq: 92 | match_obj = regex.search(authreq) 93 | if match_obj: 94 | return match_obj.group(1) 95 | 96 | return None 97 | 98 | 99 | def _get_certificate_hash(certificate_der): 100 | # https://tools.ietf.org/html/rfc5929#section-4.1 101 | cert = x509.load_der_x509_certificate(certificate_der, default_backend()) 102 | 103 | try: 104 | hash_algorithm = cert.signature_hash_algorithm 105 | except UnsupportedAlgorithm as ex: 106 | warnings.warn("Failed to get signature algorithm from certificate, " 107 | "unable to pass channel bindings: %s" % str(ex), UnknownSignatureAlgorithmOID) 108 | return None 109 | 110 | # if the cert signature algorithm is either md5 or sha1 then use sha256 111 | # otherwise use the signature algorithm 112 | if hash_algorithm.name in ['md5', 'sha1']: 113 | digest = hashes.Hash(hashes.SHA256(), default_backend()) 114 | else: 115 | digest = hashes.Hash(hash_algorithm, default_backend()) 116 | 117 | digest.update(certificate_der) 118 | certificate_hash = digest.finalize() 119 | 120 | return certificate_hash 121 | 122 | 123 | def _get_channel_bindings_application_data(response): 124 | """ 125 | https://tools.ietf.org/html/rfc5929 4. The 'tls-server-end-point' Channel Binding Type 126 | 127 | Gets the application_data value for the 'tls-server-end-point' CBT Type. 128 | This is ultimately the SHA256 hash of the certificate of the HTTPS endpoint 129 | appended onto tls-server-end-point. This value is then passed along to the 130 | kerberos library to bind to the auth response. If the socket is not an SSL 131 | socket or the raw HTTP object is not a urllib3 HTTPResponse then None will 132 | be returned and the Kerberos auth will use GSS_C_NO_CHANNEL_BINDINGS 133 | 134 | :param response: The original 401 response from the server 135 | :return: byte string used on the application_data.value field on the CBT struct 136 | """ 137 | 138 | application_data = None 139 | raw_response = response.raw 140 | 141 | if isinstance(raw_response, HTTPResponse): 142 | try: 143 | if sys.version_info > (3, 0): 144 | socket = raw_response._fp.fp.raw._sock 145 | else: 146 | socket = raw_response._fp.fp._sock 147 | except AttributeError: 148 | warnings.warn("Failed to get raw socket for CBT; has urllib3 impl changed", 149 | NoCertificateRetrievedWarning) 150 | else: 151 | try: 152 | server_certificate = socket.getpeercert(True) 153 | except AttributeError: 154 | pass 155 | else: 156 | certificate_hash = _get_certificate_hash(server_certificate) 157 | application_data = b'tls-server-end-point:' + certificate_hash 158 | else: 159 | warnings.warn( 160 | "Requests is running with a non urllib3 backend, cannot retrieve server certificate for CBT", 161 | NoCertificateRetrievedWarning) 162 | 163 | return application_data 164 | 165 | 166 | # Return the hostname as it will be canonicalized by 167 | # krb5_sname_to_principal. We can't simply use socket.getfqdn() 168 | # because it explicitly prefers results containing periods and 169 | # krb5_sname_to_principal doesn't care. 170 | # 171 | # This is necessary to support use cases where multiple services 172 | # are hosted on the same machine under multiple CNAMEs. It won't do 173 | # any good to use a SPN that mentions one of the CNAMES, we need to ask for the real machine. 174 | # 175 | # Maybe we should use socket.getfqdn here, but that doesn't use AI_CANONNAME 176 | # but rather chooses the first entry in gethostbyaddr that contains a dot, which seems 177 | # a trifle strange: https://github.com/python/cpython/blob/master/Lib/socket.py#L681 178 | # 179 | # https://github.com/krb5/krb5/blob/d406afa363554097ac48646a29249c04f498c88e/src/util/k5test.py#L505-L520 180 | # https://chromium.googlesource.com/chromium/src/+/lkgr/net/http/http_auth_handler_negotiate.cc#150 181 | def _get_default_kerb_host(hostname): 182 | try: 183 | ais = socket.getaddrinfo(hostname, None, 0, 0, 0, socket.AI_CANONNAME) 184 | except socket.gaierror: 185 | ais = [] 186 | 187 | if not ais: 188 | return hostname.lower() 189 | 190 | _family, _socktype, _proto, canonname, _sockaddr = ais[0] 191 | return canonname.lower() 192 | 193 | 194 | class HTTPKerberosAuth(AuthBase): 195 | """Attaches HTTP GSSAPI/Kerberos Authentication to the given Request 196 | object.""" 197 | def __init__( 198 | self, mutual_authentication=REQUIRED, 199 | service="HTTP", delegate=False, force_preemptive=False, 200 | principal=None, hostname_override=None, 201 | sanitize_mutual_error_response=True, send_cbt=True, 202 | canonicalize_hostname=False): 203 | self.context = {} 204 | self.mutual_authentication = mutual_authentication 205 | self.delegate = delegate 206 | self.pos = None 207 | self.service = service 208 | self.force_preemptive = force_preemptive 209 | self.principal = principal 210 | self.hostname_override = hostname_override 211 | self.canonicalize_hostname = canonicalize_hostname 212 | self.sanitize_mutual_error_response = sanitize_mutual_error_response 213 | self.auth_done = False 214 | self.winrm_encryption_available = hasattr(kerberos, 'authGSSWinRMEncryptMessage') 215 | 216 | # Set the CBT values populated after the first response 217 | self.send_cbt = send_cbt 218 | self.cbt_binding_tried = False 219 | self.cbt_struct = None 220 | 221 | def generate_request_header(self, response, host, is_preemptive=False): 222 | """ 223 | Generates the GSSAPI authentication token with kerberos. 224 | 225 | If any GSSAPI step fails, raise KerberosExchangeError 226 | with failure detail. 227 | 228 | """ 229 | 230 | # Flags used by kerberos module. 231 | gssflags = kerberos.GSS_C_MUTUAL_FLAG | kerberos.GSS_C_SEQUENCE_FLAG 232 | if self.delegate: 233 | gssflags |= kerberos.GSS_C_DELEG_FLAG 234 | 235 | try: 236 | kerb_stage = "authGSSClientInit()" 237 | # contexts still need to be stored by host, but hostname_override 238 | # allows use of an arbitrary hostname for the kerberos exchange 239 | # (eg, in cases of aliased hosts, internal vs external, CNAMEs 240 | # w/ name-based HTTP hosting) 241 | if self.hostname_override is not None: 242 | kerb_host = self.hostname_override 243 | elif self.canonicalize_hostname: 244 | kerb_host = _get_default_kerb_host(host) 245 | else: 246 | kerb_host = host 247 | kerb_spn = "{0}@{1}".format(self.service, kerb_host) 248 | 249 | result, self.context[host] = kerberos.authGSSClientInit(kerb_spn, 250 | gssflags=gssflags, principal=self.principal) 251 | 252 | if result < 1: 253 | raise EnvironmentError(result, kerb_stage) 254 | 255 | # if we have a previous response from the server, use it to continue 256 | # the auth process, otherwise use an empty value 257 | negotiate_resp_value = '' if is_preemptive else _negotiate_value(response) 258 | 259 | kerb_stage = "authGSSClientStep()" 260 | # If this is set pass along the struct to Kerberos 261 | if self.cbt_struct: 262 | result = kerberos.authGSSClientStep(self.context[host], 263 | negotiate_resp_value, 264 | channel_bindings=self.cbt_struct) 265 | else: 266 | result = kerberos.authGSSClientStep(self.context[host], 267 | negotiate_resp_value) 268 | 269 | if result < 0: 270 | raise EnvironmentError(result, kerb_stage) 271 | 272 | kerb_stage = "authGSSClientResponse()" 273 | gss_response = kerberos.authGSSClientResponse(self.context[host]) 274 | 275 | return "Negotiate {0}".format(gss_response) 276 | 277 | except kerberos.GSSError as error: 278 | log.exception( 279 | "generate_request_header(): {0} failed:".format(kerb_stage)) 280 | log.exception(error) 281 | raise KerberosExchangeError("%s failed: %s" % (kerb_stage, str(error.args))) 282 | 283 | except EnvironmentError as error: 284 | # ensure we raised this for translation to KerberosExchangeError 285 | # by comparing errno to result, re-raise if not 286 | if error.errno != result: 287 | raise 288 | message = "{0} failed, result: {1}".format(kerb_stage, result) 289 | log.error("generate_request_header(): {0}".format(message)) 290 | raise KerberosExchangeError(message) 291 | 292 | def authenticate_user(self, response, **kwargs): 293 | """Handles user authentication with gssapi/kerberos""" 294 | 295 | host = urlparse(response.url).hostname 296 | 297 | try: 298 | auth_header = self.generate_request_header(response, host) 299 | except KerberosExchangeError: 300 | # GSS Failure, return existing response 301 | return response 302 | 303 | log.debug("authenticate_user(): Authorization header: {0}".format( 304 | auth_header)) 305 | response.request.headers['Authorization'] = auth_header 306 | 307 | # Consume the content so we can reuse the connection for the next 308 | # request. 309 | response.content 310 | response.raw.release_conn() 311 | 312 | _r = response.connection.send(response.request, **kwargs) 313 | _r.history.append(response) 314 | 315 | log.debug("authenticate_user(): returning {0}".format(_r)) 316 | return _r 317 | 318 | def handle_401(self, response, **kwargs): 319 | """Handles 401's, attempts to use gssapi/kerberos authentication""" 320 | 321 | log.debug("handle_401(): Handling: 401") 322 | if _negotiate_value(response) is not None: 323 | _r = self.authenticate_user(response, **kwargs) 324 | log.debug("handle_401(): returning {0}".format(_r)) 325 | return _r 326 | else: 327 | log.debug("handle_401(): Kerberos is not supported") 328 | log.debug("handle_401(): returning {0}".format(response)) 329 | return response 330 | 331 | def handle_other(self, response): 332 | """Handles all responses with the exception of 401s. 333 | 334 | This is necessary so that we can authenticate responses if requested""" 335 | 336 | log.debug("handle_other(): Handling: %d" % response.status_code) 337 | 338 | if self.mutual_authentication in (REQUIRED, OPTIONAL) and not self.auth_done: 339 | 340 | is_http_error = response.status_code >= 400 341 | 342 | if _negotiate_value(response) is not None: 343 | log.debug("handle_other(): Authenticating the server") 344 | if not self.authenticate_server(response): 345 | # Mutual authentication failure when mutual auth is wanted, 346 | # raise an exception so the user doesn't use an untrusted 347 | # response. 348 | log.error("handle_other(): Mutual authentication failed") 349 | raise MutualAuthenticationError("Unable to authenticate " 350 | "{0}".format(response)) 351 | 352 | # Authentication successful 353 | log.debug("handle_other(): returning {0}".format(response)) 354 | self.auth_done = True 355 | return response 356 | 357 | elif is_http_error or self.mutual_authentication == OPTIONAL: 358 | if not response.ok: 359 | log.error("handle_other(): Mutual authentication unavailable " 360 | "on {0} response".format(response.status_code)) 361 | 362 | if(self.mutual_authentication == REQUIRED and 363 | self.sanitize_mutual_error_response): 364 | return SanitizedResponse(response) 365 | else: 366 | return response 367 | else: 368 | # Unable to attempt mutual authentication when mutual auth is 369 | # required, raise an exception so the user doesn't use an 370 | # untrusted response. 371 | log.error("handle_other(): Mutual authentication failed") 372 | raise MutualAuthenticationError("Unable to authenticate " 373 | "{0}".format(response)) 374 | else: 375 | log.debug("handle_other(): returning {0}".format(response)) 376 | return response 377 | 378 | def authenticate_server(self, response): 379 | """ 380 | Uses GSSAPI to authenticate the server. 381 | 382 | Returns True on success, False on failure. 383 | """ 384 | 385 | log.debug("authenticate_server(): Authenticate header: {0}".format( 386 | _negotiate_value(response))) 387 | 388 | host = urlparse(response.url).hostname 389 | 390 | try: 391 | # If this is set pass along the struct to Kerberos 392 | if self.cbt_struct: 393 | result = kerberos.authGSSClientStep(self.context[host], 394 | _negotiate_value(response), 395 | channel_bindings=self.cbt_struct) 396 | else: 397 | result = kerberos.authGSSClientStep(self.context[host], 398 | _negotiate_value(response)) 399 | except kerberos.GSSError: 400 | log.exception("authenticate_server(): authGSSClientStep() failed:") 401 | return False 402 | 403 | if result < 1: 404 | log.error("authenticate_server(): authGSSClientStep() failed: " 405 | "{0}".format(result)) 406 | return False 407 | 408 | log.debug("authenticate_server(): returning {0}".format(response)) 409 | return True 410 | 411 | def handle_response(self, response, **kwargs): 412 | """Takes the given response and tries kerberos-auth, as needed.""" 413 | num_401s = kwargs.pop('num_401s', 0) 414 | 415 | # Check if we have already tried to get the CBT data value 416 | if not self.cbt_binding_tried and self.send_cbt: 417 | # If we haven't tried, try getting it now 418 | cbt_application_data = _get_channel_bindings_application_data(response) 419 | if cbt_application_data: 420 | # Only the latest version of pykerberos has this method available 421 | try: 422 | self.cbt_struct = kerberos.channelBindings(application_data=cbt_application_data) 423 | except AttributeError: 424 | # Using older version set to None 425 | self.cbt_struct = None 426 | # Regardless of the result, set tried to True so we don't waste time next time 427 | self.cbt_binding_tried = True 428 | 429 | if self.pos is not None: 430 | # Rewind the file position indicator of the body to where 431 | # it was to resend the request. 432 | response.request.body.seek(self.pos) 433 | 434 | if response.status_code == 401 and num_401s < 2: 435 | # 401 Unauthorized. Handle it, and if it still comes back as 401, 436 | # that means authentication failed. 437 | _r = self.handle_401(response, **kwargs) 438 | log.debug("handle_response(): returning %s", _r) 439 | log.debug("handle_response() has seen %d 401 responses", num_401s) 440 | num_401s += 1 441 | return self.handle_response(_r, num_401s=num_401s, **kwargs) 442 | elif response.status_code == 401 and num_401s >= 2: 443 | # Still receiving 401 responses after attempting to handle them. 444 | # Authentication has failed. Return the 401 response. 445 | log.debug("handle_response(): returning 401 %s", response) 446 | return response 447 | else: 448 | _r = self.handle_other(response) 449 | log.debug("handle_response(): returning %s", _r) 450 | return _r 451 | 452 | def deregister(self, response): 453 | """Deregisters the response handler""" 454 | response.request.deregister_hook('response', self.handle_response) 455 | 456 | def wrap_winrm(self, host, message): 457 | if not self.winrm_encryption_available: 458 | raise NotImplementedError("WinRM encryption is not available on the installed version of pykerberos") 459 | 460 | return kerberos.authGSSWinRMEncryptMessage(self.context[host], message) 461 | 462 | def unwrap_winrm(self, host, message, header): 463 | if not self.winrm_encryption_available: 464 | raise NotImplementedError("WinRM encryption is not available on the installed version of pykerberos") 465 | 466 | return kerberos.authGSSWinRMDecryptMessage(self.context[host], message, header) 467 | 468 | def __call__(self, request): 469 | if self.force_preemptive and not self.auth_done: 470 | # add Authorization header before we receive a 401 471 | # by the 401 handler 472 | host = urlparse(request.url).hostname 473 | 474 | auth_header = self.generate_request_header(None, host, is_preemptive=True) 475 | 476 | log.debug("HTTPKerberosAuth: Preemptive Authorization header: {0}".format(auth_header)) 477 | 478 | request.headers['Authorization'] = auth_header 479 | 480 | request.register_hook('response', self.handle_response) 481 | try: 482 | self.pos = request.body.tell() 483 | except AttributeError: 484 | # In the case of HTTPKerberosAuth being reused and the body 485 | # of the previous request was a file-like object, pos has 486 | # the file position of the previous body. Ensure it's set to 487 | # None. 488 | self.pos = None 489 | return request 490 | -------------------------------------------------------------------------------- /tests/test_requests_kerberos.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """Tests for requests_kerberos.""" 5 | 6 | import base64 7 | from mock import Mock, patch 8 | from requests.compat import urlparse 9 | import requests 10 | import warnings 11 | import socket 12 | 13 | 14 | try: 15 | import kerberos 16 | kerberos_module_name='kerberos' 17 | except ImportError: 18 | import winkerberos as kerberos # On Windows 19 | kerberos_module_name = 'winkerberos' 20 | 21 | import requests_kerberos 22 | import unittest 23 | from requests_kerberos.kerberos_ import _get_certificate_hash 24 | 25 | # kerberos.authClientInit() is called with the service name (HTTP@FQDN) and 26 | # returns 1 and a kerberos context object on success. Returns -1 on failure. 27 | clientInit_complete = Mock(return_value=(1, "CTX")) 28 | clientInit_error = Mock(return_value=(-1, "CTX")) 29 | 30 | # kerberos.authGSSClientStep() is called with the kerberos context object 31 | # returned by authGSSClientInit and the negotiate auth token provided in the 32 | # http response's www-authenticate header. It returns 0 or 1 on success. 0 33 | # Indicates that authentication is progressing but not complete. 34 | clientStep_complete = Mock(return_value=1) 35 | clientStep_continue = Mock(return_value=0) 36 | clientStep_error = Mock(return_value=-1) 37 | clientStep_exception = Mock(side_effect=kerberos.GSSError) 38 | 39 | # kerberos.authGSSCLientResponse() is called with the kerberos context which 40 | # was initially returned by authGSSClientInit and had been mutated by a call by 41 | # authGSSClientStep. It returns a string. 42 | clientResponse = Mock(return_value="GSSRESPONSE") 43 | 44 | # Note: we're not using the @mock.patch decorator: 45 | # > My only word of warning is that in the past, the patch decorator hides 46 | # > tests when using the standard unittest library. 47 | # > -- sigmavirus24 in https://github.com/requests/requests-kerberos/issues/1 48 | 49 | 50 | class KerberosTestCase(unittest.TestCase): 51 | 52 | def setUp(self): 53 | """Setup.""" 54 | clientInit_complete.reset_mock() 55 | clientInit_error.reset_mock() 56 | clientStep_complete.reset_mock() 57 | clientStep_continue.reset_mock() 58 | clientStep_error.reset_mock() 59 | clientStep_exception.reset_mock() 60 | clientResponse.reset_mock() 61 | 62 | def tearDown(self): 63 | """Teardown.""" 64 | pass 65 | 66 | def test_negotate_value_extraction(self): 67 | response = requests.Response() 68 | response.headers = {'www-authenticate': 'negotiate token'} 69 | self.assertEqual( 70 | requests_kerberos.kerberos_._negotiate_value(response), 71 | 'token' 72 | ) 73 | 74 | def test_negotate_value_extraction_none(self): 75 | response = requests.Response() 76 | response.headers = {} 77 | self.assertTrue( 78 | requests_kerberos.kerberos_._negotiate_value(response) is None 79 | ) 80 | 81 | def test_force_preemptive(self): 82 | with patch.multiple(kerberos_module_name, 83 | authGSSClientInit=clientInit_complete, 84 | authGSSClientResponse=clientResponse, 85 | authGSSClientStep=clientStep_continue): 86 | auth = requests_kerberos.HTTPKerberosAuth(force_preemptive=True) 87 | 88 | request = requests.Request(url="http://www.example.org") 89 | 90 | auth.__call__(request) 91 | 92 | self.assertTrue('Authorization' in request.headers) 93 | self.assertEqual(request.headers.get('Authorization'), 'Negotiate GSSRESPONSE') 94 | 95 | def test_no_force_preemptive(self): 96 | with patch.multiple(kerberos_module_name, 97 | authGSSClientInit=clientInit_complete, 98 | authGSSClientResponse=clientResponse, 99 | authGSSClientStep=clientStep_continue): 100 | auth = requests_kerberos.HTTPKerberosAuth() 101 | 102 | request = requests.Request(url="http://www.example.org") 103 | 104 | auth.__call__(request) 105 | 106 | self.assertTrue('Authorization' not in request.headers) 107 | 108 | def test_generate_request_header(self): 109 | with patch.multiple(kerberos_module_name, 110 | authGSSClientInit=clientInit_complete, 111 | authGSSClientResponse=clientResponse, 112 | authGSSClientStep=clientStep_continue): 113 | response = requests.Response() 114 | response.url = "http://www.example.org/" 115 | response.headers = {'www-authenticate': 'negotiate token'} 116 | host = urlparse(response.url).hostname 117 | auth = requests_kerberos.HTTPKerberosAuth() 118 | self.assertEqual( 119 | auth.generate_request_header(response, host), 120 | "Negotiate GSSRESPONSE" 121 | ) 122 | clientInit_complete.assert_called_with( 123 | "HTTP@www.example.org", 124 | gssflags=( 125 | kerberos.GSS_C_MUTUAL_FLAG | 126 | kerberos.GSS_C_SEQUENCE_FLAG), 127 | principal=None) 128 | clientStep_continue.assert_called_with("CTX", "token") 129 | clientResponse.assert_called_with("CTX") 130 | 131 | def test_generate_request_header_init_error(self): 132 | with patch.multiple(kerberos_module_name, 133 | authGSSClientInit=clientInit_error, 134 | authGSSClientResponse=clientResponse, 135 | authGSSClientStep=clientStep_continue): 136 | response = requests.Response() 137 | response.url = "http://www.example.org/" 138 | response.headers = {'www-authenticate': 'negotiate token'} 139 | host = urlparse(response.url).hostname 140 | auth = requests_kerberos.HTTPKerberosAuth() 141 | self.assertRaises(requests_kerberos.exceptions.KerberosExchangeError, 142 | auth.generate_request_header, response, host 143 | ) 144 | clientInit_error.assert_called_with( 145 | "HTTP@www.example.org", 146 | gssflags=( 147 | kerberos.GSS_C_MUTUAL_FLAG | 148 | kerberos.GSS_C_SEQUENCE_FLAG), 149 | principal=None) 150 | self.assertFalse(clientStep_continue.called) 151 | self.assertFalse(clientResponse.called) 152 | 153 | def test_generate_request_header_step_error(self): 154 | with patch.multiple(kerberos_module_name, 155 | authGSSClientInit=clientInit_complete, 156 | authGSSClientResponse=clientResponse, 157 | authGSSClientStep=clientStep_error): 158 | response = requests.Response() 159 | response.url = "http://www.example.org/" 160 | response.headers = {'www-authenticate': 'negotiate token'} 161 | host = urlparse(response.url).hostname 162 | auth = requests_kerberos.HTTPKerberosAuth() 163 | self.assertRaises(requests_kerberos.exceptions.KerberosExchangeError, 164 | auth.generate_request_header, response, host 165 | ) 166 | clientInit_complete.assert_called_with( 167 | "HTTP@www.example.org", 168 | gssflags=( 169 | kerberos.GSS_C_MUTUAL_FLAG | 170 | kerberos.GSS_C_SEQUENCE_FLAG), 171 | principal=None) 172 | clientStep_error.assert_called_with("CTX", "token") 173 | self.assertFalse(clientResponse.called) 174 | 175 | def test_authenticate_user(self): 176 | with patch.multiple(kerberos_module_name, 177 | authGSSClientInit=clientInit_complete, 178 | authGSSClientResponse=clientResponse, 179 | authGSSClientStep=clientStep_continue): 180 | 181 | response_ok = requests.Response() 182 | response_ok.url = "http://www.example.org/" 183 | response_ok.status_code = 200 184 | response_ok.headers = {'www-authenticate': 'negotiate servertoken'} 185 | 186 | connection = Mock() 187 | connection.send = Mock(return_value=response_ok) 188 | 189 | raw = Mock() 190 | raw.release_conn = Mock(return_value=None) 191 | 192 | request = requests.Request() 193 | response = requests.Response() 194 | response.request = request 195 | response.url = "http://www.example.org/" 196 | response.headers = {'www-authenticate': 'negotiate token'} 197 | response.status_code = 401 198 | response.connection = connection 199 | response._content = "" 200 | response.raw = raw 201 | auth = requests_kerberos.HTTPKerberosAuth() 202 | r = auth.authenticate_user(response) 203 | 204 | self.assertTrue(response in r.history) 205 | self.assertEqual(r, response_ok) 206 | self.assertEqual( 207 | request.headers['Authorization'], 208 | 'Negotiate GSSRESPONSE') 209 | connection.send.assert_called_with(request) 210 | raw.release_conn.assert_called_with() 211 | clientInit_complete.assert_called_with( 212 | "HTTP@www.example.org", 213 | gssflags=( 214 | kerberos.GSS_C_MUTUAL_FLAG | 215 | kerberos.GSS_C_SEQUENCE_FLAG), 216 | principal=None) 217 | clientStep_continue.assert_called_with("CTX", "token") 218 | clientResponse.assert_called_with("CTX") 219 | 220 | def test_handle_401(self): 221 | with patch.multiple(kerberos_module_name, 222 | authGSSClientInit=clientInit_complete, 223 | authGSSClientResponse=clientResponse, 224 | authGSSClientStep=clientStep_continue): 225 | 226 | response_ok = requests.Response() 227 | response_ok.url = "http://www.example.org/" 228 | response_ok.status_code = 200 229 | response_ok.headers = {'www-authenticate': 'negotiate servertoken'} 230 | 231 | connection = Mock() 232 | connection.send = Mock(return_value=response_ok) 233 | 234 | raw = Mock() 235 | raw.release_conn = Mock(return_value=None) 236 | 237 | request = requests.Request() 238 | response = requests.Response() 239 | response.request = request 240 | response.url = "http://www.example.org/" 241 | response.headers = {'www-authenticate': 'negotiate token'} 242 | response.status_code = 401 243 | response.connection = connection 244 | response._content = "" 245 | response.raw = raw 246 | auth = requests_kerberos.HTTPKerberosAuth() 247 | r = auth.handle_401(response) 248 | 249 | self.assertTrue(response in r.history) 250 | self.assertEqual(r, response_ok) 251 | self.assertEqual( 252 | request.headers['Authorization'], 253 | 'Negotiate GSSRESPONSE') 254 | connection.send.assert_called_with(request) 255 | raw.release_conn.assert_called_with() 256 | clientInit_complete.assert_called_with( 257 | "HTTP@www.example.org", 258 | gssflags=( 259 | kerberos.GSS_C_MUTUAL_FLAG | 260 | kerberos.GSS_C_SEQUENCE_FLAG), 261 | principal=None) 262 | clientStep_continue.assert_called_with("CTX", "token") 263 | clientResponse.assert_called_with("CTX") 264 | 265 | def test_authenticate_server(self): 266 | with patch.multiple(kerberos_module_name, authGSSClientStep=clientStep_complete): 267 | 268 | response_ok = requests.Response() 269 | response_ok.url = "http://www.example.org/" 270 | response_ok.status_code = 200 271 | response_ok.headers = { 272 | 'www-authenticate': 'negotiate servertoken', 273 | 'authorization': 'Negotiate GSSRESPONSE'} 274 | 275 | auth = requests_kerberos.HTTPKerberosAuth() 276 | auth.context = {"www.example.org": "CTX"} 277 | result = auth.authenticate_server(response_ok) 278 | 279 | self.assertTrue(result) 280 | clientStep_complete.assert_called_with("CTX", "servertoken") 281 | 282 | def test_handle_other(self): 283 | with patch(kerberos_module_name+'.authGSSClientStep', clientStep_complete): 284 | 285 | response_ok = requests.Response() 286 | response_ok.url = "http://www.example.org/" 287 | response_ok.status_code = 200 288 | response_ok.headers = { 289 | 'www-authenticate': 'negotiate servertoken', 290 | 'authorization': 'Negotiate GSSRESPONSE'} 291 | 292 | auth = requests_kerberos.HTTPKerberosAuth() 293 | auth.context = {"www.example.org": "CTX"} 294 | 295 | r = auth.handle_other(response_ok) 296 | 297 | self.assertEqual(r, response_ok) 298 | clientStep_complete.assert_called_with("CTX", "servertoken") 299 | 300 | def test_handle_response_200(self): 301 | with patch(kerberos_module_name+'.authGSSClientStep', clientStep_complete): 302 | 303 | response_ok = requests.Response() 304 | response_ok.url = "http://www.example.org/" 305 | response_ok.status_code = 200 306 | response_ok.headers = { 307 | 'www-authenticate': 'negotiate servertoken', 308 | 'authorization': 'Negotiate GSSRESPONSE'} 309 | 310 | auth = requests_kerberos.HTTPKerberosAuth() 311 | auth.context = {"www.example.org": "CTX"} 312 | 313 | r = auth.handle_response(response_ok) 314 | 315 | self.assertEqual(r, response_ok) 316 | clientStep_complete.assert_called_with("CTX", "servertoken") 317 | 318 | def test_handle_response_200_mutual_auth_required_failure(self): 319 | with patch(kerberos_module_name+'.authGSSClientStep', clientStep_error): 320 | 321 | response_ok = requests.Response() 322 | response_ok.url = "http://www.example.org/" 323 | response_ok.status_code = 200 324 | response_ok.headers = {} 325 | 326 | auth = requests_kerberos.HTTPKerberosAuth() 327 | auth.context = {"www.example.org": "CTX"} 328 | 329 | self.assertRaises(requests_kerberos.MutualAuthenticationError, 330 | auth.handle_response, 331 | response_ok) 332 | 333 | self.assertFalse(clientStep_error.called) 334 | 335 | def test_handle_response_200_mutual_auth_required_failure_2(self): 336 | with patch(kerberos_module_name+'.authGSSClientStep', clientStep_exception): 337 | 338 | response_ok = requests.Response() 339 | response_ok.url = "http://www.example.org/" 340 | response_ok.status_code = 200 341 | response_ok.headers = { 342 | 'www-authenticate': 'negotiate servertoken', 343 | 'authorization': 'Negotiate GSSRESPONSE'} 344 | 345 | auth = requests_kerberos.HTTPKerberosAuth() 346 | auth.context = {"www.example.org": "CTX"} 347 | 348 | self.assertRaises(requests_kerberos.MutualAuthenticationError, 349 | auth.handle_response, 350 | response_ok) 351 | 352 | clientStep_exception.assert_called_with("CTX", "servertoken") 353 | 354 | def test_handle_response_200_mutual_auth_optional_hard_failure(self): 355 | with patch(kerberos_module_name+'.authGSSClientStep', clientStep_error): 356 | 357 | response_ok = requests.Response() 358 | response_ok.url = "http://www.example.org/" 359 | response_ok.status_code = 200 360 | response_ok.headers = { 361 | 'www-authenticate': 'negotiate servertoken', 362 | 'authorization': 'Negotiate GSSRESPONSE'} 363 | 364 | auth = requests_kerberos.HTTPKerberosAuth( 365 | requests_kerberos.OPTIONAL) 366 | auth.context = {"www.example.org": "CTX"} 367 | 368 | self.assertRaises(requests_kerberos.MutualAuthenticationError, 369 | auth.handle_response, 370 | response_ok) 371 | 372 | clientStep_error.assert_called_with("CTX", "servertoken") 373 | 374 | def test_handle_response_200_mutual_auth_optional_soft_failure(self): 375 | with patch(kerberos_module_name+'.authGSSClientStep', clientStep_error): 376 | 377 | response_ok = requests.Response() 378 | response_ok.url = "http://www.example.org/" 379 | response_ok.status_code = 200 380 | 381 | auth = requests_kerberos.HTTPKerberosAuth( 382 | requests_kerberos.OPTIONAL) 383 | auth.context = {"www.example.org": "CTX"} 384 | 385 | r = auth.handle_response(response_ok) 386 | 387 | self.assertEqual(r, response_ok) 388 | 389 | self.assertFalse(clientStep_error.called) 390 | 391 | def test_handle_response_500_mutual_auth_required_failure(self): 392 | with patch(kerberos_module_name+'.authGSSClientStep', clientStep_error): 393 | 394 | response_500 = requests.Response() 395 | response_500.url = "http://www.example.org/" 396 | response_500.status_code = 500 397 | response_500.headers = {} 398 | response_500.request = "REQUEST" 399 | response_500.connection = "CONNECTION" 400 | response_500._content = "CONTENT" 401 | response_500.encoding = "ENCODING" 402 | response_500.raw = "RAW" 403 | response_500.cookies = "COOKIES" 404 | 405 | auth = requests_kerberos.HTTPKerberosAuth() 406 | auth.context = {"www.example.org": "CTX"} 407 | 408 | r = auth.handle_response(response_500) 409 | 410 | self.assertTrue(isinstance(r, requests_kerberos.kerberos_.SanitizedResponse)) 411 | self.assertNotEqual(r, response_500) 412 | self.assertNotEqual(r.headers, response_500.headers) 413 | self.assertEqual(r.status_code, response_500.status_code) 414 | self.assertEqual(r.encoding, response_500.encoding) 415 | self.assertEqual(r.raw, response_500.raw) 416 | self.assertEqual(r.url, response_500.url) 417 | self.assertEqual(r.reason, response_500.reason) 418 | self.assertEqual(r.connection, response_500.connection) 419 | self.assertEqual(r.content, '') 420 | self.assertNotEqual(r.cookies, response_500.cookies) 421 | 422 | self.assertFalse(clientStep_error.called) 423 | 424 | # re-test with error response sanitizing disabled 425 | auth = requests_kerberos.HTTPKerberosAuth(sanitize_mutual_error_response=False) 426 | auth.context = {"www.example.org": "CTX"} 427 | 428 | r = auth.handle_response(response_500) 429 | 430 | self.assertFalse(isinstance(r, requests_kerberos.kerberos_.SanitizedResponse)) 431 | 432 | def test_handle_response_500_mutual_auth_optional_failure(self): 433 | with patch(kerberos_module_name+'.authGSSClientStep', clientStep_error): 434 | 435 | response_500 = requests.Response() 436 | response_500.url = "http://www.example.org/" 437 | response_500.status_code = 500 438 | response_500.headers = {} 439 | response_500.request = "REQUEST" 440 | response_500.connection = "CONNECTION" 441 | response_500._content = "CONTENT" 442 | response_500.encoding = "ENCODING" 443 | response_500.raw = "RAW" 444 | response_500.cookies = "COOKIES" 445 | 446 | auth = requests_kerberos.HTTPKerberosAuth( 447 | requests_kerberos.OPTIONAL) 448 | auth.context = {"www.example.org": "CTX"} 449 | 450 | r = auth.handle_response(response_500) 451 | 452 | self.assertEqual(r, response_500) 453 | 454 | self.assertFalse(clientStep_error.called) 455 | 456 | def test_handle_response_401(self): 457 | # Get a 401 from server, authenticate, and get a 200 back. 458 | with patch.multiple(kerberos_module_name, 459 | authGSSClientInit=clientInit_complete, 460 | authGSSClientResponse=clientResponse, 461 | authGSSClientStep=clientStep_continue): 462 | 463 | response_ok = requests.Response() 464 | response_ok.url = "http://www.example.org/" 465 | response_ok.status_code = 200 466 | response_ok.headers = {'www-authenticate': 'negotiate servertoken'} 467 | 468 | connection = Mock() 469 | connection.send = Mock(return_value=response_ok) 470 | 471 | raw = Mock() 472 | raw.release_conn = Mock(return_value=None) 473 | 474 | request = requests.Request() 475 | response = requests.Response() 476 | response.request = request 477 | response.url = "http://www.example.org/" 478 | response.headers = {'www-authenticate': 'negotiate token'} 479 | response.status_code = 401 480 | response.connection = connection 481 | response._content = "" 482 | response.raw = raw 483 | 484 | auth = requests_kerberos.HTTPKerberosAuth() 485 | auth.handle_other = Mock(return_value=response_ok) 486 | 487 | r = auth.handle_response(response) 488 | 489 | self.assertTrue(response in r.history) 490 | auth.handle_other.assert_called_once_with(response_ok) 491 | self.assertEqual(r, response_ok) 492 | self.assertEqual( 493 | request.headers['Authorization'], 494 | 'Negotiate GSSRESPONSE') 495 | connection.send.assert_called_with(request) 496 | raw.release_conn.assert_called_with() 497 | clientInit_complete.assert_called_with( 498 | "HTTP@www.example.org", 499 | gssflags=( 500 | kerberos.GSS_C_MUTUAL_FLAG | 501 | kerberos.GSS_C_SEQUENCE_FLAG), 502 | principal=None) 503 | clientStep_continue.assert_called_with("CTX", "token") 504 | clientResponse.assert_called_with("CTX") 505 | 506 | def test_handle_response_401_rejected(self): 507 | # Get a 401 from server, authenticate, and get another 401 back. 508 | # Ensure there is no infinite recursion. 509 | with patch.multiple(kerberos_module_name, 510 | authGSSClientInit=clientInit_complete, 511 | authGSSClientResponse=clientResponse, 512 | authGSSClientStep=clientStep_continue): 513 | 514 | connection = Mock() 515 | 516 | def connection_send(self, *args, **kwargs): 517 | reject = requests.Response() 518 | reject.url = "http://www.example.org/" 519 | reject.status_code = 401 520 | reject.connection = connection 521 | return reject 522 | 523 | connection.send.side_effect = connection_send 524 | 525 | raw = Mock() 526 | raw.release_conn.return_value = None 527 | 528 | request = requests.Request() 529 | response = requests.Response() 530 | response.request = request 531 | response.url = "http://www.example.org/" 532 | response.headers = {'www-authenticate': 'negotiate token'} 533 | response.status_code = 401 534 | response.connection = connection 535 | response._content = "" 536 | response.raw = raw 537 | 538 | auth = requests_kerberos.HTTPKerberosAuth() 539 | 540 | r = auth.handle_response(response) 541 | 542 | self.assertEqual(r.status_code, 401) 543 | self.assertEqual(request.headers['Authorization'], 544 | 'Negotiate GSSRESPONSE') 545 | connection.send.assert_called_with(request) 546 | raw.release_conn.assert_called_with() 547 | clientInit_complete.assert_called_with( 548 | "HTTP@www.example.org", 549 | gssflags=( 550 | kerberos.GSS_C_MUTUAL_FLAG | 551 | kerberos.GSS_C_SEQUENCE_FLAG), 552 | principal=None) 553 | clientStep_continue.assert_called_with("CTX", "token") 554 | clientResponse.assert_called_with("CTX") 555 | 556 | def test_generate_request_header_custom_service(self): 557 | with patch.multiple(kerberos_module_name, 558 | authGSSClientInit=clientInit_complete, 559 | authGSSClientResponse=clientResponse, 560 | authGSSClientStep=clientStep_continue): 561 | response = requests.Response() 562 | response.url = "http://www.example.org/" 563 | response.headers = {'www-authenticate': 'negotiate token'} 564 | host = urlparse(response.url).hostname 565 | auth = requests_kerberos.HTTPKerberosAuth(service="barfoo") 566 | auth.generate_request_header(response, host), 567 | clientInit_complete.assert_called_with( 568 | "barfoo@www.example.org", 569 | gssflags=( 570 | kerberos.GSS_C_MUTUAL_FLAG | 571 | kerberos.GSS_C_SEQUENCE_FLAG), 572 | principal=None) 573 | 574 | def test_delegation(self): 575 | with patch.multiple(kerberos_module_name, 576 | authGSSClientInit=clientInit_complete, 577 | authGSSClientResponse=clientResponse, 578 | authGSSClientStep=clientStep_continue): 579 | 580 | response_ok = requests.Response() 581 | response_ok.url = "http://www.example.org/" 582 | response_ok.status_code = 200 583 | response_ok.headers = {'www-authenticate': 'negotiate servertoken'} 584 | 585 | connection = Mock() 586 | connection.send = Mock(return_value=response_ok) 587 | 588 | raw = Mock() 589 | raw.release_conn = Mock(return_value=None) 590 | 591 | request = requests.Request() 592 | response = requests.Response() 593 | response.request = request 594 | response.url = "http://www.example.org/" 595 | response.headers = {'www-authenticate': 'negotiate token'} 596 | response.status_code = 401 597 | response.connection = connection 598 | response._content = "" 599 | response.raw = raw 600 | auth = requests_kerberos.HTTPKerberosAuth(1, "HTTP", True) 601 | r = auth.authenticate_user(response) 602 | 603 | self.assertTrue(response in r.history) 604 | self.assertEqual(r, response_ok) 605 | self.assertEqual( 606 | request.headers['Authorization'], 607 | 'Negotiate GSSRESPONSE') 608 | connection.send.assert_called_with(request) 609 | raw.release_conn.assert_called_with() 610 | clientInit_complete.assert_called_with( 611 | "HTTP@www.example.org", 612 | gssflags=( 613 | kerberos.GSS_C_MUTUAL_FLAG | 614 | kerberos.GSS_C_SEQUENCE_FLAG | 615 | kerberos.GSS_C_DELEG_FLAG), 616 | principal=None 617 | ) 618 | clientStep_continue.assert_called_with("CTX", "token") 619 | clientResponse.assert_called_with("CTX") 620 | 621 | def test_principal_override(self): 622 | with patch.multiple(kerberos_module_name, 623 | authGSSClientInit=clientInit_complete, 624 | authGSSClientResponse=clientResponse, 625 | authGSSClientStep=clientStep_continue): 626 | response = requests.Response() 627 | response.url = "http://www.example.org/" 628 | response.headers = {'www-authenticate': 'negotiate token'} 629 | host = urlparse(response.url).hostname 630 | auth = requests_kerberos.HTTPKerberosAuth(principal="user@REALM") 631 | auth.generate_request_header(response, host) 632 | clientInit_complete.assert_called_with( 633 | "HTTP@www.example.org", 634 | gssflags=( 635 | kerberos.GSS_C_MUTUAL_FLAG | 636 | kerberos.GSS_C_SEQUENCE_FLAG), 637 | principal="user@REALM") 638 | 639 | def test_realm_override(self): 640 | with patch.multiple(kerberos_module_name, 641 | authGSSClientInit=clientInit_complete, 642 | authGSSClientResponse=clientResponse, 643 | authGSSClientStep=clientStep_continue): 644 | response = requests.Response() 645 | response.url = "http://www.example.org/" 646 | response.headers = {'www-authenticate': 'negotiate token'} 647 | host = urlparse(response.url).hostname 648 | auth = requests_kerberos.HTTPKerberosAuth(hostname_override="otherhost.otherdomain.org") 649 | auth.generate_request_header(response, host) 650 | clientInit_complete.assert_called_with( 651 | "HTTP@otherhost.otherdomain.org", 652 | gssflags=( 653 | kerberos.GSS_C_MUTUAL_FLAG | 654 | kerberos.GSS_C_SEQUENCE_FLAG), 655 | principal=None) 656 | 657 | def test_realm_via_fqdn(self): 658 | with patch.multiple(kerberos_module_name, 659 | authGSSClientInit=clientInit_complete, 660 | authGSSClientResponse=clientResponse, 661 | authGSSClientStep=clientStep_continue): 662 | 663 | def get_host_mock(host): 664 | return 'otherhost.otherdomain.org' if host == 'www.example.org' else socket.getfqdn(host) 665 | 666 | with patch.multiple('requests_kerberos.kerberos_', _get_default_kerb_host=get_host_mock): 667 | response = requests.Response() 668 | response.url = "http://www.example.org/" 669 | response.headers = {'www-authenticate': 'negotiate token'} 670 | host = urlparse(response.url).hostname 671 | auth = requests_kerberos.HTTPKerberosAuth(canonicalize_hostname=True) 672 | auth.generate_request_header(response, host) 673 | clientInit_complete.assert_called_with( 674 | "HTTP@otherhost.otherdomain.org", 675 | gssflags=( 676 | kerberos.GSS_C_MUTUAL_FLAG | 677 | kerberos.GSS_C_SEQUENCE_FLAG), 678 | principal=None) 679 | 680 | 681 | class TestCertificateHash(unittest.TestCase): 682 | 683 | def test_rsa_md5(self): 684 | cert_der = b'MIIDGzCCAgOgAwIBAgIQJzshhViMG5hLHIJHxa+TcTANBgkqhkiG9w0' \ 685 | b'BAQQFADAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA4MD' \ 686 | b'MxNloXDTE4MDUzMDA4MjMxNlowFTETMBEGA1UEAwwKU0VSVkVSMjAxN' \ 687 | b'jCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN9N5GAzI7uq' \ 688 | b'AVlI6vUqhY5+EZWCWWGRwR3FT2DEXE5++AiJxXO0i0ZfAkLu7UggtBe' \ 689 | b'QwVNkaPD27EYzVUhy1iDo37BrFcLNpfjsjj8wVjaSmQmqvLvrvEh/BT' \ 690 | b'C5SBgDrk2+hiMh9PrpJoB3QAMDinz5aW0rEXMKitPBBiADrczyYrliF' \ 691 | b'AlEU6pTlKEKDUAeP7dKOBlDbCYvBxKnR3ddVH74I5T2SmNBq5gzkbKP' \ 692 | b'nlCXdHLZSh74USu93rKDZQF8YzdTO5dcBreJDJsntyj1o49w9WCt6M7' \ 693 | b'+pg6vKvE+tRbpCm7kXq5B9PDi42Nb6//MzNaMYf9V7v5MHapvVSv3+y' \ 694 | b'sCAwEAAaNnMGUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGA' \ 695 | b'QUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggpTRVJWRVIyMDE2MB0G' \ 696 | b'A1UdDgQWBBTh4L2Clr9ber6yfY3JFS3wiECL4DANBgkqhkiG9w0BAQQ' \ 697 | b'FAAOCAQEA0JK/SL7SP9/nvqWp52vnsxVefTFehThle5DLzagmms/9gu' \ 698 | b'oSE2I9XkQIttFMprPosaIZWt7WP42uGcZmoZOzU8kFFYJMfg9Ovyca+' \ 699 | b'gnG28jDUMF1E74KrC7uynJiQJ4vPy8ne7F3XJ592LsNJmK577l42gAW' \ 700 | b'u08p3TvEJFNHy2dBk/IwZp0HIPr9+JcPf7v0uL6lK930xHJHP56XLzN' \ 701 | b'YG8vCMpJFR7wVZp3rXkJQUy3GxyHPJPjS8S43I9j+PoyioWIMEotq2+' \ 702 | b'q0IpXU/KeNFkdGV6VPCmzhykijExOMwO6doUzIUM8orv9jYLHXYC+i6' \ 703 | b'IFKSb6runxF1MAik+GCSA==' 704 | 705 | expected_hash = b'\x23\x34\xB8\x47\x6C\xBF\x4E\x6D\xFC\x76\x6A\x5D' \ 706 | b'\x5A\x30\xD6\x64\x9C\x01\xBA\xE1\x66\x2A\x5C\x3A' \ 707 | b'\x13\x02\xA9\x68\xD7\xC6\xB0\xF6' 708 | actual_hash = _get_certificate_hash(base64.b64decode(cert_der)) 709 | assert actual_hash == expected_hash 710 | 711 | def test_rsa_sha1(self): 712 | cert_der = b'MIIDGzCCAgOgAwIBAgIQJg/Mf5sR55xApJRK+kabbTANBgkqhkiG9w0' \ 713 | b'BAQUFADAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA4MD' \ 714 | b'MxNloXDTE4MDUzMDA4MjMxNlowFTETMBEGA1UEAwwKU0VSVkVSMjAxN' \ 715 | b'jCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALPKwYikjbzL' \ 716 | b'Lo6JtS6cyytdMMjSrggDoTnRUKauC5/izoYJd+2YVR5YqnluBJZpoFp' \ 717 | b'hkCgFFohUOU7qUsI1SkuGnjI8RmWTrrDsSy62BrfX+AXkoPlXo6IpHz' \ 718 | b'HaEPxjHJdUACpn8QVWTPmdAhwTwQkeUutrm3EOVnKPX4bafNYeAyj7/' \ 719 | b'AGEplgibuXT4/ehbzGKOkRN3ds/pZuf0xc4Q2+gtXn20tQIUt7t6iwh' \ 720 | b'nEWjIgopFL/hX/r5q5MpF6stc1XgIwJjEzqMp76w/HUQVqaYneU4qSG' \ 721 | b'f90ANK/TQ3aDbUNtMC/ULtIfHqHIW4POuBYXaWBsqalJL2VL3YYkKTU' \ 722 | b'sCAwEAAaNnMGUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGA' \ 723 | b'QUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggpTRVJWRVIyMDE2MB0G' \ 724 | b'A1UdDgQWBBS1jgojcjPu9vqeP1uSKuiIonGwAjANBgkqhkiG9w0BAQU' \ 725 | b'FAAOCAQEAKjHL6k5Dv/Zb7dvbYEZyx0wVhjHkCTpT3xstI3+TjfAFsu' \ 726 | b'3zMmyFqFqzmr4pWZ/rHc3ObD4pEa24kP9hfB8nmr8oHMLebGmvkzh5h' \ 727 | b'0GYc4dIH7Ky1yfQN51hi7/X5iN7jnnBoCJTTlgeBVYDOEBXhfXi3cLT' \ 728 | b'u3d7nz2heyNq07gFP8iN7MfqdPZndVDYY82imLgsgar9w5d+fvnYM+k' \ 729 | b'XWItNNCUH18M26Obp4Es/Qogo/E70uqkMHost2D+tww/7woXi36X3w/' \ 730 | b'D2yBDyrJMJKZLmDgfpNIeCimncTOzi2IhzqJiOY/4XPsVN/Xqv0/dzG' \ 731 | b'TDdI11kPLq4EiwxvPanCg==' 732 | 733 | expected_hash = b'\x14\xCF\xE8\xE4\xB3\x32\xB2\x0A\x34\x3F\xC8\x40' \ 734 | b'\xB1\x8F\x9F\x6F\x78\x92\x6A\xFE\x7E\xC3\xE7\xB8' \ 735 | b'\xE2\x89\x69\x61\x9B\x1E\x8F\x3E' 736 | actual_hash = _get_certificate_hash(base64.b64decode(cert_der)) 737 | assert actual_hash == expected_hash 738 | 739 | def test_rsa_sha256(self): 740 | cert_der = b'MIIDGzCCAgOgAwIBAgIQWkeAtqoFg6pNWF7xC4YXhTANBgkqhkiG9w0' \ 741 | b'BAQsFADAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUyNzA5MD' \ 742 | b'I0NFoXDTE4MDUyNzA5MjI0NFowFTETMBEGA1UEAwwKU0VSVkVSMjAxN' \ 743 | b'jCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALIPKM5uykFy' \ 744 | b'NmVoLyvPSXGk15ZDqjYi3AbUxVFwCkVImqhefLATit3PkTUYFtAT+TC' \ 745 | b'AwK2E4lOu1XHM+Tmp2KIOnq2oUR8qMEvfxYThEf1MHxkctFljFssZ9N' \ 746 | b'vASDD4lzw8r0Bhl+E5PhR22Eu1Wago5bvIldojkwG+WBxPQv3ZR546L' \ 747 | b'MUZNaBXC0RhuGj5w83lbVz75qM98wvv1ekfZYAP7lrVyHxqCTPDomEU' \ 748 | b'I45tQQZHCZl5nRx1fPCyyYfcfqvFlLWD4Q3PZAbnw6mi0MiWJbGYKME' \ 749 | b'1XGicjqyn/zM9XKA1t/JzChS2bxf6rsyA9I7ibdRHUxsm1JgKry2jfW' \ 750 | b'0CAwEAAaNnMGUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGA' \ 751 | b'QUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggpTRVJWRVIyMDE2MB0G' \ 752 | b'A1UdDgQWBBQabLGWg1sn7AXPwYPyfE0ER921ZDANBgkqhkiG9w0BAQs' \ 753 | b'FAAOCAQEAnRohyl6ZmOsTWCtxOJx5A8yr//NweXKwWWmFQXRmCb4bMC' \ 754 | b'xhD4zqLDf5P6RotGV0I/SHvqz+pAtJuwmr+iyAF6WTzo3164LCfnQEu' \ 755 | b'psfrrfMkf3txgDwQkA0oPAw3HEwOnR+tzprw3Yg9x6UoZEhi4XqP9AX' \ 756 | b'R49jU92KrNXJcPlz5MbkzNo5t9nr2f8q39b5HBjaiBJxzdM1hxqsbfD' \ 757 | b'KirTYbkUgPlVOo/NDmopPPb8IX8ubj/XETZG2jixD0zahgcZ1vdr/iZ' \ 758 | b'+50WSXKN2TAKBO2fwoK+2/zIWrGRxJTARfQdF+fGKuj+AERIFNh88HW' \ 759 | b'xSDYjHQAaFMcfdUpa9GGQ==' 760 | 761 | expected_hash = b'\x99\x6F\x3E\xEA\x81\x2C\x18\x70\xE3\x05\x49\xFF' \ 762 | b'\x9B\x86\xCD\x87\xA8\x90\xB6\xD8\xDF\xDF\x4A\x81' \ 763 | b'\xBE\xF9\x67\x59\x70\xDA\xDB\x26' 764 | actual_hash = _get_certificate_hash(base64.b64decode(cert_der)) 765 | assert actual_hash == expected_hash 766 | 767 | def test_rsa_sha384(self): 768 | cert_der = b'MIIDGzCCAgOgAwIBAgIQEmj1prSSQYRL2zYBEjsm5jANBgkqhkiG9w0' \ 769 | b'BAQwFADAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA4MD' \ 770 | b'MxN1oXDTE4MDUzMDA4MjMxN1owFTETMBEGA1UEAwwKU0VSVkVSMjAxN' \ 771 | b'jCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKsK5NvHi4xO' \ 772 | b'081fRLMmPqKsKaHvXgPRykLA0SmKxpGJHfTAZzxojHVeVwOm87IvQj2' \ 773 | b'JUh/yrRwSi5Oqrvqx29l2IC/qQt2xkAQsO51/EWkMQ5OSJsl1MN3NXW' \ 774 | b'eRTKVoUuJzBs8XLmeraxQcBPyyLhq+WpMl/Q4ZDn1FrUEZfxV0POXgU' \ 775 | b'dI3ApuQNRtJOb6iteBIoQyMlnof0RswBUnkiWCA/+/nzR0j33j47IfL' \ 776 | b'nkmU4RtqkBlO13f6+e1GZ4lEcQVI2yZq4Zgu5VVGAFU2lQZ3aEVMTu9' \ 777 | b'8HEqD6heyNp2on5G/K/DCrGWYCBiASjnX3wiSz0BYv8f3HhCgIyVKhJ' \ 778 | b'8CAwEAAaNnMGUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGA' \ 779 | b'QUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggpTRVJWRVIyMDE2MB0G' \ 780 | b'A1UdDgQWBBQS/SI61S2UE8xwSgHxbkCTpZXo4TANBgkqhkiG9w0BAQw' \ 781 | b'FAAOCAQEAMVV/WMXd9w4jtDfSrIsKaWKGtHtiMPpAJibXmSakBRwLOn' \ 782 | b'5ZGXL2bWI/Ac2J2Y7bSzs1im2ifwmEqwzzqnpVKShIkZmtij0LS0SEr' \ 783 | b'6Fw5IrK8tD6SH+lMMXUTvp4/lLQlgRCwOWxry/YhQSnuprx8IfSPvil' \ 784 | b'kwZ0Ysim4Aa+X5ojlhHpWB53edX+lFrmR1YWValBnQ5DvnDyFyLR6II' \ 785 | b'Ialp4vmkzI9e3/eOgSArksizAhpXpC9dxQBiHXdhredN0X+1BVzbgzV' \ 786 | b'hQBEwgnAIPa+B68oDILaV0V8hvxrP6jFM4IrKoGS1cq0B+Ns0zkG7ZA' \ 787 | b'2Q0W+3nVwSxIr6bd6hw7g==' 788 | 789 | expected_hash = b'\x34\xF3\x03\xC9\x95\x28\x6F\x4B\x21\x4A\x9B\xA6' \ 790 | b'\x43\x5B\x69\xB5\x1E\xCF\x37\x58\xEA\xBC\x2A\x14' \ 791 | b'\xD7\xA4\x3F\xD2\x37\xDC\x2B\x1A\x1A\xD9\x11\x1C' \ 792 | b'\x5C\x96\x5E\x10\x75\x07\xCB\x41\x98\xC0\x9F\xEC' 793 | actual_hash = _get_certificate_hash(base64.b64decode(cert_der)) 794 | assert actual_hash == expected_hash 795 | 796 | def test_rsa_sha512(self): 797 | cert_der = b'MIIDGzCCAgOgAwIBAgIQUDHcKGevZohJV+TkIIYC1DANBgkqhkiG9w0' \ 798 | b'BAQ0FADAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA4MD' \ 799 | b'MxN1oXDTE4MDUzMDA4MjMxN1owFTETMBEGA1UEAwwKU0VSVkVSMjAxN' \ 800 | b'jCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKr9bo/XXvHt' \ 801 | b'D6Qnhb1wyLg9lDQxxe/enH49LQihtVTZMwGf2010h81QrRUe/bkHTvw' \ 802 | b'K22s2lqj3fUpGxtEbYFWLAHxv6IFnIKd+Zi1zaCPGfas9ekqCSj3vZQ' \ 803 | b'j7lCJVGUGuuqnSDvsed6g2Pz/g6mJUa+TzjxN+8wU5oj5YVUK+aing1' \ 804 | b'zPSA2MDCfx3+YzjxVwNoGixOz6Yx9ijT4pUsAYQAf1o9R+6W1/IpGgu' \ 805 | b'oax714QILT9heqIowwlHzlUZc1UAYs0/JA4CbDZaw9hlJyzMqe/aE46' \ 806 | b'efqPDOpO3vCpOSRcSyzh02WijPvEEaPejQRWg8RX93othZ615MT7dqp' \ 807 | b'ECAwEAAaNnMGUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGA' \ 808 | b'QUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggpTRVJWRVIyMDE2MB0G' \ 809 | b'A1UdDgQWBBTgod3R6vejt6kOASAApA19xIG6kTANBgkqhkiG9w0BAQ0' \ 810 | b'FAAOCAQEAVfz0okK2bh3OQE8cWNbJ5PjJRSAJEqVUvYaTlS0Nqkyuaj' \ 811 | b'gicP3hb/pF8FvaVaB6r7LqgBxyW5NNL1xwdNLt60M2zaULL6Fhm1vzM' \ 812 | b'sSMc2ynkyN4++ODwii674YcQAnkUh+ZGIx+CTdZBWJfVM9dZb7QjgBT' \ 813 | b'nVukeFwN2EOOBSpiQSBpcoeJEEAq9csDVRhEfcB8Wtz7TTItgOVsilY' \ 814 | b'dQY56ON5XszjCki6UA3GwdQbBEHjWF2WERqXWrojrSSNOYDvxM5mrEx' \ 815 | b'sG1npzUTsaIr9w8ty1beh/2aToCMREvpiPFOXnVV/ovHMU1lFQTNeQ0' \ 816 | b'OI7elR0nJ0peai30eMpQQ==' 817 | 818 | expected_hash = b'\x55\x6E\x1C\x17\x84\xE3\xB9\x57\x37\x0B\x7F\x54' \ 819 | b'\x4F\x62\xC5\x33\xCB\x2C\xA5\xC1\xDA\xE0\x70\x6F' \ 820 | b'\xAE\xF0\x05\x44\xE1\xAD\x2B\x76\xFF\x25\xCF\xBE' \ 821 | b'\x69\xB1\xC4\xE6\x30\xC3\xBB\x02\x07\xDF\x11\x31' \ 822 | b'\x4C\x67\x38\xBC\xAE\xD7\xE0\x71\xD7\xBF\xBF\x2C' \ 823 | b'\x9D\xFA\xB8\x5D' 824 | actual_hash = _get_certificate_hash(base64.b64decode(cert_der)) 825 | assert actual_hash == expected_hash 826 | 827 | def test_ecdsa_sha1(self): 828 | cert_der = b'MIIBjjCCATSgAwIBAgIQRCJw7nbtvJ5F8wikRmwgizAJBgcqhkjOPQQ' \ 829 | b'BMBUxEzARBgNVBAMMClNFUlZFUjIwMTYwHhcNMTcwNTMwMDgwMzE3Wh' \ 830 | b'cNMTgwNTMwMDgyMzE3WjAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MFkwE' \ 831 | b'wYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEk3fOh178kRglmnPKe9K/mbgi' \ 832 | b'gf8YgNq62rF2EpfzpyQY0eGw4xnmKDG73aZ+ATSlV2IybxiUVsKyMUn' \ 833 | b'LhPfvmaNnMGUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQ' \ 834 | b'UFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggpTRVJWRVIyMDE2MB0GA' \ 835 | b'1UdDgQWBBQSK8qwmiQmyAWWya3FxQDj9wqQAzAJBgcqhkjOPQQBA0kA' \ 836 | b'MEYCIQCiOsP56Iqo+cHRvCp2toj65Mgxo/PQY1tn+S3WH4RJFQIhAJe' \ 837 | b'gGQuaPWg6aCWV+2+6pNCNMdg/Nix+mMOJ88qCBNHi' 838 | 839 | expected_hash = b'\x1E\xC9\xAD\x46\xDE\xE9\x34\x0E\x45\x03\xCF\xFD' \ 840 | b'\xB5\xCD\x81\x0C\xB2\x6B\x77\x8F\x46\xBE\x95\xD5' \ 841 | b'\xEA\xF9\x99\xDC\xB1\xC4\x5E\xDA' 842 | actual_hash = _get_certificate_hash(base64.b64decode(cert_der)) 843 | assert actual_hash == expected_hash 844 | 845 | def test_ecdsa_sha256(self): 846 | cert_der = b'MIIBjzCCATWgAwIBAgIQeNQTxkMgq4BF9tKogIGXUTAKBggqhkjOPQQ' \ 847 | b'DAjAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA4MDMxN1' \ 848 | b'oXDTE4MDUzMDA4MjMxN1owFTETMBEGA1UEAwwKU0VSVkVSMjAxNjBZM' \ 849 | b'BMGByqGSM49AgEGCCqGSM49AwEHA0IABDAfXTLOaC3ElgErlgk2tBlM' \ 850 | b'wf9XmGlGBw4vBtMJap1hAqbsdxFm6rhK3QU8PFFpv8Z/AtRG7ba3UwQ' \ 851 | b'prkssClejZzBlMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBg' \ 852 | b'EFBQcDAgYIKwYBBQUHAwEwFQYDVR0RBA4wDIIKU0VSVkVSMjAxNjAdB' \ 853 | b'gNVHQ4EFgQUnFDE8824TYAiBeX4fghEEg33UgYwCgYIKoZIzj0EAwID' \ 854 | b'SAAwRQIhAK3rXA4/0i6nm/U7bi6y618Ci2Is8++M3tYIXnEsA7zSAiA' \ 855 | b'w2s6bJoI+D7Xaey0Hp0gkks9z55y976keIEI+n3qkzw==' 856 | 857 | expected_hash = b'\xFE\xCF\x1B\x25\x85\x44\x99\x90\xD9\xE3\xB2\xC9' \ 858 | b'\x2D\x3F\x59\x7E\xC8\x35\x4E\x12\x4E\xDA\x75\x1D' \ 859 | b'\x94\x83\x7C\x2C\x89\xA2\xC1\x55' 860 | actual_hash = _get_certificate_hash(base64.b64decode(cert_der)) 861 | assert actual_hash == expected_hash 862 | 863 | def test_ecdsa_sha384(self): 864 | cert_der = b'MIIBjzCCATWgAwIBAgIQcO3/jALdQ6BOAoaoseLSCjAKBggqhkjOPQQ' \ 865 | b'DAzAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA4MDMxOF' \ 866 | b'oXDTE4MDUzMDA4MjMxOFowFTETMBEGA1UEAwwKU0VSVkVSMjAxNjBZM' \ 867 | b'BMGByqGSM49AgEGCCqGSM49AwEHA0IABJLjZH274heB/8PhmhWWCIVQ' \ 868 | b'Wle1hBZEN3Tk2yWSKaz9pz1bjwb9t79lVpQE9tvGL0zP9AqJYHcVOO9' \ 869 | b'YG9trqfejZzBlMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBg' \ 870 | b'EFBQcDAgYIKwYBBQUHAwEwFQYDVR0RBA4wDIIKU0VSVkVSMjAxNjAdB' \ 871 | b'gNVHQ4EFgQUkRajoFr8qZ/8L8rKB3zGiGolDygwCgYIKoZIzj0EAwMD' \ 872 | b'SAAwRQIgfi8dAxXljCMSvngtDtagGCTGBs7Xxh8Z3WX6ZwJZsHYCIQC' \ 873 | b'D4iNReh1afXKYC0ipjXWAIkiihnEEycCIQMbkMNst7A==' 874 | 875 | expected_hash = b'\xD2\x98\x7A\xD8\xF2\x0E\x83\x16\xA8\x31\x26\x1B' \ 876 | b'\x74\xEF\x7B\x3E\x55\x15\x5D\x09\x22\xE0\x7F\xFE' \ 877 | b'\x54\x62\x08\x06\x98\x2B\x68\xA7\x3A\x5E\x3C\x47' \ 878 | b'\x8B\xAA\x5E\x77\x14\x13\x5C\xB2\x6D\x98\x07\x49' 879 | actual_hash = _get_certificate_hash(base64.b64decode(cert_der)) 880 | assert actual_hash == expected_hash 881 | 882 | def test_ecdsa_sha512(self): 883 | cert_der = b'MIIBjjCCATWgAwIBAgIQHVj2AGEwd6pOOSbcf0skQDAKBggqhkjOPQQ' \ 884 | b'DBDAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA3NTUzOV' \ 885 | b'oXDTE4MDUzMDA4MTUzOVowFTETMBEGA1UEAwwKU0VSVkVSMjAxNjBZM' \ 886 | b'BMGByqGSM49AgEGCCqGSM49AwEHA0IABL8d9S++MFpfzeH8B3vG/PjA' \ 887 | b'AWg8tGJVgsMw9nR+OfC9ltbTUwhB+yPk3JPcfW/bqsyeUgq4//LhaSp' \ 888 | b'lOWFNaNqjZzBlMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBg' \ 889 | b'EFBQcDAgYIKwYBBQUHAwEwFQYDVR0RBA4wDIIKU0VSVkVSMjAxNjAdB' \ 890 | b'gNVHQ4EFgQUKUkCgLlxoeai0EtQrZth1/BSc5kwCgYIKoZIzj0EAwQD' \ 891 | b'RwAwRAIgRrV7CLpDG7KueyFA3ZDced9dPOcv2Eydx/hgrfxYEcYCIBQ' \ 892 | b'D35JvzmqU05kSFV5eTvkhkaDObd7V55vokhm31+Li' 893 | 894 | expected_hash = b'\xE5\xCB\x68\xB2\xF8\x43\xD6\x3B\xF4\x0B\xCB\x20' \ 895 | b'\x07\x60\x8F\x81\x97\x61\x83\x92\x78\x3F\x23\x30' \ 896 | b'\xE5\xEF\x19\xA5\xBD\x8F\x0B\x2F\xAA\xC8\x61\x85' \ 897 | b'\x5F\xBB\x63\xA2\x21\xCC\x46\xFC\x1E\x22\x6A\x07' \ 898 | b'\x24\x11\xAF\x17\x5D\xDE\x47\x92\x81\xE0\x06\x87' \ 899 | b'\x8B\x34\x80\x59' 900 | actual_hash = _get_certificate_hash(base64.b64decode(cert_der)) 901 | assert actual_hash == expected_hash 902 | 903 | def test_invalid_signature_algorithm(self): 904 | # Manually edited from test_ecdsa_sha512 to change the OID to '1.2.840.10045.4.3.5' 905 | cert_der = b'MIIBjjCCATWgAwIBAgIQHVj2AGEwd6pOOSbcf0skQDAKBggqhkjOPQQ' \ 906 | b'DBTAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA3NTUzOV' \ 907 | b'oXDTE4MDUzMDA4MTUzOVowFTETMBEGA1UEAwwKU0VSVkVSMjAxNjBZM' \ 908 | b'BMGByqGSM49AgEGCCqGSM49AwEHA0IABL8d9S++MFpfzeH8B3vG/PjA' \ 909 | b'AWg8tGJVgsMw9nR+OfC9ltbTUwhB+yPk3JPcfW/bqsyeUgq4//LhaSp' \ 910 | b'lOWFNaNqjZzBlMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBg' \ 911 | b'EFBQcDAgYIKwYBBQUHAwEwFQYDVR0RBA4wDIIKU0VSVkVSMjAxNjAdB' \ 912 | b'gNVHQ4EFgQUKUkCgLlxoeai0EtQrZth1/BSc5kwCgYIKoZIzj0EAwUD' \ 913 | b'RwAwRAIgRrV7CLpDG7KueyFA3ZDced9dPOcv2Eydx/hgrfxYEcYCIBQ' \ 914 | b'D35JvzmqU05kSFV5eTvkhkaDObd7V55vokhm31+Li' 915 | 916 | expected_hash = None 917 | expected_warning = "Failed to get signature algorithm from " \ 918 | "certificate, unable to pass channel bindings:" 919 | 920 | with warnings.catch_warnings(record=True) as w: 921 | warnings.simplefilter("always") 922 | actual_hash = _get_certificate_hash(base64.b64decode(cert_der)) 923 | assert actual_hash == expected_hash 924 | assert expected_warning in str(w[-1].message) 925 | 926 | 927 | if __name__ == '__main__': 928 | unittest.main() 929 | --------------------------------------------------------------------------------