Use max-age, auto-cover Authorization header
parent
6b5169c898
commit
7ed5b7a022
|
@ -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]
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -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"
|
||||||
],
|
],
|
||||||
|
|
|
@ -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'
|
||||||
|
|
Loading…
Reference in New Issue