Pass through verified body in VerifiedRequest

pull/34/head
Andrey Kislyuk 2022-04-10 22:10:03 -07:00
parent 5d029f4f17
commit c1acb39a5d
No known key found for this signature in database
GPG Key ID: 8AFAFCD242818A52
4 changed files with 53 additions and 26 deletions

View File

@ -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
~~~~

View File

@ -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,
key_resolver=key_resolver,
component_resolver_class=component_resolver_class)
self.signer = HTTPMessageSigner(**handler_args)
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:
@ -108,13 +110,14 @@ 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"]
hasher = self._digest_hashers[algorithm]
digest = hasher(request.body).digest()
digest_node = http_sfv.Dictionary({algorithm: digest})
request.headers["Content-Digest"] = str(digest_node)
if "Content-Digest" not in request.headers:
hasher = self._digest_hashers[algorithm]
digest = hasher(request.body).digest()
digest_node = http_sfv.Dictionary({algorithm: digest})
request.headers["Content-Digest"] = str(digest_node)
def get_nonce(self, request):
if self.use_nonce:
@ -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

View File

@ -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"
],

View File

@ -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