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) requests.get(url, auth=auth)
By default, only the ``Date`` header and the ``@method``, ``@authority``, and ``@target-uri`` derived component 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 identifiers are signed for body-less requests such as GET. The ``Date`` header is set if it is absent. In addition,
requests with bodies (such as POST), the ``Content-Digest`` header is set to the SHA256 of the request body using the the ``Authorization`` header is signed if it is present, and for requests with bodies (such as POST), the
format described in 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. `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. 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 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' return 'monorail_cat'
response = requests.get(url, auth=auth) response = requests.get(url, auth=auth)
HTTPSignatureAuth.verify(response, verify_result = HTTPSignatureAuth.verify(response,
signature_algorithm=algorithms.HMAC_SHA256, signature_algorithm=algorithms.HMAC_SHA256,
key_resolver=MyKeyResolver()) 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 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()``: `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 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): class MySigner(HTTPSignatureAuth):
def add_digest(self, request): signing_content_digest_hasher = "sha-512"
super().add_digest(request, algorithm="sha-512")
Links Links
----- -----

View File

@ -77,7 +77,10 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
to customize the retrieval of header and derived component values if needed. 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"} _auto_cover_header_fields = {"authorization", "content-digest", "date"}
def __init__(self, *, def __init__(self, *,
@ -111,14 +114,14 @@ 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, algorithm="sha-256"): def add_digest(self, request):
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: if request.body is not None:
if "Content-Digest" not in request.headers: 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 = 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) request.headers["Content-Digest"] = str(digest_node)
def get_nonce(self, request): def get_nonce(self, request):
@ -168,7 +171,7 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
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,
max_age: datetime.timedelta = None): max_age: datetime.timedelta = datetime.timedelta(days=1)):
""" """
Verify an HTTP message signature. Verify an HTTP message signature.
@ -192,15 +195,14 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
HTTPSignatureAuth.verify(prepared_request, ...) HTTPSignatureAuth.verify(prepared_request, ...)
:param require_components: :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``, A list of lowercased header names or derived component IDs (``@method``, ``@target-uri``, ``@authority``,
``@scheme``, ``@request-target``, ``@path``, ``@query``, ``@query-params``, ``@status``, or ``@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 ``@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). 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") 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 for messages without a body, and ``("@method", "@authority", "@target-uri", "content-digest")`` for messages
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 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: if len(digest) < 1:
raise InvalidSignature("Found a content-digest header with no digests") 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._content_digest_hashers:
raise InvalidSignature(f'Unsupported digest algorithm "{k}"') raise InvalidSignature(f'Unsupported content digest algorithm "{k}"')
raw_digest = v.value raw_digest = v.value
hasher = cls._digest_hashers[k] hasher = cls._content_digest_hashers[k]
expect_digest = hasher(body).digest() expect_digest = hasher(body).digest()
if raw_digest != expect_digest: if raw_digest != expect_digest:
raise InvalidSignature("The content-digest header does not match the message body") 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()) response.raw = io.BytesIO(json.dumps({}).encode())
signer = HTTPMessageSigner(signature_algorithm=self.client_auth.signer.signature_algorithm, signer = HTTPMessageSigner(signature_algorithm=self.client_auth.signer.signature_algorithm,
key_resolver=self.client_auth.signer.key_resolver) 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() digest = hasher(response.raw.getvalue()).digest()
response.headers["Content-Digest"] = str(http_sfv.Dictionary({"sha-256": digest})) response.headers["Content-Digest"] = str(http_sfv.Dictionary({"sha-256": digest}))
signer.sign(response, signer.sign(response,