Expand docs
parent
1fe5e37fef
commit
1e3cb991e7
|
@ -1,5 +1,8 @@
|
||||||
|
import os, sys
|
||||||
import guzzle_sphinx_theme
|
import guzzle_sphinx_theme
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath('..'))
|
||||||
|
|
||||||
project = "requests-http-signature"
|
project = "requests-http-signature"
|
||||||
copyright = "Andrey Kislyuk"
|
copyright = "Andrey Kislyuk"
|
||||||
author = "Andrey Kislyuk"
|
author = "Andrey Kislyuk"
|
||||||
|
|
|
@ -5,7 +5,6 @@ API documentation
|
||||||
|
|
||||||
.. automodule:: requests_http_signature
|
.. automodule:: requests_http_signature
|
||||||
:members:
|
:members:
|
||||||
:special-members:
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
|
@ -32,19 +32,57 @@ class SingleKeyResolver(HTTPSignatureKeyResolver):
|
||||||
|
|
||||||
|
|
||||||
class HTTPSignatureAuth(requests.auth.AuthBase):
|
class HTTPSignatureAuth(requests.auth.AuthBase):
|
||||||
hasher_name = "sha-256"
|
"""
|
||||||
hasher_constructor = hashlib.sha256
|
A `Requests <https://github.com/requests/requests>`_ `authentication plugin
|
||||||
|
<http://docs.python-requests.org/en/master/user/authentication/>`_ (``requests.auth.AuthBase`` subclass)
|
||||||
|
implementing the `IETF HTTP Message Signatures draft RFC
|
||||||
|
<https://datatracker.ietf.org/doc/draft-ietf-httpbis-message-signatures/>`_.
|
||||||
|
|
||||||
|
:param signature_algorithm:
|
||||||
|
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
|
||||||
|
``requests_http_signature.algorithms.RSA_V1_5_SHA256``.
|
||||||
|
:param key:
|
||||||
|
Key material that will be used to sign the request. In the case of HMAC, this should be the raw bytes of the
|
||||||
|
shared secret; for all other algorithms, this should be the bytes of the PEM-encoded private key material.
|
||||||
|
:param key_id: The key ID to use in the signature.
|
||||||
|
:param key_resolver:
|
||||||
|
Instead of specifying a fixed key, you can instead pass a key resolver, which should be an instance of a
|
||||||
|
subclass of ``http_message_signatures.HTTPSignatureKeyResolver``. A key resolver should have two methods,
|
||||||
|
``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 covered_component_ids:
|
||||||
|
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 sign.
|
||||||
|
:param label: The label to use to identify the signature.
|
||||||
|
:param include_alg:
|
||||||
|
By default, the signature parameters will include the ``alg`` parameter, using it to identify the signature
|
||||||
|
algorithm. If you wish not to include this parameter, set this to ``False``.
|
||||||
|
:param use_nonce:
|
||||||
|
Set this to ``True`` to include a unique message-specific nonce in the signature parameters. The format of
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
_digest_hashers = {"sha-256": hashlib.sha256, "sha-512": hashlib.sha512}
|
||||||
|
|
||||||
def __init__(self, *,
|
def __init__(self, *,
|
||||||
|
signature_algorithm: HTTPSignatureAlgorithm,
|
||||||
key: bytes = None,
|
key: bytes = None,
|
||||||
key_id: str,
|
key_id: str,
|
||||||
|
key_resolver: HTTPSignatureKeyResolver = None,
|
||||||
|
covered_component_ids: List[str] = ("@method", "@authority", "@target-uri"),
|
||||||
label: str = None,
|
label: str = None,
|
||||||
include_alg: bool = True,
|
include_alg: bool = True,
|
||||||
use_nonce: bool = False,
|
use_nonce: bool = False,
|
||||||
covered_component_ids: List[str] = ("@method", "@authority", "@target-uri"),
|
|
||||||
expires_in: datetime.timedelta = None,
|
expires_in: datetime.timedelta = None,
|
||||||
signature_algorithm: HTTPSignatureAlgorithm,
|
|
||||||
key_resolver: HTTPSignatureKeyResolver = None,
|
|
||||||
component_resolver_class: type = HTTPSignatureComponentResolver):
|
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.")
|
||||||
|
@ -68,14 +106,15 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
|
||||||
if "Date" not in request.headers:
|
if "Date" not in request.headers:
|
||||||
request.headers["Date"] = email.utils.formatdate(timestamp, usegmt=True)
|
request.headers["Date"] = email.utils.formatdate(timestamp, usegmt=True)
|
||||||
|
|
||||||
def add_digest(self, request):
|
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 and "Content-Digest" not in request.headers:
|
||||||
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"]
|
||||||
digest = self.hasher_constructor(request.body).digest()
|
hasher = self._digest_hashers[algorithm]
|
||||||
digest_node = http_sfv.Dictionary({self.hasher_name: digest})
|
digest = hasher(request.body).digest()
|
||||||
|
digest_node = http_sfv.Dictionary({algorithm: digest})
|
||||||
request.headers["Content-Digest"] = str(digest_node)
|
request.headers["Content-Digest"] = str(digest_node)
|
||||||
|
|
||||||
def get_nonce(self, request):
|
def get_nonce(self, request):
|
||||||
|
@ -111,10 +150,31 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
|
||||||
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.
|
||||||
|
|
||||||
|
:param signature_algorithm:
|
||||||
|
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
|
||||||
|
``requests_http_signature.algorithms.RSA_V1_5_SHA256``.
|
||||||
|
:param key_resolver:
|
||||||
|
Instead of specifying a fixed key, you can instead pass a key resolver, which should be an instance of a
|
||||||
|
subclass of ``http_message_signatures.HTTPSignatureKeyResolver``. A key resolver should have two methods,
|
||||||
|
``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.
|
||||||
|
"""
|
||||||
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)
|
||||||
verifier.verify(request)
|
verify_result = verifier.verify(request)
|
||||||
|
# TODO: get content-digest from verify result, not from independent parsing of headers
|
||||||
|
# TODO: add options to require specific components
|
||||||
headers = CaseInsensitiveDict(request.headers)
|
headers = CaseInsensitiveDict(request.headers)
|
||||||
if "content-digest" in headers:
|
if "content-digest" in headers:
|
||||||
if request.body is None:
|
if request.body is None:
|
||||||
|
@ -122,9 +182,11 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
|
||||||
digest = http_sfv.Dictionary()
|
digest = http_sfv.Dictionary()
|
||||||
digest.parse(headers["content-digest"].encode())
|
digest.parse(headers["content-digest"].encode())
|
||||||
for k, v in digest.items():
|
for k, v in digest.items():
|
||||||
if k != cls.hasher_name:
|
if k not in cls._digest_hashers:
|
||||||
raise InvalidSignature(f'Unsupported digest algorithm "{k}"')
|
raise InvalidSignature(f'Unsupported digest algorithm "{k}"')
|
||||||
raw_digest = v.value
|
raw_digest = v.value
|
||||||
expect_digest = cls.hasher_constructor(request.body).digest()
|
hasher = cls._digest_hashers[k]
|
||||||
if raw_digest != expect_digest:
|
expect_digest = hasher(request.body).digest()
|
||||||
raise InvalidSignature("The content-digest header does not match the request body")
|
if raw_digest != expect_digest:
|
||||||
|
raise InvalidSignature("The content-digest header does not match the request body")
|
||||||
|
return verify_result
|
||||||
|
|
Loading…
Reference in New Issue