├── 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 |
--------------------------------------------------------------------------------