Skip to content

Commit 7ed5b7a

Browse files
committed
Use max-age, auto-cover Authorization header
1 parent 6b5169c commit 7ed5b7a

File tree

3 files changed

+33
-17
lines changed

3 files changed

+33
-17
lines changed

requests_http_signature/__init__.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from requests.exceptions import RequestException
1111
from http_message_signatures import (algorithms, HTTPSignatureComponentResolver, HTTPSignatureKeyResolver, # noqa: F401
1212
HTTPMessageSigner, HTTPMessageVerifier, HTTPSignatureAlgorithm, InvalidSignature)
13+
from http_message_signatures.structures import CaseInsensitiveDict
1314

1415

1516
class RequestsHttpSignatureException(RequestException):
@@ -54,9 +55,11 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
5455
verifying). Your implementation should ensure that the key id is recognized and return the corresponding
5556
key material as PEM bytes (or shared secret bytes for HMAC).
5657
:param covered_component_ids:
57-
A list of lowercased header names or derived component IDs ("@method", "@target-uri", "@authority",
58-
"@scheme", "@request-target", "@path", "@query", "@query-params", "@status", or "@request-response" as
59-
specified in the standard) to sign.
58+
A list of lowercased header names or derived component IDs (``@method``, ``@target-uri``, ``@authority``,
59+
``@scheme``, ``@request-target``, ``@path``, ``@query``, ``@query-params``, ``@status``, or
60+
``@request-response``, as specified in the standard) to sign. By default, ``@method``, ``@authority``,
61+
and ``@target-uri`` are covered, and the ``Authorization``, ``Content-Digest``, and ``Date`` header fields
62+
are always covered if present.
6063
:param label: The label to use to identify the signature.
6164
:param include_alg:
6265
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):
7578
"""
7679

7780
_digest_hashers = {"sha-256": hashlib.sha256, "sha-512": hashlib.sha512}
81+
_auto_cover_header_fields = {"authorization", "content-digest", "date"}
7882

7983
def __init__(self, *,
8084
signature_algorithm: HTTPSignatureAlgorithm,
@@ -111,8 +115,6 @@ def add_digest(self, request, algorithm="sha-256"):
111115
if request.body is None and "content-digest" in self.covered_component_ids:
112116
raise RequestsHttpSignatureException("Could not compute digest header for request without a body")
113117
if request.body is not None:
114-
if "content-digest" not in self.covered_component_ids:
115-
self.covered_component_ids = list(self.covered_component_ids) + ["content-digest"]
116118
if "Content-Digest" not in request.headers:
117119
hasher = self._digest_hashers[algorithm]
118120
digest = hasher(request.body).digest()
@@ -126,25 +128,33 @@ def get_nonce(self, request):
126128
def get_created(self, request):
127129
created = datetime.datetime.now()
128130
self.add_date(request, timestamp=int(created.timestamp()))
129-
# TODO: add Date to covered components
130131
return created
131132

132133
def get_expires(self, request, created):
133134
if self.expires_in:
134135
return datetime.datetime.now() + self.expires_in
135136

137+
def get_covered_component_ids(self, request):
138+
covered_component_ids = CaseInsensitiveDict((k, None) for k in self.covered_component_ids)
139+
headers = CaseInsensitiveDict(request.headers)
140+
for header in self._auto_cover_header_fields:
141+
if header in headers:
142+
covered_component_ids.setdefault(header, None)
143+
return list(covered_component_ids)
144+
136145
def __call__(self, request):
137146
self.add_digest(request)
138147
created = self.get_created(request)
139148
expires = self.get_expires(request, created=created)
149+
covered_component_ids = self.get_covered_component_ids(request)
140150
self.signer.sign(request,
141151
key_id=self.key_id,
142152
created=created,
143153
expires=expires,
144154
nonce=self.get_nonce(request),
145155
label=self.label,
146156
include_alg=self.include_alg,
147-
covered_component_ids=self.covered_component_ids)
157+
covered_component_ids=covered_component_ids)
148158
return request
149159

150160
@classmethod
@@ -157,7 +167,8 @@ def get_body(cls, message):
157167
def verify(cls, message: Union[requests.PreparedRequest, requests.Response], *,
158168
require_components: List[str] = ("@method", "@authority", "@target-uri"),
159169
signature_algorithm: HTTPSignatureAlgorithm,
160-
key_resolver: HTTPSignatureKeyResolver):
170+
key_resolver: HTTPSignatureKeyResolver,
171+
max_age: datetime.timedelta = None):
161172
"""
162173
Verify an HTTP message signature.
163174
@@ -181,11 +192,12 @@ def verify(cls, message: Union[requests.PreparedRequest, requests.Response], *,
181192
HTTPSignatureAuth.verify(prepared_request, ...)
182193
183194
:param require_components:
184-
A list of lowercased header names or derived component IDs ("@method", "@target-uri", "@authority",
185-
"@scheme", "@request-target", "@path", "@query", "@query-params", "@status", or "@request-response" as
186-
specified in the standard) to require to be covered by the signature. If the "content-digest" header field
187-
is specified here (recommended for messages that have a body), it will be verified by matching it against
188-
the digest hash computed on the body of the message (expected to be bytes).
195+
A list of lowercased header names or derived component IDs (
196+
A list of lowercased header names or derived component IDs (``@method``, ``@target-uri``, ``@authority``,
197+
``@scheme``, ``@request-target``, ``@path``, ``@query``, ``@query-params``, ``@status``, or
198+
``@request-response``, as specified in the standard) to require to be covered by the signature. If the
199+
"content-digest" header field is specified here (recommended for messages that have a body), it will be
200+
verified by matching it against the digest hash computed on the body of the message (expected to be bytes).
189201
190202
If this parameter is not specified, ``verify()`` will set it to ("@method", "@authority", "@target-uri")
191203
for messages without a body, and ("@method", "@authority", "@target-uri", "content-digest") for messages
@@ -203,6 +215,8 @@ def verify(cls, message: Union[requests.PreparedRequest, requests.Response], *,
203215
``get_private_key(key_id)`` (required only for signing) and ``get_public_key(key_id)`` (required only for
204216
verifying). Your implementation should ensure that the key id is recognized and return the corresponding
205217
key material as PEM bytes (or shared secret bytes for HMAC).
218+
:param max_age:
219+
The maximum age of the signature, defined as the difference between the ``created`` parameter value and now.
206220
207221
:returns: *VerifyResult*, a namedtuple with the following attributes:
208222
@@ -225,7 +239,7 @@ def verify(cls, message: Union[requests.PreparedRequest, requests.Response], *,
225239
verifier = HTTPMessageVerifier(signature_algorithm=signature_algorithm,
226240
key_resolver=key_resolver,
227241
component_resolver_class=cls.component_resolver_class)
228-
verify_results = verifier.verify(message)
242+
verify_results = verifier.verify(message, max_age=max_age)
229243
if len(verify_results) != 1:
230244
raise InvalidSignature("Multiple signatures are not supported.")
231245
verify_result = verify_results[0]

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
},
1717
setup_requires=['setuptools_scm >= 3.4.3'],
1818
install_requires=[
19-
"http-message-signatures >= 0.2.3",
19+
"http-message-signatures >= 0.4.0",
2020
"http-sfv >= 0.9.3",
2121
"requests >= 2.27.1"
2222
],

test/test.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,10 @@ def test_basic_statements(self):
8888
with self.assertRaises(InvalidSignature):
8989
HTTPSignatureAuth.verify(res, **verify_args)
9090

91-
def test_expired_signature(self):
92-
"TODO"
91+
def test_auto_cover_authorization_header(self):
92+
url = 'http://example.com/path?query#fragment'
93+
res = self.session.get(url, auth=self.auth, headers={"Authorization": "Bearer 12345"})
94+
self.assertIn('"authorization"', res.headers["Received-Signature-Input"])
9395

9496
def test_b21(self):
9597
url = 'https://example.com/foo?param=Value&Pet=dog'

0 commit comments

Comments
 (0)