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()) auth = HTTPSignatureAuth(algorithm=algorithms.RSA_V1_5_SHA256, key=fh.read(), key_resolver=MyKeyResolver())
requests.get(url, auth=auth) 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 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>`_ * `Project home page (GitHub) <https://github.com/pyauth/requests-http-signature>`_
* `Package documentation <https://pyauth.github.io/requests-http-signature/>`_ * `Package documentation <https://pyauth.github.io/requests-http-signature/>`_
* `Package distribution (PyPI) <https://pypi.python.org/pypi/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>`_ * `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 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. the nonce can be controlled by subclassing this class and overloading the ``get_nonce()`` method.
:param expires_in: :param expires_in:
Use this to set the ``expires`` signature parameter to the time of signing plus the given timedelta. 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} _digest_hashers = {"sha-256": hashlib.sha256, "sha-512": hashlib.sha512}
def __init__(self, *, def __init__(self, *,
@ -81,8 +85,7 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
label: str = None, label: str = None,
include_alg: bool = True, include_alg: bool = True,
use_nonce: bool = False, use_nonce: bool = False,
expires_in: datetime.timedelta = None, expires_in: datetime.timedelta = None):
component_resolver_class: type = HTTPSignatureComponentResolver):
if key_resolver is None and key is None: if key_resolver is None and key is None:
raise RequestsHttpSignatureException("Either key_resolver or key must be specified.") raise RequestsHttpSignatureException("Either key_resolver or key must be specified.")
if key_resolver is not None and key is not None: 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.use_nonce = use_nonce
self.covered_component_ids = covered_component_ids self.covered_component_ids = covered_component_ids
self.expires_in = expires_in self.expires_in = expires_in
handler_args = dict(signature_algorithm=signature_algorithm, self.signer = HTTPMessageSigner(signature_algorithm=signature_algorithm,
key_resolver=key_resolver, key_resolver=key_resolver,
component_resolver_class=component_resolver_class) component_resolver_class=self.component_resolver_class)
self.signer = HTTPMessageSigner(**handler_args)
def add_date(self, request, timestamp): def add_date(self, request, timestamp):
if "Date" not in request.headers: if "Date" not in request.headers:
@ -108,13 +110,14 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
def add_digest(self, request, algorithm="sha-256"): def add_digest(self, request, algorithm="sha-256"):
if request.body is None and "content-digest" in self.covered_component_ids: 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") 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: if "content-digest" not in self.covered_component_ids:
self.covered_component_ids = list(self.covered_component_ids) + ["content-digest"] self.covered_component_ids = list(self.covered_component_ids) + ["content-digest"]
hasher = self._digest_hashers[algorithm] if "Content-Digest" not in request.headers:
digest = hasher(request.body).digest() hasher = self._digest_hashers[algorithm]
digest_node = http_sfv.Dictionary({algorithm: digest}) digest = hasher(request.body).digest()
request.headers["Content-Digest"] = str(digest_node) digest_node = http_sfv.Dictionary({algorithm: digest})
request.headers["Content-Digest"] = str(digest_node)
def get_nonce(self, request): def get_nonce(self, request):
if self.use_nonce: if self.use_nonce:
@ -148,8 +151,7 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
def verify(cls, request, *, def verify(cls, request, *,
require_components: List[str] = ("@method", "@authority", "@target-uri"), require_components: List[str] = ("@method", "@authority", "@target-uri"),
signature_algorithm: HTTPSignatureAlgorithm, signature_algorithm: HTTPSignatureAlgorithm,
key_resolver: HTTPSignatureKeyResolver, key_resolver: HTTPSignatureKeyResolver):
component_resolver_class: type = HTTPSignatureComponentResolver):
""" """
Verify an HTTP message signature. 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 for requests without a body, and ("@method", "@authority", "@target-uri", "content-digest") for requests
with a body. with a body.
:param signature_algorithm: :param signature_algorithm:
The algorithm expected to be used by the signature. Any signature not using the expected algorithm will be The algorithm expected to be used by the signature. Any signature not using the expected algorithm will
rejected. One of ``requests_http_signature.algorithms.HMAC_SHA256``, 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.ECDSA_P256_SHA256``,
``requests_http_signature.algorithms.ED25519``, ``requests_http_signature.algorithms.ED25519``,
``requests_http_signature.algorithms.RSA_PSS_SHA512``, or ``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 ``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 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). 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: :returns: *VerifyResult*, a namedtuple with the following attributes:
@ -198,6 +197,8 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
* ``algorithm``: (same as ``signature_algorithm`` above) * ``algorithm``: (same as ``signature_algorithm`` above)
* ``covered_components``: A mapping of component names to their values, as covered by the signature * ``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 * ``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. :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, verifier = HTTPMessageVerifier(signature_algorithm=signature_algorithm,
key_resolver=key_resolver, key_resolver=key_resolver,
component_resolver_class=component_resolver_class) component_resolver_class=cls.component_resolver_class)
verify_results = verifier.verify(request) verify_results = verifier.verify(request)
if len(verify_results) != 1: if len(verify_results) != 1:
raise InvalidSignature("Multiple signatures are not supported.") 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") raise InvalidSignature("Found a content-digest header in a request with no body")
digest = http_sfv.Dictionary() digest = http_sfv.Dictionary()
digest.parse(verify_result.covered_components[component_key].encode()) 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(): for k, v in digest.items():
if k not in cls._digest_hashers: if k not in cls._digest_hashers:
raise InvalidSignature(f'Unsupported digest algorithm "{k}"') raise InvalidSignature(f'Unsupported digest algorithm "{k}"')
@ -231,4 +234,5 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
expect_digest = hasher(request.body).digest() expect_digest = hasher(request.body).digest()
if raw_digest != expect_digest: if raw_digest != expect_digest:
raise InvalidSignature("The content-digest header does not match the request body") raise InvalidSignature("The content-digest header does not match the request body")
verify_result = verify_result._replace(body=request.body)
return verify_result return verify_result

View File

@ -16,7 +16,7 @@ setup(
}, },
setup_requires=['setuptools_scm >= 3.4.3'], setup_requires=['setuptools_scm >= 3.4.3'],
install_requires=[ install_requires=[
"http-message-signatures >= 0.2.1", "http-message-signatures >= 0.2.2",
"http-sfv >= 0.9.3", "http-sfv >= 0.9.3",
"requests >= 2.27.1" "requests >= 2.27.1"
], ],

View File

@ -36,6 +36,8 @@ class TestAdapter(HTTPAdapter):
response = requests.Response() response = requests.Response()
response.status_code = requests.codes.ok response.status_code = requests.codes.ok
response.url = request.url response.url = request.url
response.headers["Received-Signature-Input"] = request.headers["Signature-Input"]
response.headers["Received-Signature"] = request.headers["Signature"]
return response return response
@ -49,6 +51,7 @@ class TestRequestsHTTPSignature(unittest.TestCase):
self.session = requests.Session() self.session = requests.Session()
self.auth = HTTPSignatureAuth(key_id=default_keyid, key=hmac_secret, signature_algorithm=algorithms.HMAC_SHA256) 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("http://", TestAdapter(self.auth))
self.session.mount("https://", TestAdapter(self.auth))
def test_basic_statements(self): def test_basic_statements(self):
url = 'http://example.com/path?query#fragment' url = 'http://example.com/path?query#fragment'
@ -63,6 +66,19 @@ class TestRequestsHTTPSignature(unittest.TestCase):
def test_expired_signature(self): def test_expired_signature(self):
"TODO" "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") @unittest.skip("TODO")
def test_rsa(self): def test_rsa(self):
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend