commit c474f2a8889bb6bdb1c9be4d2c4e73625adbf692 Author: Andrey Kislyuk Date: Mon Aug 21 17:02:39 2017 -0700 Begin requests-http-signature diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..958826b --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Reminder: +# - A leading slash means the pattern is anchored at the root. +# - No leading slash means the pattern matches at any depth. + +# Python files +*.pyc +__pycache__/ +.tox/ +*.egg-info/ +/build/ +/dist/ +/.eggs/ + +# Sphinx documentation +/docs/_build/ + +# IDE project files +/.pydevproject + +# vim python-mode plugin +/.ropeproject + +# IntelliJ IDEA / PyCharm project files +/.idea +/*.iml + +# JS/node/npm/web dev files +node_modules +npm-debug.log + +# OS X metadata files +.DS_Store + +# Python coverage +.coverage diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..aa55311 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +language: python +python: + - 2.7 + - 3.3 + - 3.4 + - 3.5 + - 3.6 + - nightly + +dist: trusty +sudo: false + +before_install: + - pip install --quiet codecov wheel + +install: + - make install + +script: + - make test + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/Changes.rst b/Changes.rst new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..08d0322 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +test_deps: + pip install coverage flake8 wheel + +lint: test_deps + ./setup.py flake8 + +test: test_deps lint + coverage run --source=$$(python setup.py --name) ./test/test.py + +init_docs: + cd docs; sphinx-quickstart + +docs: + $(MAKE) -C docs html + +install: clean + pip install wheel + python setup.py bdist_wheel + pip install --upgrade dist/*.whl + +clean: + -rm -rf build dist + -rm -rf *.egg-info + +.PHONY: lint test test_deps docs install clean + +include common.mk diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..255ca5a --- /dev/null +++ b/README.rst @@ -0,0 +1,49 @@ +requests-http-signature: A Requests auth module for HTTP Signature +================================================================== + +**requests-http-signature** is a `Requests `_ `authentication plugin +`_ (``requests.auth.AuthBase`` subclass) implementing +the `IETF HTTP Signatures draft `_. It has no dependencies +outside the standard library. + +.. code-block:: python + + import requests + from requests_http_signature import HTTPSignatureAuth + preshared_secret = 'monorail_cat' + url = 'http://httpbin.org/get' + requests.get(url, auth=HTTPSignatureAuth(secret=preshared_secret)) + + +Installation +------------ +:: + + pip install requests-http-signature + +Links +----- +* `IETF HTTP Signatures draft `_ +* `Project home page (GitHub) `_ +* `Documentation (Read the Docs) `_ +* `Package distribution (PyPI) `_ +* `Change log `_ + +Bugs +~~~~ +Please report bugs, issues, feature requests, etc. on `GitHub `_. + +License +------- +Licensed under the terms of the `Apache License, Version 2.0 `_. + +.. image:: https://travis-ci.org/kislyuk/requests-http-signature.png + :target: https://travis-ci.org/kislyuk/requests-http-signature +.. image:: https://codecov.io/github/kislyuk/requests-http-signature/coverage.svg?branch=master + :target: https://codecov.io/github/kislyuk/requests-http-signature?branch=master +.. image:: https://img.shields.io/pypi/v/requests-http-signature.svg + :target: https://pypi.python.org/pypi/requests-http-signature +.. image:: https://img.shields.io/pypi/l/requests-http-signature.svg + :target: https://pypi.python.org/pypi/requests-http-signature +.. image:: https://readthedocs.org/projects/requests-http-signature/badge/?version=latest + :target: https://requests-http-signature.readthedocs.org/ diff --git a/common.mk b/common.mk new file mode 100644 index 0000000..edd3efe --- /dev/null +++ b/common.mk @@ -0,0 +1,45 @@ +SHELL=/bin/bash -eo pipefail + +release_major: + $(eval export TAG=$(shell git describe --tags --match 'v*.*.*' | perl -ne '/^v(\d)+\.(\d)+\.(\d+)+/; print "v@{[$$1+1]}.0.0"')) + $(MAKE) release + +release_minor: + $(eval export TAG=$(shell git describe --tags --match 'v*.*.*' | perl -ne '/^v(\d)+\.(\d)+\.(\d+)+/; print "v$$1.@{[$$2+1]}.0"')) + $(MAKE) release + +release_patch: + $(eval export TAG=$(shell git describe --tags --match 'v*.*.*' | perl -ne '/^v(\d)+\.(\d)+\.(\d+)+/; print "v$$1.$$2.@{[$$3+1]}"')) + $(MAKE) release + +release: + @if [[ -z $$TAG ]]; then echo "Use release_{major,minor,patch}"; exit 1; fi + $(eval REMOTE=$(shell git remote get-url origin | perl -ne '/([^\/\:]+\/.+?)(\.git)?$$/; print $$1')) + $(eval GIT_USER=$(shell git config --get user.email)) + $(eval GH_AUTH=$(shell if grep -q '@github.com' ~/.git-credentials; then echo $$(grep '@github.com' ~/.git-credentials | python3 -c 'import sys, urllib.parse as p; print(p.urlparse(sys.stdin.read()).netloc.split("@")[0])'); else echo $(GIT_USER); fi)) + $(eval RELEASES_API=https://api.github.com/repos/${REMOTE}/releases) + $(eval UPLOADS_API=https://uploads.github.com/repos/${REMOTE}/releases) + git pull + git clean -x --force $$(python setup.py --name) + sed -i -e "s/version=\([\'\"]\)[0-9]*\.[0-9]*\.[0-9]*/version=\1$${TAG:1}/" setup.py + git add setup.py + TAG_MSG=$$(mktemp); \ + echo "# Changes for ${TAG} ($$(date +%Y-%m-%d))" > $$TAG_MSG; \ + git log --pretty=format:%s $$(git describe --abbrev=0)..HEAD >> $$TAG_MSG; \ + $${EDITOR:-emacs} $$TAG_MSG; \ + if [[ -f Changes.md ]]; then cat $$TAG_MSG <(echo) Changes.md | sponge Changes.md; git add Changes.md; fi; \ + if [[ -f Changes.rst ]]; then cat <(pandoc --from markdown --to rst $$TAG_MSG) <(echo) Changes.rst | sponge Changes.rst; git add Changes.rst; fi; \ + git commit -m ${TAG}; \ + git tag --sign --annotate --file $$TAG_MSG ${TAG} + git push --follow-tags + http --auth ${GH_AUTH} ${RELEASES_API} tag_name=${TAG} name=${TAG} \ + body="$$(git tag --list ${TAG} -n99 | perl -pe 's/^\S+\s*// if $$. == 1' | sed 's/^\s\s\s\s//')" + $(MAKE) install + http --auth ${GH_AUTH} POST ${UPLOADS_API}/$$(http --auth ${GH_AUTH} ${RELEASES_API}/latest | jq .id)/assets \ + name==$$(basename dist/*.whl) label=="Python Wheel" < dist/*.whl + $(MAKE) pypi_release + +pypi_release: + python setup.py sdist bdist_wheel upload --sign + +.PHONY: release diff --git a/requests_http_signature/__init__.py b/requests_http_signature/__init__.py new file mode 100644 index 0000000..7292064 --- /dev/null +++ b/requests_http_signature/__init__.py @@ -0,0 +1,107 @@ +from __future__ import absolute_import, division, print_function, unicode_literals + +import base64, hashlib, hmac, time +import email.utils +import requests +from requests.compat import urlparse + +class Crypto: + def __init__(self, algorithm): + if algorithm != "hmac-sha256": + from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.asymmetric import rsa, ec + from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 + from cryptography.hazmat.primitives.hashes import SHA1, SHA256 + self.__dict__.update(locals()) + + def sign(self, string_to_sign, key, passphrase=None): + if self.algorithm == "hmac-sha256": + return hmac.new(key, string_to_sign, digestmod=hashlib.sha256).digest() + key = self.load_pem_private_key(key, password=passphrase, backend=self.default_backend()) + if self.algorithm in {"rsa-sha1", "rsa-sha256"}: + hasher = self.SHA1() if self.algorithm.endswith("sha1") else self.SHA256() + signer = key.signer(padding=self.PKCS1v15(), algorithm=hasher) + elif self.algorithm == "ecdsa-sha256": + signer = key.signer(signature_algorithm=self.ec.ECDSA(algorithm=self.SHA256())) + signer.update(string_to_sign) + return signer.finalize() + + def verify(self, signature, string_to_sign, key): + if self.algorithm == "hmac-sha256": + assert signature == hmac.new(key, string_to_sign, digestmod=hashlib.sha256).digest() + else: + key = self.load_pem_public_key(key, backend=self.default_backend()) + key.verify(signature, string_to_sign, self.PKCS1v15(), self.SHA256()) + +class HTTPSignatureAuth(requests.auth.AuthBase): + hasher_constructor = hashlib.sha256 + known_algorithms = { + "rsa-sha1", + "rsa-sha256", + "hmac-sha256", + "ecdsa-sha256", + } + + def __init__(self, key, key_id="hmac-key-1", algorithm="hmac-sha256", headers=None, passphrase=None): + assert algorithm in self.known_algorithms + self.key = key + self.key_id = key_id + self.algorithm = algorithm + self.headers = [h.lower() for h in headers] if headers is not None else ["date"] + self.passphrase = passphrase + + def add_date(self, request, timestamp=None): + if "Date" not in request.headers: + if timestamp is None: + timestamp = time.time() + request.headers["Date"] = email.utils.formatdate(timestamp, usegmt=True) + + def add_digest(self, request): + if request.body is not None and "Digest" not in request.headers: + if "digest" not in self.headers: + self.headers.append("digest") + digest = self.hasher_constructor(request.body).digest() + request.headers["Digest"] = "SHA-256=" + base64.b64encode(digest).decode() + + def get_string_to_sign(self, request, headers): + sts = [] + for header in headers: + if header == "(request-target)": + sts.append("(request-target): {} {}".format(request.method.lower(), request.path_url)) + else: + if header.lower() == "host": + value = request.headers.get("host", urlparse(request.url).hostname) + else: + value = request.headers[header] + sts.append("{k}: {v}".format(k=header.lower(), v=value)) + return "\n".join(sts).encode() + + def __call__(self, request): + self.add_date(request) + self.add_digest(request) + raw_sig = Crypto(self.algorithm).sign(string_to_sign=self.get_string_to_sign(request, self.headers), + key=self.key, + passphrase=self.passphrase) + sig = base64.b64encode(raw_sig).decode() + sig_struct = [("keyId", self.key_id), + ("algorithm", self.algorithm), + ("headers", " ".join(self.headers)), + ("signature", sig)] + request.headers["Authorization"] = "Signature " + ",".join('{}="{}"'.format(k, v) for k, v in sig_struct) + return request + + def verify(self, request, key_resolver): + assert "Authorization" in request.headers, "No Authorization header found" + msg = 'Unexpected scheme found in Authorization header (expected "Signature")' + assert request.headers["Authorization"].startswith("Signature "), msg + scheme, sig_struct = request.headers["Authorization"].split(" ", 1) + sig_struct = {i.split("=", 1)[0]: i.split("=", 1)[1].strip('"') for i in sig_struct.split(",")} + for field in "keyId", "algorithm", "signature": + assert field in sig_struct, 'Required signature parameter "{}" not found'.format(field) + assert sig_struct["algorithm"] in self.known_algorithms, "Unknown signature algorithm" + headers = sig_struct.get("headers", "date").split(" ") + sig = base64.b64decode(sig_struct["signature"]) + sts = self.get_string_to_sign(request, headers) + key = key_resolver(key_id=sig_struct["keyId"], algorithm=sig_struct["algorithm"]) + Crypto(sig_struct["algorithm"]).verify(sig, sts, key) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..e9a8c95 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[bdist_wheel] +universal=1 +[flake8] +max-line-length=120 +ignore: E301, E302, E401, E261, E265, E226, F401, E501 diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..979aa71 --- /dev/null +++ b/setup.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python + +import os, glob +from setuptools import setup, find_packages + +setup( + name='requests-http-signature', + version='0.0.1', + url='https://github.com/kislyuk/requests-http-signature', + license='Apache Software License', + author='Andrey Kislyuk', + author_email='kislyuk@gmail.com', + description="A Requests auth module for HTTP Signature", + long_description=open('README.rst').read(), + packages=find_packages(exclude=['test']), + include_package_data=True, + platforms=['MacOS X', 'Posix'], + test_suite='test', + classifiers=[ + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: MacOS :: MacOS X', + 'Operating System :: POSIX', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Development Status :: 5 - Production/Stable', + 'Topic :: Software Development :: Libraries :: Python Modules' + ] +) diff --git a/test/test.py b/test/test.py new file mode 100755 index 0000000..0e0081d --- /dev/null +++ b/test/test.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python + +from __future__ import absolute_import, division, print_function, unicode_literals + +import os, sys, unittest, json, logging, base64 + +import requests +from requests.adapters import HTTPAdapter + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) # noqa + +from requests_http_signature import HTTPSignatureAuth + +hmac_secret = b"monorail_cat" +passphrase = b"passw0rd" + +class TestAdapter(HTTPAdapter): + def send(self, request, *args, **kwargs): + def key_resolver(key_id, algorithm): + if "pubkey" in request.headers: + return base64.b64decode(request.headers["pubkey"]) + return hmac_secret + HTTPSignatureAuth(key=hmac_secret).verify(request, key_resolver=key_resolver) + response = requests.Response() + response.status_code = requests.codes.ok + response.url = request.url + return response + +class TestRequestsHTTPSignature(unittest.TestCase): + def setUp(self): + logging.basicConfig(level="DEBUG") + self.session = requests.Session() + self.session.mount("http://", TestAdapter()) + + def test_basic_statements(self): + url = 'http://example.com/path?query#fragment' + self.session.get(url, auth=HTTPSignatureAuth(key=hmac_secret)) + with self.assertRaises(AssertionError): + self.session.get(url, auth=HTTPSignatureAuth(key=hmac_secret[::-1])) + + def test_rfc_example(self): + url = 'http://example.org/foo' + payload = {"hello": "world"} + date = "Tue, 07 Jun 2014 20:51:35 GMT" + auth = HTTPSignatureAuth(key=hmac_secret, + headers=["(request-target)", "host", "date", "digest", "content-length"]) + self.session.post(url, json=payload, headers={"Date": date}, auth=auth) + + def test_rsa(self): + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.asymmetric import rsa + from cryptography.hazmat.primitives import serialization + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=default_backend() + ) + private_key_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.BestAvailableEncryption(passphrase) + ) + public_key_pem = private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + url = 'http://example.com/path?query#fragment' + auth = HTTPSignatureAuth(algorithm="rsa-sha256", key=private_key_pem, passphrase=passphrase) + self.session.get(url, auth=auth, headers=dict(pubkey=base64.b64encode(public_key_pem))) + +if __name__ == '__main__': + unittest.main()