Add require_components to verify; expand docs
parent
fabae549c8
commit
d71b15bb07
15
README.rst
15
README.rst
|
@ -44,7 +44,20 @@ incoming requests:
|
||||||
|
|
||||||
HTTPSignatureAuth.verify(request, signature_algorithm=algorithms.HMAC_SHA256, key_resolver=key_resolver)
|
HTTPSignatureAuth.verify(request, signature_algorithm=algorithms.HMAC_SHA256, key_resolver=key_resolver)
|
||||||
|
|
||||||
See the `API documentation <https://pyauth.github.io/requests-http-signature/#api-documentation>`_ for full detials.
|
.. admonition:: See what is signed
|
||||||
|
|
||||||
|
It is important to understand and follow the best practice rule of "See what is signed" when verifying HTTP message
|
||||||
|
signatures. The gist of this rule is: if your application neglects to verify that the information it trusts is
|
||||||
|
what was actually signed, the attacker can supply a valid signature but point you to malicious data that wasn't signed
|
||||||
|
by that signature. Failure to follow this rule can lead to vulnerability against signature wrapping and substitution
|
||||||
|
attacks.
|
||||||
|
|
||||||
|
In requests-http-signature, you can ensure that the information signed is what you expect to be signed by only trusting
|
||||||
|
the data returned by the ``verify()`` method::
|
||||||
|
|
||||||
|
verify_result = HTTPSignatureAuth.verify(request, ...)
|
||||||
|
|
||||||
|
See the `API documentation <https://pyauth.github.io/requests-http-signature/#api-documentation>`_ for full details.
|
||||||
|
|
||||||
Asymmetric key algorithms
|
Asymmetric key algorithms
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
|
@ -10,7 +10,6 @@ import requests
|
||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
from http_message_signatures import (algorithms, HTTPSignatureComponentResolver, HTTPSignatureKeyResolver, # noqa: F401
|
from http_message_signatures import (algorithms, HTTPSignatureComponentResolver, HTTPSignatureKeyResolver, # noqa: F401
|
||||||
HTTPMessageSigner, HTTPMessageVerifier, HTTPSignatureAlgorithm, InvalidSignature)
|
HTTPMessageSigner, HTTPMessageVerifier, HTTPSignatureAlgorithm, InvalidSignature)
|
||||||
from http_message_signatures.structures import CaseInsensitiveDict
|
|
||||||
|
|
||||||
|
|
||||||
class RequestsHttpSignatureException(RequestException):
|
class RequestsHttpSignatureException(RequestException):
|
||||||
|
@ -147,14 +146,38 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def verify(cls, request, *,
|
def verify(cls, request, *,
|
||||||
|
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):
|
component_resolver_class: type = HTTPSignatureComponentResolver):
|
||||||
"""
|
"""
|
||||||
Verify an HTTP message signature.
|
Verify an HTTP message signature.
|
||||||
|
|
||||||
|
.. admonition:: See what is signed
|
||||||
|
|
||||||
|
It is important to understand and follow the best practice rule of "See what is signed" when verifying HTTP
|
||||||
|
message signatures. The gist of this rule is: if your application neglects to verify that the information it
|
||||||
|
trusts is what was actually signed, the attacker can supply a valid signature but point you to malicious data
|
||||||
|
that wasn't signed by that signature. Failure to follow this rule can lead to vulnerability against signature
|
||||||
|
wrapping and substitution attacks.
|
||||||
|
|
||||||
|
You can ensure that the information signed is what you expect to be signed by only trusting the *VerifyResult*
|
||||||
|
tuple returned by ``verify()``.
|
||||||
|
|
||||||
|
:param request: The HTTP request to verify.
|
||||||
|
:param require_components:
|
||||||
|
A list of lowercased header names or derived component IDs ("@method", "@target-uri", "@authority",
|
||||||
|
"@scheme", "@request-target", "@path", "@query", "@query-params", "@status", or "@request-response" as
|
||||||
|
specified in the standard) to require to be covered by the signature. If the "content-digest" is
|
||||||
|
specified here (recommended for requests that have a body), it will be verified by matching it against the
|
||||||
|
digest hash computed on the body of the request (expected to be bytes).
|
||||||
|
|
||||||
|
If this parameter is not specified, ``verify()`` will set it to ("@method", "@authority", "@target-uri")
|
||||||
|
for requests without a body, and ("@method", "@authority", "@target-uri", "content-digest") for requests
|
||||||
|
with a body.
|
||||||
:param signature_algorithm:
|
:param signature_algorithm:
|
||||||
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 be
|
||||||
|
rejected. 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
|
||||||
|
@ -168,25 +191,44 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
|
||||||
:param component_resolver_class:
|
:param component_resolver_class:
|
||||||
Use this to subclass ``http_message_signatures.HTTPSignatureComponentResolver`` and customize header and
|
Use this to subclass ``http_message_signatures.HTTPSignatureComponentResolver`` and customize header and
|
||||||
derived component retrieval if needed.
|
derived component retrieval if needed.
|
||||||
|
|
||||||
|
:returns: *VerifyResult*, a namedtuple with the following attributes:
|
||||||
|
|
||||||
|
* ``label`` (str): The label for the signature
|
||||||
|
* ``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
|
||||||
|
|
||||||
|
:raises: ``InvalidSignature`` - raised whenever signature validation fails for any reason.
|
||||||
"""
|
"""
|
||||||
|
if request.body is not None:
|
||||||
|
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,
|
verifier = HTTPMessageVerifier(signature_algorithm=signature_algorithm,
|
||||||
key_resolver=key_resolver,
|
key_resolver=key_resolver,
|
||||||
component_resolver_class=component_resolver_class)
|
component_resolver_class=component_resolver_class)
|
||||||
verify_result = verifier.verify(request)
|
verify_results = verifier.verify(request)
|
||||||
# TODO: get content-digest from verify result, not from independent parsing of headers
|
if len(verify_results) != 1:
|
||||||
# TODO: add options to require specific components
|
raise InvalidSignature("Multiple signatures are not supported.")
|
||||||
headers = CaseInsensitiveDict(request.headers)
|
verify_result = verify_results[0]
|
||||||
if "content-digest" in headers:
|
for component_name in require_components:
|
||||||
if request.body is None:
|
component_key = component_name
|
||||||
raise InvalidSignature("Found a content-digest header in a request with no body")
|
if not component_key.startswith('"'):
|
||||||
digest = http_sfv.Dictionary()
|
component_key = str(http_sfv.List([http_sfv.Item(component_name)]))
|
||||||
digest.parse(headers["content-digest"].encode())
|
if component_key not in verify_result.covered_components:
|
||||||
for k, v in digest.items():
|
raise InvalidSignature(f"A required component, {component_key}, was not covered by the signature.")
|
||||||
if k not in cls._digest_hashers:
|
if component_key == '"content-digest"':
|
||||||
raise InvalidSignature(f'Unsupported digest algorithm "{k}"')
|
if request.body is None:
|
||||||
raw_digest = v.value
|
raise InvalidSignature("Found a content-digest header in a request with no body")
|
||||||
hasher = cls._digest_hashers[k]
|
digest = http_sfv.Dictionary()
|
||||||
expect_digest = hasher(request.body).digest()
|
digest.parse(verify_result.covered_components[component_key].encode())
|
||||||
if raw_digest != expect_digest:
|
for k, v in digest.items():
|
||||||
raise InvalidSignature("The content-digest header does not match the request body")
|
if k not in cls._digest_hashers:
|
||||||
|
raise InvalidSignature(f'Unsupported digest algorithm "{k}"')
|
||||||
|
raw_digest = v.value
|
||||||
|
hasher = cls._digest_hashers[k]
|
||||||
|
expect_digest = hasher(request.body).digest()
|
||||||
|
if raw_digest != expect_digest:
|
||||||
|
raise InvalidSignature("The content-digest header does not match the request body")
|
||||||
return verify_result
|
return verify_result
|
||||||
|
|
13
test/test.py
13
test/test.py
|
@ -23,9 +23,16 @@ class TestAdapter(HTTPAdapter):
|
||||||
self.client_auth = auth
|
self.client_auth = auth
|
||||||
|
|
||||||
def send(self, request, *args, **kwargs):
|
def send(self, request, *args, **kwargs):
|
||||||
HTTPSignatureAuth.verify(request,
|
verify_args = dict(signature_algorithm=self.client_auth.signer.signature_algorithm,
|
||||||
signature_algorithm=self.client_auth.signer.signature_algorithm,
|
key_resolver=self.client_auth.signer.key_resolver)
|
||||||
key_resolver=self.client_auth.signer.key_resolver)
|
HTTPSignatureAuth.verify(request, **verify_args)
|
||||||
|
if request.body is not None:
|
||||||
|
request.body = request.body[::-1]
|
||||||
|
try:
|
||||||
|
HTTPSignatureAuth.verify(request, **verify_args)
|
||||||
|
raise Exception("Expected InvalidSignature to be raised")
|
||||||
|
except InvalidSignature:
|
||||||
|
pass
|
||||||
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
|
||||||
|
|
Loading…
Reference in New Issue