Skip to content

Commit af4856a

Browse files
authored
Added fallback signers and switch back to sha1
2 parents 2ef8fe0 + 92a6423 commit af4856a

File tree

10 files changed

+128
-26
lines changed

10 files changed

+128
-26
lines changed

CHANGES.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
1+
Version 1.1.0
2+
-------------
3+
4+
Released 2018-10-26
5+
6+
- Change algorithm back to SHA-1
7+
- Add support for fallback algorithm
8+
- Changed capitalization of packages back to lowercase as the
9+
change in capitalization broke some tooling.
10+
111
Version 1.0.0
212
-------------
313

414
Released 2018-10-18
515

16+
YANKED
17+
18+
*Note*: this release was yanked from pypi because it changed the default
19+
algorithm to SHA-512. This decision was reverted and it remains at SHA1.
20+
621
- Drop support for Python 2.6 and 3.3.
722
- Refactor code from a single module to a package. Any object in the
823
API docs is still importable from the top-level ``itsdangerous``

README.rst

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Install and update using `pip`_:
1919

2020
.. code-block:: text
2121
22-
pip install -U ItsDangerous
22+
pip install -U itsdangerous
2323
2424
.. _pip: https://pip.pypa.io/en/stable/quickstart/
2525

@@ -33,13 +33,11 @@ name between web requests.
3333
.. code-block:: python
3434
3535
from itsdangerous import URLSafeSerializer
36-
3736
auth_s = URLSafeSerializer("secret key", "auth")
3837
token = auth_s.dumps({"id": 5, "name": "itsdangerous"})
3938
4039
print(token)
41-
# eyJpZCI6NSwibmFtZSI6Iml0c2Rhbmdlcm91cyJ9.AmSPrPa_iZ6q-ERXXdQxt6ce8NEqt
42-
# 3i2Uke3sIRnDG0riZD6OoqckqC72VJ9SBIu-vAf_XlwNHnt7dLEClT0JA
40+
# eyJpZCI6NSwibmFtZSI6Iml0c2Rhbmdlcm91cyJ9.6YP6T0BaO67XP--9UzTrmurXSmg
4341
4442
data = auth_s.loads(token)
4543
print(data["name"])

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
project = "It's Dangerous"
77
copyright = "2011 Pallets Team"
88
author = "Pallets Team"
9-
release, version = get_version("ItsDangerous")
9+
release, version = get_version("itsdangerous")
1010

1111
# General --------------------------------------------------------------
1212

@@ -22,7 +22,7 @@
2222
"project_links": [
2323
ProjectLink("Donate to Pallets", "https://palletsprojects.com/donate"),
2424
ProjectLink("Website", "https://palletsprojects.com/p/itsdangerous/"),
25-
ProjectLink("PyPI releases", "https://pypi.org/project/ItsDangerous/"),
25+
ProjectLink("PyPI releases", "https://pypi.org/project/itsdangerous/"),
2626
ProjectLink("Source Code", "https://github.com/pallets/itsdangerous/"),
2727
ProjectLink("Issue Tracker", "https://github.com/pallets/itsdangerous/issues/"),
2828
]

docs/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Install and update using `pip`_:
3030

3131
.. code-block:: text
3232
33-
pip install -U ItsDangerous
33+
pip install -U itsdangerous
3434
3535
.. _pip: https://pip.pypa.io/en/stable/quickstart/
3636

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
version = re.search(r"__version__ = \"(.*?)\"", f.read()).group(1)
1212

1313
setup(
14-
name="ItsDangerous",
14+
name="itsdangerous",
1515
version=version,
1616
url="https://palletsprojects.com/p/itsdangerous/",
1717
project_urls={

src/itsdangerous/serializer.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,29 @@ class to the constructor as well as keyword arguments as a dict that
3232
3333
s = Serializer(signer_kwargs={'key_derivation': 'hmac'})
3434
35+
Additionally as of 1.1 fallback signers can be defined by providing
36+
a list as `fallback_signers`. These are used for deserialization as a
37+
fallback. Each item can be one of the following:
38+
a signer class (which is instantiated with `signer_kwargs`, salt and
39+
secret key), a tuple `(signer_class, signer_kwargs)` or just `signer_kwargs`.
40+
If kwargs are provided they need to be a dict.
41+
42+
For instance this is a serializer that supports deserialization that
43+
supports both SHA1 and SHA512:
44+
45+
.. code-block:: python3
46+
47+
s = Serializer(
48+
signer_kwargs={'digest_method': hashlib.sha512},
49+
fallback_signers=[{'digest_method': hashlib.sha1}]
50+
)
51+
3552
.. versionchanged:: 0.14:
3653
The ``signer`` and ``signer_kwargs`` parameters were added to
3754
the constructor.
55+
56+
.. versionchanged:: 1.1:
57+
Added support for `fallback_signers`.
3858
"""
3959

4060
#: If a serializer module or class is not passed to the constructor
@@ -55,6 +75,7 @@ def __init__(
5575
serializer_kwargs=None,
5676
signer=None,
5777
signer_kwargs=None,
78+
fallback_signers=None,
5879
):
5980
self.secret_key = want_bytes(secret_key)
6081
self.salt = want_bytes(salt)
@@ -66,6 +87,7 @@ def __init__(
6687
signer = self.default_signer
6788
self.signer = signer
6889
self.signer_kwargs = signer_kwargs or {}
90+
self.fallback_signers = fallback_signers or ()
6991
self.serializer_kwargs = serializer_kwargs or {}
7092

7193
def load_payload(self, payload, serializer=None):
@@ -106,6 +128,21 @@ def make_signer(self, salt=None):
106128
salt = self.salt
107129
return self.signer(self.secret_key, salt=salt, **self.signer_kwargs)
108130

131+
def iter_unsigners(self, salt=None):
132+
"""Iterates over all signers for unsigning."""
133+
if salt is None:
134+
salt = self.salt
135+
yield self.make_signer(salt)
136+
for fallback in self.fallback_signers:
137+
if type(fallback) is dict:
138+
kwargs = fallback
139+
fallback = self.signer
140+
elif type(fallback) is tuple:
141+
fallback, kwargs = fallback
142+
else:
143+
kwargs = self.signer_kwargs
144+
yield fallback(self.secret_key, salt=salt, **kwargs)
145+
109146
def dumps(self, obj, salt=None):
110147
"""Returns a signed string serialized with the internal
111148
serializer. The return value can be either a byte or unicode
@@ -128,7 +165,13 @@ def loads(self, s, salt=None):
128165
signature validation fails.
129166
"""
130167
s = want_bytes(s)
131-
return self.load_payload(self.make_signer(salt).unsign(s))
168+
last_exception = None
169+
for signer in self.iter_unsigners(salt):
170+
try:
171+
return self.load_payload(signer.unsign(s))
172+
except BadSignature as err:
173+
last_exception = err
174+
raise last_exception
132175

133176
def load(self, f, salt=None):
134177
"""Like :meth:`loads` but loads from a file."""

src/itsdangerous/signer.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,9 @@ class HMACAlgorithm(SigningAlgorithm):
3838
"""Provides signature generation using HMACs."""
3939

4040
#: The digest method to use with the MAC algorithm. This defaults to
41-
#: SHA-512, but can be changed to any other function in the hashlib
41+
#: SHA1, but can be changed to any other function in the hashlib
4242
#: module.
43-
#:
44-
#: .. versionchanged:: 1.0
45-
#: The default was changed from SHA-1 to SHA-512.
46-
default_digest_method = staticmethod(hashlib.sha512)
43+
default_digest_method = staticmethod(hashlib.sha1)
4744

4845
def __init__(self, digest_method=None):
4946
if digest_method is None:
@@ -77,14 +74,11 @@ class Signer(object):
7774
"""
7875

7976
#: The digest method to use for the signer. This defaults to
80-
#: SHA-512 but can be changed to any other function in the hashlib
77+
#: SHA1 but can be changed to any other function in the hashlib
8178
#: module.
8279
#:
83-
#: .. versionchanged:: 1.0
84-
#: The default was changed from SHA-1 to SHA-512.
85-
#:
8680
#: .. versionadded:: 0.14
87-
default_digest_method = staticmethod(hashlib.sha512)
81+
default_digest_method = staticmethod(hashlib.sha1)
8882

8983
#: Controls how the key is derived. The default is Django-style
9084
#: concatenation. Possible values are ``concat``, ``django-concat``

src/itsdangerous/timed.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,18 @@ def loads(self, s, max_age=None, return_timestamp=False, salt=None):
123123
raised. All arguments are forwarded to the signer's
124124
:meth:`~TimestampSigner.unsign` method.
125125
"""
126-
base64d, timestamp = self.make_signer(salt).unsign(
127-
s, max_age, return_timestamp=True
128-
)
129-
payload = self.load_payload(base64d)
130-
if return_timestamp:
131-
return payload, timestamp
132-
return payload
126+
s = want_bytes(s)
127+
last_exception = None
128+
for signer in self.iter_unsigners(salt):
129+
try:
130+
base64d, timestamp = signer.unsign(s, max_age, return_timestamp=True)
131+
payload = self.load_payload(base64d)
132+
if return_timestamp:
133+
return payload, timestamp
134+
return payload
135+
except BadSignature as err:
136+
last_exception = err
137+
raise last_exception
133138

134139
def loads_unsafe(self, s, max_age=None, salt=None):
135140
load_kwargs = {"max_age": max_age}

tests/test_itsdangerous/test_serializer.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import hashlib
12
import pickle
23
from functools import partial
34
from io import BytesIO
@@ -131,3 +132,36 @@ def test_serializer_kwargs(self, serializer_factory):
131132
return
132133

133134
assert serializer.loads(serializer.dumps({(): 1})) == {}
135+
136+
def test_fallback_signers(self):
137+
serializer = Serializer(
138+
secret_key="foo", signer_kwargs={"digest_method": hashlib.sha512}
139+
)
140+
value = serializer.dumps([1, 2, 3])
141+
fallback_serializer = Serializer(
142+
secret_key="foo",
143+
signer_kwargs={"digest_method": hashlib.sha1},
144+
fallback_signers=[{"digest_method": hashlib.sha512}],
145+
)
146+
assert fallback_serializer.loads(value) == [1, 2, 3]
147+
148+
def test_digests(self):
149+
default_value = Serializer(
150+
secret_key="dev key", salt="dev salt", signer_kwargs={}
151+
).dumps([42])
152+
sha1_value = Serializer(
153+
secret_key="dev key",
154+
salt="dev salt",
155+
signer_kwargs={"digest_method": hashlib.sha1},
156+
).dumps([42])
157+
sha512_value = Serializer(
158+
secret_key="dev key",
159+
salt="dev salt",
160+
signer_kwargs={"digest_method": hashlib.sha512},
161+
).dumps([42])
162+
assert default_value == sha1_value
163+
assert sha1_value == "[42].-9cNi0CxsSB3hZPNCe9a2eEs1ZM"
164+
assert sha512_value == (
165+
"[42].MKCz_0nXQqv7wKpfHZcRtJRmpT2T5uvs9YQsJEhJimqxc"
166+
"9bCLxG31QzS5uC8OVBI1i6jyOLAFNoKaF5ckO9L5Q"
167+
)

tests/test_itsdangerous/test_timed.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import hashlib
12
from datetime import datetime
23
from datetime import timedelta
34
from functools import partial
@@ -84,3 +85,15 @@ def test_max_age(self, serializer, value, ts, freeze):
8485
def test_return_payload(self, serializer, value, ts):
8586
signed = serializer.dumps(value)
8687
assert serializer.loads(signed, return_timestamp=True) == (value, ts)
88+
89+
def test_fallback_signers(self):
90+
serializer = TimedSerializer(
91+
secret_key="foo", signer_kwargs={"digest_method": hashlib.sha512}
92+
)
93+
value = serializer.dumps([1, 2, 3])
94+
fallback_serializer = TimedSerializer(
95+
secret_key="foo",
96+
signer_kwargs={"digest_method": hashlib.sha1},
97+
fallback_signers=[{"digest_method": hashlib.sha512}],
98+
)
99+
assert fallback_serializer.loads(value) == [1, 2, 3]

0 commit comments

Comments
 (0)