Skip to content

Commit c1acb39

Browse files
committed
Pass through verified body in VerifiedRequest
1 parent 5d029f4 commit c1acb39

File tree

4 files changed

+53
-26
lines changed

4 files changed

+53
-26
lines changed

README.rst

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,22 @@ constructor as bytes in the PEM format, or configure the key resolver as follows
8585
auth = HTTPSignatureAuth(algorithm=algorithms.RSA_V1_5_SHA256, key=fh.read(), key_resolver=MyKeyResolver())
8686
requests.get(url, auth=auth)
8787
88+
Digest algorithms
89+
~~~~~~~~~~~~~~~~~
90+
The library supports SHA-512 digests via subclassing::
91+
class MySigner(HTTPSignatureAuth):
92+
def add_digest(self, request):
93+
super().add_digest(request, algorithm="sha-512")
94+
8895
Links
8996
-----
90-
* `IETF HTTP Signatures draft <https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures>`_
91-
* `http-message-signatures <https://github.com/pyauth/http-message-signatures>`_ - a dependency of this library that
92-
handles much of the implementation
9397
* `Project home page (GitHub) <https://github.com/pyauth/requests-http-signature>`_
9498
* `Package documentation <https://pyauth.github.io/requests-http-signature/>`_
9599
* `Package distribution (PyPI) <https://pypi.python.org/pypi/requests-http-signature>`_
96100
* `Change log <https://github.com/pyauth/requests-http-signature/blob/master/Changes.rst>`_
101+
* `http-message-signatures <https://github.com/pyauth/http-message-signatures>`_ - a dependency of this library that
102+
handles much of the implementation
103+
* `IETF HTTP Signatures draft <https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures>`_
97104

98105
Bugs
99106
~~~~

