Use max-age, auto-cover Authorization header

pull/34/head
Andrey Kislyuk 2022-04-14 19:18:19 -07:00
parent 6b5169c898
commit 7ed5b7a022
No known key found for this signature in database
GPG Key ID: 8AFAFCD242818A52
3 changed files with 33 additions and 17 deletions

View File

@ -10,6 +10,7 @@ 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):
@ -54,9 +55,11 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
verifying). Your implementation should ensure that the key id is recognized and return the corresponding 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). key material as PEM bytes (or shared secret bytes for HMAC).
:param covered_component_ids: :param covered_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 "@request-response" as ``@scheme``, ``@request-target``, ``@path``, ``@query``, ``@query-params``, ``@status``, or
specified in the standard) to sign. ``@request-response``, as specified in the standard) to sign. By default, ``@method``, ``@authority``,
and ``@target-uri`` are covered, and the ``Authorization``, ``Content-Digest``, and ``Date`` header fields
are always covered if present.
:param label: The label to use to identify the signature. :param label: The label to use to identify the signature.
:param include_alg: :param include_alg:
By default, the signature parameters will include the ``alg`` parameter, using it to identify the signature By default, the signature parameters will include the ``alg`` parameter, using it to identify the signature
@ -75,6 +78,7 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
""" """
_digest_hashers = {"sha-256": hashlib.sha256, "sha-512": hashlib.sha512} _digest_hashers = {"sha-256": hashlib.sha256, "sha-512": hashlib.sha512}
_auto_cover_header_fields = {"authorization", "content-digest", "date"}
def __init__(self, *, def __init__(self, *,
signature_algorithm: HTTPSignatureAlgorithm, signature_algorithm: HTTPSignatureAlgorithm,
@ -111,8 +115,6 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
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 self.covered_component_ids:
self.covered_component_ids = list(self.covered_component_ids) + ["content-digest"]
if "Content-Digest" not in request.headers: if "Content-Digest" not in request.headers:
hasher = self._digest_hashers[algorithm] hasher = self._digest_hashers[algorithm]
digest = hasher(request.body).digest() digest = hasher(request.body).digest()
@ -126,17 +128,25 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
def get_created(self, request): def get_created(self, request):
created = datetime.datetime.now() created = datetime.datetime.now()
self.add_date(request, timestamp=int(created.timestamp())) self.add_date(request, timestamp=int(created.timestamp()))
# TODO: add Date to covered components
return created return created
def get_expires(self, request, created): def get_expires(self, request, created):
if self.expires_in: if self.expires_in:
return datetime.datetime.now() + self.expires_in return datetime.datetime.now() + self.expires_in
def get_covered_component_ids(self, request):
covered_component_ids = CaseInsensitiveDict((k, None) for k in self.covered_component_ids)
headers = CaseInsensitiveDict(request.headers)
for header in self._auto_cover_header_fields:
if header in headers:
covered_component_ids.setdefault(header, None)
return list(covered_component_ids)
def __call__(self, request): def __call__(self, request):
self.add_digest(request) self.add_digest(request)
created = self.get_created(request) created = self.get_created(request)
expires = self.get_expires(request, created=created) expires = self.get_expires(request, created=created)
covered_component_ids = self.get_covered_component_ids(request)
self.signer.sign(request, self.signer.sign(request,
key_id=self.key_id, key_id=self.key_id,
created=created, created=created,
@ -144,7 +154,7 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
nonce=self.get_nonce(request), nonce=self.get_nonce(request),
label=self.label, label=self.label,
include_alg=self.include_alg, include_alg=self.include_alg,
covered_component_ids=self.covered_component_ids) covered_component_ids=covered_component_ids)
return request return request
@classmethod @classmethod
@ -157,7 +167,8 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
def verify(cls, message: Union[requests.PreparedRequest, requests.Response], *, def verify(cls, message: Union[requests.PreparedRequest, requests.Response], *,
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):
""" """
Verify an HTTP message signature. Verify an HTTP message signature.
@ -181,11 +192,12 @@ 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 ("@method", "@target-uri", "@authority", A list of lowercased header names or derived component IDs (
"@scheme", "@request-target", "@path", "@query", "@query-params", "@status", or "@request-response" as A list of lowercased header names or derived component IDs (``@method``, ``@target-uri``, ``@authority``,
specified in the standard) to require to be covered by the signature. If the "content-digest" header field ``@scheme``, ``@request-target``, ``@path``, ``@query``, ``@query-params``, ``@status``, or
is specified here (recommended for messages that have a body), it will be verified by matching it against ``@request-response``, as specified in the standard) to require to be covered by the signature. If the
the digest hash computed on the body of the message (expected to be bytes). "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") 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
@ -203,6 +215,8 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
``get_private_key(key_id)`` (required only for signing) and ``get_public_key(key_id)`` (required only for ``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 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). key material as PEM bytes (or shared secret bytes for HMAC).
:param max_age:
The maximum age of the signature, defined as the difference between the ``created`` parameter value and now.
:returns: *VerifyResult*, a namedtuple with the following attributes: :returns: *VerifyResult*, a namedtuple with the following attributes:
@ -225,7 +239,7 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
verifier = HTTPMessageVerifier(signature_algorithm=signature_algorithm, verifier = HTTPMessageVerifier(signature_algorithm=signature_algorithm,
key_resolver=key_resolver, key_resolver=key_resolver,
component_resolver_class=cls.component_resolver_class) component_resolver_class=cls.component_resolver_class)
verify_results = verifier.verify(message) verify_results = verifier.verify(message, max_age=max_age)
if len(verify_results) != 1: if len(verify_results) != 1:
raise InvalidSignature("Multiple signatures are not supported.") raise InvalidSignature("Multiple signatures are not supported.")
verify_result = verify_results[0] verify_result = verify_results[0]

View File

@ -16,7 +16,7 @@ setup(
}, },
setup_requires=['setuptools_scm >= 3.4.3'], setup_requires=['setuptools_scm >= 3.4.3'],
install_requires=[ install_requires=[
"http-message-signatures >= 0.2.3", "http-message-signatures >= 0.4.0",
"http-sfv >= 0.9.3", "http-sfv >= 0.9.3",
"requests >= 2.27.1" "requests >= 2.27.1"
], ],

View File

@ -88,8 +88,10 @@ class TestRequestsHTTPSignature(unittest.TestCase):
with self.assertRaises(InvalidSignature): with self.assertRaises(InvalidSignature):
HTTPSignatureAuth.verify(res, **verify_args) HTTPSignatureAuth.verify(res, **verify_args)
def test_expired_signature(self): def test_auto_cover_authorization_header(self):
"TODO" url = 'http://example.com/path?query#fragment'
res = self.session.get(url, auth=self.auth, headers={"Authorization": "Bearer 12345"})
self.assertIn('"authorization"', res.headers["Received-Signature-Input"])
def test_b21(self): def test_b21(self):
url = 'https://example.com/foo?param=Value&Pet=dog' url = 'https://example.com/foo?param=Value&Pet=dog'