diff --git a/docs/conf.py b/docs/conf.py index fa6d554..5c2a18e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,8 @@ +import os, sys import guzzle_sphinx_theme +sys.path.insert(0, os.path.abspath('..')) + project = "requests-http-signature" copyright = "Andrey Kislyuk" author = "Andrey Kislyuk" diff --git a/docs/index.rst b/docs/index.rst index 82e6a78..9d2beb2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,7 +5,6 @@ API documentation .. automodule:: requests_http_signature :members: - :special-members: .. toctree:: :maxdepth: 2 diff --git a/requests_http_signature/__init__.py b/requests_http_signature/__init__.py index 2b52c69..3949672 100644 --- a/requests_http_signature/__init__.py +++ b/requests_http_signature/__init__.py @@ -32,19 +32,57 @@ class SingleKeyResolver(HTTPSignatureKeyResolver): class HTTPSignatureAuth(requests.auth.AuthBase): - hasher_name = "sha-256" - hasher_constructor = hashlib.sha256 + """ + A `Requests `_ `authentication plugin + `_ (``requests.auth.AuthBase`` subclass) + implementing the `IETF HTTP Message Signatures draft RFC + `_. + + :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, *, + signature_algorithm: HTTPSignatureAlgorithm, key: bytes = None, key_id: str, + key_resolver: HTTPSignatureKeyResolver = None, + covered_component_ids: List[str] = ("@method", "@authority", "@target-uri"), label: str = None, include_alg: bool = True, use_nonce: bool = False, - covered_component_ids: List[str] = ("@method", "@authority", "@target-uri"), expires_in: datetime.timedelta = None, - signature_algorithm: HTTPSignatureAlgorithm, - key_resolver: HTTPSignatureKeyResolver = None, component_resolver_class: type = HTTPSignatureComponentResolver): if key_resolver is None and key is None: 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: 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: 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 "content-digest" not in self.covered_component_ids: self.covered_component_ids = list(self.covered_component_ids) + ["content-digest"] - digest = self.hasher_constructor(request.body).digest() - digest_node = http_sfv.Dictionary({self.hasher_name: 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) def get_nonce(self, request): @@ -111,10 +150,31 @@ class HTTPSignatureAuth(requests.auth.AuthBase): signature_algorithm: HTTPSignatureAlgorithm, key_resolver: HTTPSignatureKeyResolver, 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, key_resolver=key_resolver, 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) if "content-digest" in headers: if request.body is None: @@ -122,9 +182,11 @@ class HTTPSignatureAuth(requests.auth.AuthBase): digest = http_sfv.Dictionary() digest.parse(headers["content-digest"].encode()) 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}"') raw_digest = v.value - expect_digest = cls.hasher_constructor(request.body).digest() - if raw_digest != expect_digest: - raise InvalidSignature("The content-digest header does not match the request body") + 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