Compare commits

...

6 Commits

Author SHA1 Message Date
Andrey Kislyuk 69f17689f5
Mention that Authorization header is signed 2022-04-15 00:08:46 -07:00
Andrey Kislyuk 7fd0077c58
Expand verify example 2022-04-15 00:01:39 -07:00
Andrey Kislyuk d8b7916639
Clarify naming 2022-04-14 23:54:34 -07:00
Andrey Kislyuk adecf44d46
Use max-age=24h by default 2022-04-14 23:52:20 -07:00
Andrey Kislyuk e3e7c0f4c7
Use a class variable to set content digest alg 2022-04-14 23:48:53 -07:00
Andrey Kislyuk b68a6e7db4
Use max-age=36h by default 2022-04-14 23:33:05 -07:00
3 changed files with 25 additions and 22 deletions

View File

@ -28,9 +28,9 @@ Usage
requests.get(url, auth=auth)
By default, only the ``Date`` header and the ``@method``, ``@authority``, and ``@target-uri`` derived component
identifiers are signed for body-less requests such as GET. The ``Date`` header is set if it is absent. In addition, for
requests with bodies (such as POST), the ``Content-Digest`` header is set to the SHA256 of the request body using the
format described in the
identifiers are signed for body-less requests such as GET. The ``Date`` header is set if it is absent. In addition,
the ``Authorization`` header is signed if it is present, and for requests with bodies (such as POST), the
``Content-Digest`` header is set to the SHA256 of the request body using the format described in the
`IETF Digest Fields draft RFC <https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-digest-headers>`_ and signed.
To add other headers to the signature, pass an array of header names in the ``covered_component_ids`` keyword argument.
See the `API documentation <https://pyauth.github.io/requests-http-signature/#id3>`_ for the full list of options and
@ -48,9 +48,10 @@ The class method ``HTTPSignatureAuth.verify()`` can be used to verify responses
return 'monorail_cat'
response = requests.get(url, auth=auth)
HTTPSignatureAuth.verify(response,
signature_algorithm=algorithms.HMAC_SHA256,
key_resolver=MyKeyResolver())
verify_result = HTTPSignatureAuth.verify(response,
signature_algorithm=algorithms.HMAC_SHA256,
key_resolver=MyKeyResolver())
# To avoid substitution attacks, only trust response data referenced by verify_result
More generally, you can reconstruct an arbitrary request using the
`Requests API <https://docs.python-requests.org/en/latest/api/#requests.Request>`_ and pass it to ``verify()``:
@ -112,11 +113,11 @@ constructor as bytes in the PEM format, or configure the key resolver as follows
Digest algorithms
~~~~~~~~~~~~~~~~~
If you need to generate a Content-Digest header using SHA-512, you can do so via subclassing::
To generate a Content-Digest header using SHA-512 instead of the default SHA-256, subclass ``HTTPSignatureAuth`` as
follows::
class MySigner(HTTPSignatureAuth):
def add_digest(self, request):
super().add_digest(request, algorithm="sha-512")
signing_content_digest_hasher = "sha-512"
Links
-----

View File

@ -77,7 +77,10 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
to customize the retrieval of header and derived component values if needed.
"""
_digest_hashers = {"sha-256": hashlib.sha256, "sha-512": hashlib.sha512}
_content_digest_hashers = {"sha-256": hashlib.sha256, "sha-512": hashlib.sha512}
signing_content_digest_hasher = "sha-256"
"The hash algorithm to use to generate the Content-Digest header field (either ``sha-256`` or ``sha-512``)."
_auto_cover_header_fields = {"authorization", "content-digest", "date"}
def __init__(self, *,
@ -111,14 +114,14 @@ 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, algorithm="sha-256"):
def add_digest(self, request):
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:
if "Content-Digest" not in request.headers:
hasher = self._digest_hashers[algorithm]
hasher = self._content_digest_hashers[self.signing_content_digest_hasher]
digest = hasher(request.body).digest()
digest_node = http_sfv.Dictionary({algorithm: digest})
digest_node = http_sfv.Dictionary({self.signing_content_digest_hasher: digest})
request.headers["Content-Digest"] = str(digest_node)
def get_nonce(self, request):
@ -168,7 +171,7 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
require_components: List[str] = ("@method", "@authority", "@target-uri"),
signature_algorithm: HTTPSignatureAlgorithm,
key_resolver: HTTPSignatureKeyResolver,
max_age: datetime.timedelta = None):
max_age: datetime.timedelta = datetime.timedelta(days=1)):
"""
Verify an HTTP message signature.
@ -192,15 +195,14 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
HTTPSignatureAuth.verify(prepared_request, ...)
:param require_components:
A list of lowercased header names or derived 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 require to be covered by the signature. If the
"content-digest" header field is specified here (recommended for messages that have a body), it will be
``content-digest`` header field is specified here (recommended for messages that have a body), it will be
verified by matching it against the digest hash computed on the body of the message (expected to be bytes).
If this parameter is not specified, ``verify()`` will set it to ("@method", "@authority", "@target-uri")
for messages without a body, and ("@method", "@authority", "@target-uri", "content-digest") for messages
If this parameter is not specified, ``verify()`` will set it to ``("@method", "@authority", "@target-uri")``
for messages without a body, and ``("@method", "@authority", "@target-uri", "content-digest")`` for messages
with a body.
:param signature_algorithm:
The algorithm expected to be used by the signature. Any signature not using the expected algorithm will
@ -257,10 +259,10 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
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}"')
if k not in cls._content_digest_hashers:
raise InvalidSignature(f'Unsupported content digest algorithm "{k}"')
raw_digest = v.value
hasher = cls._digest_hashers[k]
hasher = cls._content_digest_hashers[k]
expect_digest = hasher(body).digest()
if raw_digest != expect_digest:
raise InvalidSignature("The content-digest header does not match the message body")

View File

@ -43,7 +43,7 @@ class TestAdapter(HTTPAdapter):
response.raw = io.BytesIO(json.dumps({}).encode())
signer = HTTPMessageSigner(signature_algorithm=self.client_auth.signer.signature_algorithm,
key_resolver=self.client_auth.signer.key_resolver)
hasher = HTTPSignatureAuth._digest_hashers["sha-256"]
hasher = HTTPSignatureAuth._content_digest_hashers["sha-256"]
digest = hasher(response.raw.getvalue()).digest()
response.headers["Content-Digest"] = str(http_sfv.Dictionary({"sha-256": digest}))
signer.sign(response,