requests_http_signature/__init__.py

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,14 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
6666
the nonce can be controlled by subclassing this class and overloading the ``get_nonce()`` method.
6767
:param expires_in:
6868
Use this to set the ``expires`` signature parameter to the time of signing plus the given timedelta.
69-
:param component_resolver_class:
70-
Use this to subclass ``http_message_signatures.HTTPSignatureComponentResolver`` and customize header and
71-
derived component retrieval if needed.
7269
"""
70+
71+
component_resolver_class: type = HTTPSignatureComponentResolver
72+
"""
73+
A subclass of ``http_message_signatures.HTTPSignatureComponentResolver`` can be used to override this value
74+
to customize the retrieval of header and derived component values if needed.
75+
"""
76+
7377
_digest_hashers = {"sha-256": hashlib.sha256, "sha-512": hashlib.sha512}
7478

7579
def __init__(self, *,
@@ -81,8 +85,7 @@ def __init__(self, *,
8185
label: str = None,
8286
include_alg: bool = True,
8387
use_nonce: bool = False,
84-
expires_in: datetime.timedelta = None,
85-
component_resolver_class: type = HTTPSignatureComponentResolver):
88+
expires_in: datetime.timedelta = None):
8689
if key_resolver is None and key is None:
8790
raise RequestsHttpSignatureException("Either key_resolver or key must be specified.")
8891
if key_resolver is not None and key is not None:
@@ -96,10 +99,9 @@ def __init__(self, *,
9699
self.use_nonce = use_nonce
97100
self.covered_component_ids = covered_component_ids
98101
self.expires_in = expires_in
99-
handler_args = dict(signature_algorithm=signature_algorithm,
100-
key_resolver=key_resolver,
101-
component_resolver_class=component_resolver_class)
102-
self.signer = HTTPMessageSigner(**handler_args)
102+
self.signer = HTTPMessageSigner(signature_algorithm=signature_algorithm,
103+
key_resolver=key_resolver,
104+
component_resolver_class=self.component_resolver_class)
103105

104106
def add_date(self, request, timestamp):
105107
if "Date" not in request.headers:
@@ -108,13 +110,14 @@ def add_date(self, request, timestamp):
108110
def add_digest(self, request, algorithm="sha-256"):
109111
if request.body is None and "content-digest" in self.covered_component_ids:
110112
raise RequestsHttpSignatureException("Could not compute digest header for request without a body")
111-
if request.body is not None and "Content-Digest" not in request.headers:
113+
if request.body is not None:
112114
if "content-digest" not in self.covered_component_ids:
113115
self.covered_component_ids = list(self.covered_component_ids) + ["content-digest"]
114-
hasher = self._digest_hashers[algorithm]
115-
digest = hasher(request.body).digest()
116-
digest_node = http_sfv.Dictionary({algorithm: digest})
117-
request.headers["Content-Digest"] = str(digest_node)
116+
if "Content-Digest" not in request.headers:
117+
hasher = self._digest_hashers[algorithm]
118+
digest = hasher(request.body).digest()
119+
digest_node = http_sfv.Dictionary({algorithm: digest})
120+
request.headers["Content-Digest"] = str(digest_node)
118121

119122
def get_nonce(self, request):
120123
if self.use_nonce:
@@ -148,8 +151,7 @@ def __call__(self, request):
148151
def verify(cls, request, *,
149152
require_components: List[str] = ("@method", "@authority", "@target-uri"),
150153
signature_algorithm: HTTPSignatureAlgorithm,
151-
key_resolver: HTTPSignatureKeyResolver,
152-
component_resolver_class: type = HTTPSignatureComponentResolver):
154+
key_resolver: HTTPSignatureKeyResolver):
153155
"""
154156
Verify an HTTP message signature.
155157
@@ -176,8 +178,8 @@ def verify(cls, request, *,
176178
for requests without a body, and ("@method", "@authority", "@target-uri", "content-digest") for requests
177179
with a body.
178180
:param signature_algorithm:
179-
The algorithm expected to be used by the signature. Any signature not using the expected algorithm will be
180-
rejected. One of ``requests_http_signature.algorithms.HMAC_SHA256``,
181+
The algorithm expected to be used by the signature. Any signature not using the expected algorithm will
182+
cause an ``InvalidSignature`` exception. Must be one of ``requests_http_signature.algorithms.HMAC_SHA256``,
181183
``requests_http_signature.algorithms.ECDSA_P256_SHA256``,
182184
``requests_http_signature.algorithms.ED25519``,
183185
``requests_http_signature.algorithms.RSA_PSS_SHA512``, or
@@ -188,16 +190,15 @@ def verify(cls, request, *,
188190
``get_private_key(key_id)`` (required only for signing) and ``get_public_key(key_id)`` (required only for
189191
verifying). Your implementation should ensure that the key id is recognized and return the corresponding
190192
key material as PEM bytes (or shared secret bytes for HMAC).
191-
:param component_resolver_class:
192-
Use this to subclass ``http_message_signatures.HTTPSignatureComponentResolver`` and customize header and
193-
derived component retrieval if needed.
194193
195194
:returns: *VerifyResult*, a namedtuple with the following attributes:
196195
197196
* ``label`` (str): The label for the signature
198197
* ``algorithm``: (same as ``signature_algorithm`` above)
199198
* ``covered_components``: A mapping of component names to their values, as covered by the signature
200199
* ``parameters``: A mapping of signature parameters to their values, as covered by the signature
200+
* ``body``: The message body for requests that have a body and pass validation of the covered
201+
content-digest; ``None`` otherwise.
201202
202203
:raises: ``InvalidSignature`` - raised whenever signature validation fails for any reason.
203204
"""
@@ -207,7 +208,7 @@ def verify(cls, request, *,
207208

208209
verifier = HTTPMessageVerifier(signature_algorithm=signature_algorithm,
209210
key_resolver=key_resolver,
210-
component_resolver_class=component_resolver_class)
211+
component_resolver_class=cls.component_resolver_class)
211212
verify_results = verifier.verify(request)
212213
if len(verify_results) != 1:
213214
raise InvalidSignature("Multiple signatures are not supported.")
@@ -223,6 +224,8 @@ def verify(cls, request, *,
223224
raise InvalidSignature("Found a content-digest header in a request with no body")
224225
digest = http_sfv.Dictionary()
225226
digest.parse(verify_result.covered_components[component_key].encode())
227+
if len(digest) < 1:
228+
raise InvalidSignature("Found a content-digest header with no digests")
226229
for k, v in digest.items():
227230
if k not in cls._digest_hashers:
228231
raise InvalidSignature(f'Unsupported digest algorithm "{k}"')
@@ -231,4 +234,5 @@ def verify(cls, request, *,
231234
expect_digest = hasher(request.body).digest()
232235
if raw_digest != expect_digest:
233236
raise InvalidSignature("The content-digest header does not match the request body")
237+
verify_result = verify_result._replace(body=request.body)
234238
return verify_result

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.1",
19+
"http-message-signatures >= 0.2.2",
2020
"http-sfv >= 0.9.3",
2121
"requests >= 2.27.1"
2222
],

test/test.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ def send(self, request, *args, **kwargs):
3636
response = requests.Response()
3737
response.status_code = requests.codes.ok
3838
response.url = request.url
39+
response.headers["Received-Signature-Input"] = request.headers["Signature-Input"]
40+
response.headers["Received-Signature"] = request.headers["Signature"]
3941
return response
4042

4143

@@ -49,6 +51,7 @@ def setUp(self):
4951
self.session = requests.Session()
5052
self.auth = HTTPSignatureAuth(key_id=default_keyid, key=hmac_secret, signature_algorithm=algorithms.HMAC_SHA256)
5153
self.session.mount("http://", TestAdapter(self.auth))
54+
self.session.mount("https://", TestAdapter(self.auth))
5255

5356
def test_basic_statements(self):
5457
url = 'http://example.com/path?query#fragment'
@@ -63,6 +66,19 @@ def test_basic_statements(self):
6366
def test_expired_signature(self):
6467
"TODO"
6568

69+
def test_b21(self):
70+
url = 'https://example.com/foo?param=Value&Pet=dog'
71+
self.session.post(
72+
url,
73+
json={"hello": "world"},
74+
headers={
75+
"Date": "Tue, 20 Apr 2021 02:07:55 GMT",
76+
"Content-Digest": ("sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+"
77+
"AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:")
78+
},
79+
auth=self.auth
80+
)
81+
6682
@unittest.skip("TODO")
6783
def test_rsa(self):
6884
from cryptography.hazmat.backends import default_backend

0 commit comments

Comments
 (0)