Skip to content

Commit b348b3f

Browse files
committed
Refactor multisig support
1 parent 6547dec commit b348b3f

File tree

4 files changed

+81
-27
lines changed

4 files changed

+81
-27
lines changed

README.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ http-message-signatures: An implementation of RFC 9421, the IETF HTTP Message Si
55
`RFC 9421 HTTP Message Signatures <https://datatracker.ietf.org/doc/rfc9421/>`_ standard in
66
Python.
77

8+
.. admonition:: Security considerations
9+
10+
It is recommended that you read and understand
11+
`section 7 of the RFC, Security Considerations <https://www.rfc-editor.org/rfc/rfc9421#name-security-considerations>`_
12+
before using this library.
13+
814
Installation
915
------------
1016
::

http_message_signatures/signatures.py

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -167,27 +167,29 @@ def validate_created_and_expires(self, sig_input, max_age=None):
167167
if self._parse_integer_timestamp(sig_input.params["created"], field_name="created") + max_age < min_time:
168168
raise InvalidSignature(f"Signature age exceeds maximum allowable age {max_age}")
169169

170-
def _get_sig_input_and_signature(self, message, expect_label):
170+
def _get_sig_inputs_and_signatures(self, message, *, expect_tag=None, expect_label=None):
171171
sig_inputs = self._parse_dict_header("Signature-Input", message.headers)
172172
signatures = self._parse_dict_header("Signature", message.headers)
173173
if len(sig_inputs) == 0 or len(signatures) == 0:
174174
raise InvalidSignature("No signatures found in the message")
175-
if (len(sig_inputs) > 1 or len(signatures) > 1) and expect_label is None:
176-
raise InvalidSignature("Multiple signatures found and no label specified")
175+
if (len(sig_inputs) > 1 or len(signatures) > 1) and expect_tag is None and expect_label is None:
176+
raise InvalidSignature("Multiple signatures found and no tag or label specified")
177177
if sig_inputs.keys() != signatures.keys():
178178
raise InvalidSignature("Signature-Input and Signature headers have different labels")
179+
n_sigs = 0
179180
for label, sig_input in sig_inputs.items():
180-
if expect_label is not None and label != expect_label:
181-
continue
182181
if label not in signatures:
183182
raise InvalidSignature(f'Signature missing expected label "{label}"')
184-
return label, sig_input, signatures[label]
185-
raise InvalidSignature(f'Signature-Input does not contain expected label "{expect_label}"')
183+
if expect_tag is not None and sig_input.params.get("tag") != expect_tag:
184+
continue
185+
if expect_label is not None and label != expect_label:
186+
continue
187+
yield label, sig_input, signatures[label]
188+
n_sigs += 1
189+
if n_sigs == 0:
190+
raise InvalidSignature("No signatures found matching the expected tag or label")
186191

187-
def verify(
188-
self, message, *, max_age: datetime.timedelta = datetime.timedelta(days=1), expect_label: str | None = None
189-
) -> List[VerifyResult]:
190-
label, sig_input, signature = self._get_sig_input_and_signature(message, expect_label)
192+
def _verify_one(self, *, label, sig_input, signature, message, max_age):
191193
self.validate_created_and_expires(sig_input, max_age=max_age)
192194
if "alg" in sig_input.params:
193195
if sig_input.params["alg"] != self.signature_algorithm.algorithm_id:
@@ -208,12 +210,28 @@ def verify(
208210
verifier.verify(signature=raw_signature, message=sig_base.encode())
209211
except Exception as e:
210212
raise InvalidSignature(e) from e
211-
return [
212-
VerifyResult(
213-
label=label,
214-
algorithm=self.signature_algorithm,
215-
covered_components=sig_elements,
216-
parameters=dict(sig_params_node.params),
217-
body=None,
213+
return VerifyResult(
214+
label=label,
215+
algorithm=self.signature_algorithm,
216+
covered_components=sig_elements,
217+
parameters=dict(sig_params_node.params),
218+
body=None,
219+
)
220+
221+
def verify(
222+
self,
223+
message,
224+
*,
225+
max_age: datetime.timedelta = datetime.timedelta(days=1),
226+
expect_tag: str | None = None,
227+
expect_label: str | None = None,
228+
) -> List[VerifyResult]:
229+
verify_results = []
230+
for label, sig_input, signature in self._get_sig_inputs_and_signatures(
231+
message, expect_tag=expect_tag, expect_label=expect_label
232+
):
233+
verify_result = self._verify_one(
234+
label=label, sig_input=sig_input, signature=signature, message=message, max_age=max_age
218235
)
219-
]
236+
verify_results.append(verify_result)
237+
return verify_results

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,6 @@ profile = "black"
5151

5252
[tool.ruff]
5353
line-length = 120
54+
55+
[lint]
56+
unfixable = ["F401"]

test/test.py

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import os
88
import sys
99
import unittest
10+
from contextlib import contextmanager
1011
from datetime import datetime, timedelta
12+
from unittest.mock import patch
1113

1214
import requests
1315
from cryptography.hazmat.primitives.serialization import (
@@ -36,6 +38,17 @@
3638
)
3739

3840

41+
@contextmanager
42+
def patch_time(dt):
43+
class MockDateTime(datetime):
44+
@classmethod
45+
def now(cls, tz=None):
46+
return dt
47+
48+
with patch("http_message_signatures.signatures.datetime.datetime", MockDateTime):
49+
yield
50+
51+
3952
class MyHTTPSignatureKeyResolver(HTTPSignatureKeyResolver):
4053
known_pem_keys = {"test-key-rsa", "test-key-rsa-pss", "test-key-ecc-p256", "test-key-ed25519"}
4154

@@ -77,20 +90,20 @@ def setUp(self):
7790
self.key_resolver = MyHTTPSignatureKeyResolver()
7891
self.max_age = timedelta(weeks=90000)
7992

80-
def verify(self, verifier, message, max_age=None, expect_label=None):
93+
def verify(self, verifier, message, max_age=None, expect_tag=None, expect_label=None):
8194
if max_age is None:
8295
max_age = self.max_age
8396
m = copy.deepcopy(message)
8497
m.headers["Signature"] = m.headers["Signature"][:8] + m.headers["Signature"][8:].upper()
8598
with self.assertRaises(InvalidSignature):
86-
verifier.verify(m, max_age=max_age, expect_label=expect_label)
99+
verifier.verify(m, max_age=max_age, expect_tag=expect_tag, expect_label=expect_label)
87100
m.headers["Signature"] = m.headers["Signature"].upper()
88101
with self.assertRaisesRegex(InvalidSignature, "Malformed structured header field"):
89-
verifier.verify(m, max_age=max_age, expect_label=expect_label)
102+
verifier.verify(m, max_age=max_age, expect_tag=expect_tag, expect_label=expect_label)
90103
del m.headers["Signature"]
91104
with self.assertRaisesRegex(InvalidSignature, 'Expected "Signature" header field to be present'):
92-
verifier.verify(m, max_age=max_age, expect_label=expect_label)
93-
return verifier.verify(message, max_age=max_age, expect_label=expect_label)
105+
verifier.verify(m, max_age=max_age, expect_tag=expect_tag, expect_label=expect_label)
106+
return verifier.verify(message, max_age=max_age, expect_tag=expect_tag, expect_label=expect_label)
94107

95108
def test_http_message_signatures_B21(self):
96109
signer = HTTPMessageSigner(signature_algorithm=RSA_PSS_SHA512, key_resolver=self.key_resolver)
@@ -345,6 +358,7 @@ def test_multiple_signatures(self):
345358
created=datetime.fromtimestamp(1618884480),
346359
expires=datetime.fromtimestamp(1618884540),
347360
label="proxy_sig",
361+
tag=None,
348362
append_if_signature_exists=False,
349363
)
350364
signer2 = HTTPMessageSigner(signature_algorithm=RSA_V1_5_SHA256, key_resolver=self.key_resolver)
@@ -363,13 +377,26 @@ def test_multiple_signatures(self):
363377
"sig1=:X5spyd6CFnAG5QnDyHfqoSNICd+BUP4LYMz2Q0JXlb//4Ijpzp+kve2w4NIyqeAuM7jTDX+sNalzA8ESSaHD3A==:, proxy_sig=:S6ZzPXSdAMOPjN/6KXfXWNO/f7V6cHm7BXYUh3YD/fRad4BCaRZxP+JH+8XY1I6+8Cy+CM5g92iHgxtRPz+MjniOaYmdkDcnL9cCpXJleXsOckpURl49GwiyUpZ10KHgOEe11sx3G2gxI8S0jnxQB+Pu68U9vVcasqOWAEObtNKKZd8tSFu7LB5YAv0RAGhB8tmpv7sFnIm9y+7X5kXQfi8NMaZaA8i2ZHwpBdg7a6CMfwnnrtflzvZdXAsD3LH2TwevU+/PBPv0B6NMNk93wUs/vfJvye+YuI87HU38lZHowtznbLVdp770I6VHR6WfgS9ddzirrswsE1w5o0LV/g==:",
364378
)
365379
self.assertIn("sig1", self.test_request.headers["Signature-Input"])
366-
with self.assertRaisesRegex(InvalidSignature, "Multiple signatures found and no label specified"):
380+
with self.assertRaisesRegex(InvalidSignature, "Multiple signatures found and no tag or label specified"):
367381
verifier.verify(self.test_request)
368382
verifier2 = HTTPMessageVerifier(signature_algorithm=RSA_V1_5_SHA256, key_resolver=self.key_resolver)
369383
with self.assertRaisesRegex(InvalidSignature, 'Signature "expires" parameter is set to a time in the past'):
370384
self.verify(verifier2, self.test_request, expect_label="proxy_sig")
371-
verifier2.validate_created_and_expires = lambda *args, **kwargs: None
372-
self.verify(verifier2, self.test_request, expect_label="proxy_sig")
385+
386+
with patch_time(datetime.fromtimestamp(1618884500)):
387+
res = self.verify(verifier2, self.test_request, expect_label="proxy_sig")
388+
self.assertEqual(len(res), 1)
389+
self.assertEqual(res[0].label, "proxy_sig")
390+
391+
signer2_args.update(label="my-label", tag="my-tag")
392+
signer2.sign(self.test_request, **signer2_args)
393+
with self.assertRaisesRegex(InvalidSignature, "No signatures found matching the expected tag or label"):
394+
self.verify(verifier2, self.test_request, expect_tag="test")
395+
with patch_time(datetime.fromtimestamp(1618884500)):
396+
res = self.verify(verifier2, self.test_request, expect_tag="my-tag")
397+
self.assertEqual(len(res), 1)
398+
self.assertEqual(res[0].label, "my-label")
399+
self.assertEqual(res[0].parameters["tag"], "my-tag")
373400

374401
def test_query_parameters(self):
375402
signer = HTTPMessageSigner(signature_algorithm=HMAC_SHA256, key_resolver=self.key_resolver)

0 commit comments

Comments
 (0)