Compare commits

...

11 Commits

Author SHA1 Message Date
Adam Romanek 6306c36c1d
Update README.rst - fix HTTPSignatureAuth params (#38)
The `key` and `key_resolver` params are mutually exclusive.
2023-04-11 18:58:51 -07:00
Andrey Kislyuk aff77a5db3 Update actions 2023-03-05 11:54:11 -08:00
Andrey Kislyuk 2baa832c66
Use gh to manage releases 2023-03-04 16:38:19 -08:00
Andrey Kislyuk fe661e5be8
Fix typo in package data manifest 2022-10-15 22:00:13 -07:00
Andrey Kislyuk 358f719912 Update isort config 2022-09-11 11:42:53 -07:00
Andrey Kislyuk 45f540a22c
Use isort and black 2022-08-31 10:50:44 -07:00
Andrey Kislyuk c643c8ccd7 Sort imports 2022-08-21 08:54:43 -07:00
Marc Seguí Coll d9091e090d
fix(readme): updated HTTPSignatureAuth arguments (#34)
Changed `algorithm` to `signature_algorithm` while instantiating `HTTPSignatureAuth` objects in the README.rst file
2022-05-04 08:56:42 -07:00
Andrey Kislyuk 434e68f87e
RSA-PSS now supported by cryptography 2022-04-26 15:18:24 -07:00
Andrey Kislyuk 04ce9858fe
Use mypy check-untyped-defs 2022-04-21 16:28:37 -07:00
Andrey Kislyuk e710760497
Fix GHA mypy run 2022-04-19 11:43:25 -07:00
9 changed files with 150 additions and 116 deletions

View File

@ -13,9 +13,8 @@ jobs:
python-version: ["3.7", "3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install build dependencies
@ -24,3 +23,11 @@ jobs:
run: pip install .[tests]
- name: Test
run: make test
black:
runs-on: ubuntu-22.04
steps:
- uses: psf/black@stable
isort:
runs-on: ubuntu-22.04
steps:
- uses: isort/isort-action@v1.1.0

View File

@ -2,7 +2,7 @@ SHELL=/bin/bash
lint:
flake8
mypy requests_http_signature
mypy --check-untyped-defs requests_http_signature
test: lint
python ./test/test.py -v

View File

@ -84,8 +84,7 @@ Asymmetric key algorithms
~~~~~~~~~~~~~~~~~~~~~~~~~
To sign or verify messages with an asymmetric key algorithm, set the ``signature_algorithm`` keyword argument to
``algorithms.ED25519``, ``algorithms.ECDSA_P256_SHA256``, ``algorithms.RSA_V1_5_SHA256``, or
``algorithms.RSA_PSS_SHA512``. Note that signing with rsa-pss-sha512 is not currently supported due to a limitation of
the cryptography library.
``algorithms.RSA_PSS_SHA512``.
For asymmetric key algorithms, you can supply the private key as the ``key`` parameter to the ``HTTPSignatureAuth()``
constructor as bytes in the PEM format, or configure the key resolver as follows:
@ -93,7 +92,7 @@ constructor as bytes in the PEM format, or configure the key resolver as follows
.. code-block:: python
with open('key.pem', 'rb') as fh:
auth = HTTPSignatureAuth(algorithm=algorithms.RSA_V1_5_SHA256,
auth = HTTPSignatureAuth(signature_algorithm=algorithms.RSA_V1_5_SHA256,
key=fh.read(),
key_id=preshared_key_id)
requests.get(url, auth=auth)
@ -105,9 +104,9 @@ constructor as bytes in the PEM format, or configure the key resolver as follows
def resolve_private_key(self, key_id: str):
return private_key_pem_bytes[key_id]
auth = HTTPSignatureAuth(algorithm=algorithms.RSA_V1_5_SHA256,
key=fh.read(),
key_resolver=MyKeyResolver())
auth = HTTPSignatureAuth(signature_algorithm=algorithms.RSA_V1_5_SHA256,
key_resolver=MyKeyResolver(),
key_id="my-key-id")
requests.get(url, auth=auth)
Digest algorithms

View File

@ -17,13 +17,8 @@ release:
@if [[ -z $$TAG ]]; then echo "Use release-{major,minor,patch}"; exit 1; fi
@if ! type -P pandoc; then echo "Please install pandoc"; exit 1; fi
@if ! type -P sponge; then echo "Please install moreutils"; exit 1; fi
@if ! type -P http; then echo "Please install httpie"; exit 1; fi
@if ! type -P gh; then echo "Please install gh"; exit 1; fi
@if ! type -P twine; then echo "Please install twine"; 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)
TAG_MSG=$$(mktemp); \
@ -35,14 +30,15 @@ release:
git commit -m ${TAG}; \
git tag --sign --annotate --file $$TAG_MSG ${TAG}
git push --follow-tags
http --check-status --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 --check-status --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
gh release create ${TAG} dist/*.whl --notes="$$(git tag --list ${TAG} -n99 | perl -pe 's/^\S+\s*// if $$. == 1' | sed 's/^\s\s\s\s//')"
$(MAKE) release-pypi
$(MAKE) release-docs
release-pypi:
python -m build
twine upload dist/*.tar.gz dist/*.whl --sign --verbose
release-docs:
$(MAKE) docs
-git branch -D gh-pages
@ -53,8 +49,4 @@ release-docs:
git push --force origin $$(git subtree split --prefix docs/html --branch gh-pages):refs/heads/gh-pages
git checkout -
release-pypi:
python -m build
twine upload dist/*.tar.gz dist/*.whl --sign --verbose
.PHONY: release

View File

@ -1,7 +1,9 @@
import os, sys
import os
import sys
import guzzle_sphinx_theme
sys.path.insert(0, os.path.abspath('..'))
sys.path.insert(0, os.path.abspath(".."))
project = "requests-http-signature"
copyright = "Andrey Kislyuk"
@ -27,6 +29,6 @@ html_sidebars = {
"logo-text.html",
# "globaltoc.html",
"localtoc.html",
"searchbox.html"
"searchbox.html",
]
}

6
pyproject.toml Normal file
View File

@ -0,0 +1,6 @@
[tool.black]
line-length = 120
exclude = ".*/version.py"
[tool.isort]
profile = "black"
line_length = 120

View File

@ -2,15 +2,21 @@ import datetime
import email.utils
import hashlib
import secrets
from typing import Union, Sequence, Type
from typing import Sequence, Type, Union
import http_sfv
import requests
from requests.exceptions import RequestException
from http_message_signatures import (algorithms, HTTPSignatureComponentResolver, HTTPSignatureKeyResolver, # noqa: F401
HTTPMessageSigner, HTTPMessageVerifier, HTTPSignatureAlgorithm, InvalidSignature)
from http_message_signatures import ( # noqa: F401
HTTPMessageSigner,
HTTPMessageVerifier,
HTTPSignatureAlgorithm,
HTTPSignatureComponentResolver,
HTTPSignatureKeyResolver,
InvalidSignature,
algorithms,
)
from http_message_signatures.structures import CaseInsensitiveDict, VerifyResult
from requests.exceptions import RequestException
class RequestsHttpSignatureException(RequestException):
@ -83,16 +89,19 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
_auto_cover_header_fields = {"authorization", "content-digest", "date"}
def __init__(self, *,
signature_algorithm: Type[HTTPSignatureAlgorithm],
key: bytes = None,
key_id: str,
key_resolver: HTTPSignatureKeyResolver = None,
covered_component_ids: Sequence[str] = ("@method", "@authority", "@target-uri"),
label: str = None,
include_alg: bool = True,
use_nonce: bool = False,
expires_in: datetime.timedelta = None):
def __init__(
self,
*,
signature_algorithm: Type[HTTPSignatureAlgorithm],
key: bytes = None,
key_id: str,
key_resolver: HTTPSignatureKeyResolver = None,
covered_component_ids: Sequence[str] = ("@method", "@authority", "@target-uri"),
label: str = None,
include_alg: bool = True,
use_nonce: bool = False,
expires_in: datetime.timedelta = None,
):
if key_resolver is None and key is None:
raise RequestsHttpSignatureException("Either key_resolver or key must be specified.")
if key_resolver is not None and key is not None:
@ -106,9 +115,11 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
self.use_nonce = use_nonce
self.covered_component_ids = covered_component_ids
self.expires_in = expires_in
self.signer = HTTPMessageSigner(signature_algorithm=signature_algorithm,
key_resolver=key_resolver,
component_resolver_class=self.component_resolver_class)
self.signer = HTTPMessageSigner(
signature_algorithm=signature_algorithm,
key_resolver=key_resolver,
component_resolver_class=self.component_resolver_class,
)
def add_date(self, request, timestamp):
if "Date" not in request.headers:
@ -150,14 +161,16 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
created = self.get_created(request)
expires = self.get_expires(request, created=created)
covered_component_ids = self.get_covered_component_ids(request)
self.signer.sign(request,
key_id=self.key_id,
created=created,
expires=expires,
nonce=self.get_nonce(request),
label=self.label,
include_alg=self.include_alg,
covered_component_ids=covered_component_ids)
self.signer.sign(
request,
key_id=self.key_id,
created=created,
expires=expires,
nonce=self.get_nonce(request),
label=self.label,
include_alg=self.include_alg,
covered_component_ids=covered_component_ids,
)
return request
@classmethod
@ -167,11 +180,15 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
return message.body
@classmethod
def verify(cls, message: Union[requests.PreparedRequest, requests.Response], *,
require_components: Sequence[str] = ("@method", "@authority", "@target-uri"),
signature_algorithm: Type[HTTPSignatureAlgorithm],
key_resolver: HTTPSignatureKeyResolver,
max_age: datetime.timedelta = datetime.timedelta(days=1)) -> VerifyResult:
def verify(
cls,
message: Union[requests.PreparedRequest, requests.Response],
*,
require_components: Sequence[str] = ("@method", "@authority", "@target-uri"),
signature_algorithm: Type[HTTPSignatureAlgorithm],
key_resolver: HTTPSignatureKeyResolver,
max_age: datetime.timedelta = datetime.timedelta(days=1),
) -> VerifyResult:
"""
Verify an HTTP message signature.
@ -238,9 +255,11 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
if "content-digest" not in require_components and '"content-digest"' not in require_components:
require_components = list(require_components) + ["content-digest"]
verifier = HTTPMessageVerifier(signature_algorithm=signature_algorithm,
key_resolver=key_resolver,
component_resolver_class=cls.component_resolver_class)
verifier = HTTPMessageVerifier(
signature_algorithm=signature_algorithm,
key_resolver=key_resolver,
component_resolver_class=cls.component_resolver_class,
)
verify_results = verifier.verify(message, max_age=max_age)
if len(verify_results) != 1:
raise InvalidSignature("Multiple signatures are not supported.")

View File

@ -1,24 +1,20 @@
#!/usr/bin/env python
from setuptools import setup, find_packages
from setuptools import find_packages, setup
setup(
name='requests-http-signature',
url='https://github.com/pyauth/requests-http-signature',
license='Apache Software License',
author='Andrey Kislyuk',
author_email='kislyuk@gmail.com',
name="requests-http-signature",
url="https://github.com/pyauth/requests-http-signature",
license="Apache Software License",
author="Andrey Kislyuk",
author_email="kislyuk@gmail.com",
description="A Requests auth module for HTTP Message Signatures",
long_description=open('README.rst').read(),
long_description=open("README.rst").read(),
use_scm_version={
"write_to": "requests_http_signature/version.py",
},
setup_requires=['setuptools_scm >= 3.4.3'],
install_requires=[
"http-message-signatures >= 0.4.3",
"http-sfv >= 0.9.3",
"requests >= 2.25.1"
],
setup_requires=["setuptools_scm >= 3.4.3"],
install_requires=["http-message-signatures >= 0.4.3", "http-sfv >= 0.9.3", "requests >= 2.25.1"],
extras_require={
"tests": [
"flake8",
@ -26,25 +22,26 @@ setup(
"build",
"wheel",
"mypy",
"types-requests",
]
},
packages=find_packages(exclude=['test']),
packages=find_packages(exclude=["test"]),
include_package_data=True,
package_data={
"http_message_signatures": ["py.typed"],
"requests_http_signature": ["py.typed"],
},
platforms=['MacOS X', 'Posix'],
test_suite='test',
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 :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Topic :: Software Development :: Libraries :: Python Modules'
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Operating System :: MacOS :: MacOS X",
"Operating System :: POSIX",
"Programming Language :: Python",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Software Development :: Libraries :: Python Modules",
],
)

View File

@ -1,15 +1,23 @@
#!/usr/bin/env python
import os, sys, unittest, logging, base64, io, json
import base64
import io
import json
import logging
import os
import sys
import unittest
import http_sfv
import requests
from requests.adapters import HTTPAdapter
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from requests_http_signature import algorithms, HTTPSignatureAuth # noqa: E402
from http_message_signatures import HTTPMessageSigner, InvalidSignature # noqa: E402
from http_message_signatures import HTTPMessageSigner # noqa: E402
from http_message_signatures import InvalidSignature # noqa: E402
from requests_http_signature import HTTPSignatureAuth, algorithms # noqa: E402
logging.basicConfig(level="DEBUG")
@ -24,8 +32,10 @@ class TestAdapter(HTTPAdapter):
self.client_auth = auth
def send(self, request, *args, **kwargs):
verify_args = dict(signature_algorithm=self.client_auth.signer.signature_algorithm,
key_resolver=self.client_auth.signer.key_resolver)
verify_args = dict(
signature_algorithm=self.client_auth.signer.signature_algorithm,
key_resolver=self.client_auth.signer.key_resolver,
)
HTTPSignatureAuth.verify(request, **verify_args)
if request.body is not None:
request.body = request.body[::-1]
@ -41,14 +51,18 @@ class TestAdapter(HTTPAdapter):
response.headers["Received-Signature-Input"] = request.headers["Signature-Input"]
response.headers["Received-Signature"] = request.headers["Signature"]
response.raw = io.BytesIO(json.dumps({}).encode())
signer = HTTPMessageSigner(signature_algorithm=self.client_auth.signer.signature_algorithm,
key_resolver=self.client_auth.signer.key_resolver)
signer = HTTPMessageSigner(
signature_algorithm=self.client_auth.signer.signature_algorithm,
key_resolver=self.client_auth.signer.key_resolver,
)
hasher = HTTPSignatureAuth._content_digest_hashers["sha-256"]
digest = hasher(response.raw.getvalue()).digest()
response.headers["Content-Digest"] = str(http_sfv.Dictionary({"sha-256": digest}))
signer.sign(response,
key_id=default_keyid,
covered_component_ids=("@method", "@authority", "content-digest", "@target-uri"))
signer.sign(
response,
key_id=default_keyid,
covered_component_ids=("@method", "@authority", "content-digest", "@target-uri"),
)
return response
@ -65,7 +79,7 @@ class TestRequestsHTTPSignature(unittest.TestCase):
self.session.mount("https://", TestAdapter(self.auth))
def test_basic_statements(self):
url = 'http://example.com/path?query#fragment'
url = "http://example.com/path?query#fragment"
self.session.get(url, auth=self.auth)
self.auth.signer.key_resolver.resolve_public_key = lambda k: b"abc"
with self.assertRaises(InvalidSignature):
@ -89,46 +103,44 @@ class TestRequestsHTTPSignature(unittest.TestCase):
HTTPSignatureAuth.verify(res, **verify_args)
def test_auto_cover_authorization_header(self):
url = 'http://example.com/path?query#fragment'
url = "http://example.com/path?query#fragment"
res = self.session.get(url, auth=self.auth, headers={"Authorization": "Bearer 12345"})
self.assertIn('"authorization"', res.headers["Received-Signature-Input"])
def test_b21(self):
url = 'https://example.com/foo?param=Value&Pet=dog'
url = "https://example.com/foo?param=Value&Pet=dog"
self.session.post(
url,
json={"hello": "world"},
headers={
"Date": "Tue, 20 Apr 2021 02:07:55 GMT",
"Content-Digest": ("sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+"
"AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:")
"Content-Digest": (
"sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+"
"AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:"
),
},
auth=self.auth
auth=self.auth,
)
@unittest.skip("TODO")
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()
)
from cryptography.hazmat.primitives.asymmetric import rsa
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)
encryption_algorithm=serialization.BestAvailableEncryption(passphrase),
)
public_key_pem = private_key.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo
)
url = 'http://example.com/path?query#fragment'
url = "http://example.com/path?query#fragment"
auth = HTTPSignatureAuth(algorithm="rsa-sha256", key=private_key_pem, key_id="sekret", passphrase=passphrase)
self.session.get(url, auth=auth, headers=dict(pubkey=base64.b64encode(public_key_pem)))
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()