Pass through verified body in VerifiedRequest
parent
5d029f4f17
commit
c1acb39a5d
13
README.rst
13
README.rst
|
@ -85,15 +85,22 @@ constructor as bytes in the PEM format, or configure the key resolver as follows
|
|||
auth = HTTPSignatureAuth(algorithm=algorithms.RSA_V1_5_SHA256, key=fh.read(), key_resolver=MyKeyResolver())
|
||||
requests.get(url, auth=auth)
|
||||
|
||||
Digest algorithms
|
||||
~~~~~~~~~~~~~~~~~
|
||||
The library supports SHA-512 digests via subclassing::
|
||||
class MySigner(HTTPSignatureAuth):
|
||||
def add_digest(self, request):
|
||||
super().add_digest(request, algorithm="sha-512")
|
||||
|
||||
Links
|
||||
-----
|
||||
* `IETF HTTP Signatures draft <https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures>`_
|
||||
* `http-message-signatures <https://github.com/pyauth/http-message-signatures>`_ - a dependency of this library that
|
||||
handles much of the implementation
|
||||
* `Project home page (GitHub) <https://github.com/pyauth/requests-http-signature>`_
|
||||
* `Package documentation <https://pyauth.github.io/requests-http-signature/>`_
|
||||
* `Package distribution (PyPI) <https://pypi.python.org/pypi/requests-http-signature>`_
|
||||
* `Change log <https://github.com/pyauth/requests-http-signature/blob/master/Changes.rst>`_
|
||||
* `http-message-signatures <https://github.com/pyauth/http-message-signatures>`_ - a dependency of this library that
|
||||
handles much of the implementation
|
||||
* `IETF HTTP Signatures draft <https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures>`_
|
||||
|
||||
Bugs
|
||||
~~~~
|
||||
|
|
|
@ -66,10 +66,14 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
|
|||
the nonce can be controlled by subclassing this class and overloading the ``get_nonce()`` method.
|
||||
:param expires_in:
|
||||
Use this to set the ``expires`` signature parameter to the time of signing plus the given timedelta.
|
||||
:param component_resolver_class:
|
||||
Use this to subclass ``http_message_signatures.HTTPSignatureComponentResolver`` and customize header and
|
||||
derived component retrieval if needed.
|
||||
"""
|
||||
|
||||
component_resolver_class: type = HTTPSignatureComponentResolver
|
||||
"""
|
||||
A subclass of ``http_message_signatures.HTTPSignatureComponentResolver`` can be used to override this value
|
||||
to customize the retrieval of header and derived component values if needed.
|
||||
"""
|
||||
|
||||
_digest_hashers = {"sha-256": hashlib.sha256, "sha-512": hashlib.sha512}
|
||||
|
||||
def __init__(self, *,
|
||||
|
@ -81,8 +85,7 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
|
|||
label: str = None,
|
||||
include_alg: bool = True,
|
||||
use_nonce: bool = False,
|
||||
expires_in: datetime.timedelta = None,
|
||||
component_resolver_class: type = HTTPSignatureComponentResolver):
|
||||
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:
|
||||
|
@ -96,10 +99,9 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
|
|||
self.use_nonce = use_nonce
|
||||
self.covered_component_ids = covered_component_ids
|
||||
self.expires_in = expires_in
|
||||
handler_args = dict(signature_algorithm=signature_algorithm,
|
||||
self.signer = HTTPMessageSigner(signature_algorithm=signature_algorithm,
|
||||
key_resolver=key_resolver,
|
||||
component_resolver_class=component_resolver_class)
|
||||
self.signer = HTTPMessageSigner(**handler_args)
|
||||
component_resolver_class=self.component_resolver_class)
|
||||
|
||||
def add_date(self, request, timestamp):
|
||||
if "Date" not in request.headers:
|
||||
|
@ -108,9 +110,10 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
|
|||
def add_digest(self, request, algorithm="sha-256"):
|
||||
if request.body is None and "content-digest" in self.covered_component_ids:
|
||||
raise RequestsHttpSignatureException("Could not compute digest header for request without a body")
|
||||
if request.body is not None and "Content-Digest" not in request.headers:
|
||||
if request.body is not None:
|
||||
if "content-digest" not in self.covered_component_ids:
|
||||
self.covered_component_ids = list(self.covered_component_ids) + ["content-digest"]
|
||||
if "Content-Digest" not in request.headers:
|
||||
hasher = self._digest_hashers[algorithm]
|
||||
digest = hasher(request.body).digest()
|
||||
digest_node = http_sfv.Dictionary({algorithm: digest})
|
||||
|
@ -148,8 +151,7 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
|
|||
def verify(cls, request, *,
|
||||
require_components: List[str] = ("@method", "@authority", "@target-uri"),
|
||||
signature_algorithm: HTTPSignatureAlgorithm,
|
||||
key_resolver: HTTPSignatureKeyResolver,
|
||||
component_resolver_class: type = HTTPSignatureComponentResolver):
|
||||
key_resolver: HTTPSignatureKeyResolver):
|
||||
"""
|
||||
Verify an HTTP message signature.
|
||||
|
||||
|
@ -176,8 +178,8 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
|
|||
for requests without a body, and ("@method", "@authority", "@target-uri", "content-digest") for requests
|
||||
with a body.
|
||||
:param signature_algorithm:
|
||||
The algorithm expected to be used by the signature. Any signature not using the expected algorithm will be
|
||||
rejected. One of ``requests_http_signature.algorithms.HMAC_SHA256``,
|
||||
The algorithm expected to be used by the signature. Any signature not using the expected algorithm will
|
||||
cause an ``InvalidSignature`` exception. Must be one of ``requests_http_signature.algorithms.HMAC_SHA256``,
|
||||
``requests_http_signature.algorithms.ECDSA_P256_SHA256``,
|
||||
``requests_http_signature.algorithms.ED25519``,
|
||||
``requests_http_signature.algorithms.RSA_PSS_SHA512``, or
|
||||
|
@ -188,9 +190,6 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
|
|||
``get_private_key(key_id)`` (required only for signing) and ``get_public_key(key_id)`` (required only for
|
||||
verifying). Your implementation should ensure that the key id is recognized and return the corresponding
|
||||
key material as PEM bytes (or shared secret bytes for HMAC).
|
||||
:param component_resolver_class:
|
||||
Use this to subclass ``http_message_signatures.HTTPSignatureComponentResolver`` and customize header and
|
||||
derived component retrieval if needed.
|
||||
|
||||
:returns: *VerifyResult*, a namedtuple with the following attributes:
|
||||
|
||||
|
@ -198,6 +197,8 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
|
|||
* ``algorithm``: (same as ``signature_algorithm`` above)
|
||||
* ``covered_components``: A mapping of component names to their values, as covered by the signature
|
||||
* ``parameters``: A mapping of signature parameters to their values, as covered by the signature
|
||||
* ``body``: The message body for requests that have a body and pass validation of the covered
|
||||
content-digest; ``None`` otherwise.
|
||||
|
||||
:raises: ``InvalidSignature`` - raised whenever signature validation fails for any reason.
|
||||
"""
|
||||
|
@ -207,7 +208,7 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
|
|||
|
||||
verifier = HTTPMessageVerifier(signature_algorithm=signature_algorithm,
|
||||
key_resolver=key_resolver,
|
||||
component_resolver_class=component_resolver_class)
|
||||
component_resolver_class=cls.component_resolver_class)
|
||||
verify_results = verifier.verify(request)
|
||||
if len(verify_results) != 1:
|
||||
raise InvalidSignature("Multiple signatures are not supported.")
|
||||
|
@ -223,6 +224,8 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
|
|||
raise InvalidSignature("Found a content-digest header in a request with no body")
|
||||
digest = http_sfv.Dictionary()
|
||||
digest.parse(verify_result.covered_components[component_key].encode())
|
||||
if len(digest) < 1:
|
||||
raise InvalidSignature("Found a content-digest header with no digests")
|
||||
for k, v in digest.items():
|
||||
if k not in cls._digest_hashers:
|
||||
raise InvalidSignature(f'Unsupported digest algorithm "{k}"')
|
||||
|
@ -231,4 +234,5 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
|
|||
expect_digest = hasher(request.body).digest()
|
||||
if raw_digest != expect_digest:
|
||||
raise InvalidSignature("The content-digest header does not match the request body")
|
||||
verify_result = verify_result._replace(body=request.body)
|
||||
return verify_result
|
||||
|
|
2
setup.py
2
setup.py
|
@ -16,7 +16,7 @@ setup(
|
|||
},
|
||||
setup_requires=['setuptools_scm >= 3.4.3'],
|
||||
install_requires=[
|
||||
"http-message-signatures >= 0.2.1",
|
||||
"http-message-signatures >= 0.2.2",
|
||||
"http-sfv >= 0.9.3",
|
||||
"requests >= 2.27.1"
|
||||
],
|
||||
|
|
16
test/test.py
16
test/test.py
|
@ -36,6 +36,8 @@ class TestAdapter(HTTPAdapter):
|
|||
response = requests.Response()
|
||||
response.status_code = requests.codes.ok
|
||||
response.url = request.url
|
||||
response.headers["Received-Signature-Input"] = request.headers["Signature-Input"]
|
||||
response.headers["Received-Signature"] = request.headers["Signature"]
|
||||
return response
|
||||
|
||||
|
||||
|
@ -49,6 +51,7 @@ class TestRequestsHTTPSignature(unittest.TestCase):
|
|||
self.session = requests.Session()
|
||||
self.auth = HTTPSignatureAuth(key_id=default_keyid, key=hmac_secret, signature_algorithm=algorithms.HMAC_SHA256)
|
||||
self.session.mount("http://", TestAdapter(self.auth))
|
||||
self.session.mount("https://", TestAdapter(self.auth))
|
||||
|
||||
def test_basic_statements(self):
|
||||
url = 'http://example.com/path?query#fragment'
|
||||
|
@ -63,6 +66,19 @@ class TestRequestsHTTPSignature(unittest.TestCase):
|
|||
def test_expired_signature(self):
|
||||
"TODO"
|
||||
|
||||
def test_b21(self):
|
||||
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==:")
|
||||
},
|
||||
auth=self.auth
|
||||
)
|
||||
|
||||
@unittest.skip("TODO")
|
||||
def test_rsa(self):
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
|
Loading…
Reference in New Issue