Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added fallback signers and switch back to sha1 #113

Merged
merged 8 commits into from
Oct 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
Version 1.1.0
-------------

Released 2018-10-26

- Change algorithm back to SHA-1
- Add support for fallback algorithm
- Changed capitalization of packages back to lowercase as the
change in capitalization broke some tooling.

Version 1.0.0
-------------

Released 2018-10-18

YANKED

*Note*: this release was yanked from pypi because it changed the default
algorithm to SHA-512. This decision was reverted and it remains at SHA1.

- Drop support for Python 2.6 and 3.3.
- Refactor code from a single module to a package. Any object in the
API docs is still importable from the top-level ``itsdangerous``
Expand Down
6 changes: 2 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Install and update using `pip`_:

.. code-block:: text

pip install -U ItsDangerous
pip install -U itsdangerous

.. _pip: https://pip.pypa.io/en/stable/quickstart/

Expand All @@ -33,13 +33,11 @@ name between web requests.
.. code-block:: python

from itsdangerous import URLSafeSerializer

auth_s = URLSafeSerializer("secret key", "auth")
token = auth_s.dumps({"id": 5, "name": "itsdangerous"})

print(token)
# eyJpZCI6NSwibmFtZSI6Iml0c2Rhbmdlcm91cyJ9.AmSPrPa_iZ6q-ERXXdQxt6ce8NEqt
# 3i2Uke3sIRnDG0riZD6OoqckqC72VJ9SBIu-vAf_XlwNHnt7dLEClT0JA
# eyJpZCI6NSwibmFtZSI6Iml0c2Rhbmdlcm91cyJ9.6YP6T0BaO67XP--9UzTrmurXSmg

data = auth_s.loads(token)
print(data["name"])
Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
project = "It's Dangerous"
copyright = "2011 Pallets Team"
author = "Pallets Team"
release, version = get_version("ItsDangerous")
release, version = get_version("itsdangerous")

# General --------------------------------------------------------------

Expand All @@ -22,7 +22,7 @@
"project_links": [
ProjectLink("Donate to Pallets", "https://palletsprojects.com/donate"),
ProjectLink("Website", "https://palletsprojects.com/p/itsdangerous/"),
ProjectLink("PyPI releases", "https://pypi.org/project/ItsDangerous/"),
ProjectLink("PyPI releases", "https://pypi.org/project/itsdangerous/"),
ProjectLink("Source Code", "https://github.com/pallets/itsdangerous/"),
ProjectLink("Issue Tracker", "https://github.com/pallets/itsdangerous/issues/"),
]
Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Install and update using `pip`_:

.. code-block:: text

pip install -U ItsDangerous
pip install -U itsdangerous

.. _pip: https://pip.pypa.io/en/stable/quickstart/

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
version = re.search(r"__version__ = \"(.*?)\"", f.read()).group(1)

setup(
name="ItsDangerous",
name="itsdangerous",
version=version,
url="https://palletsprojects.com/p/itsdangerous/",
project_urls={
Expand Down
45 changes: 44 additions & 1 deletion src/itsdangerous/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,29 @@ class to the constructor as well as keyword arguments as a dict that

s = Serializer(signer_kwargs={'key_derivation': 'hmac'})

Additionally as of 1.1 fallback signers can be defined by providing
a list as `fallback_signers`. These are used for deserialization as a
fallback. Each item can be one of the following:
a signer class (which is instantiated with `signer_kwargs`, salt and
secret key), a tuple `(signer_class, signer_kwargs)` or just `signer_kwargs`.
If kwargs are provided they need to be a dict.

For instance this is a serializer that supports deserialization that
supports both SHA1 and SHA512:

.. code-block:: python3

s = Serializer(
signer_kwargs={'digest_method': hashlib.sha512},
fallback_signers=[{'digest_method': hashlib.sha1}]
)

.. versionchanged:: 0.14:
The ``signer`` and ``signer_kwargs`` parameters were added to
the constructor.

.. versionchanged:: 1.1:
Added support for `fallback_signers`.
"""

#: If a serializer module or class is not passed to the constructor
Expand All @@ -55,6 +75,7 @@ def __init__(
serializer_kwargs=None,
signer=None,
signer_kwargs=None,
fallback_signers=None,
):
self.secret_key = want_bytes(secret_key)
self.salt = want_bytes(salt)
Expand All @@ -66,6 +87,7 @@ def __init__(
signer = self.default_signer
self.signer = signer
self.signer_kwargs = signer_kwargs or {}
self.fallback_signers = fallback_signers or ()
self.serializer_kwargs = serializer_kwargs or {}

def load_payload(self, payload, serializer=None):
Expand Down Expand Up @@ -106,6 +128,21 @@ def make_signer(self, salt=None):
salt = self.salt
return self.signer(self.secret_key, salt=salt, **self.signer_kwargs)

def iter_unsigners(self, salt=None):
"""Iterates over all signers for unsigning."""
if salt is None:
salt = self.salt
yield self.make_signer(salt)
for fallback in self.fallback_signers:
if type(fallback) is dict:
kwargs = fallback
fallback = self.signer
elif type(fallback) is tuple:
fallback, kwargs = fallback
else:
kwargs = self.signer_kwargs
yield fallback(self.secret_key, salt=salt, **kwargs)

def dumps(self, obj, salt=None):
"""Returns a signed string serialized with the internal
serializer. The return value can be either a byte or unicode
Expand All @@ -128,7 +165,13 @@ def loads(self, s, salt=None):
signature validation fails.
"""
s = want_bytes(s)
return self.load_payload(self.make_signer(salt).unsign(s))
last_exception = None
for signer in self.iter_unsigners(salt):
try:
return self.load_payload(signer.unsign(s))
except BadSignature as err:
last_exception = err
raise last_exception

def load(self, f, salt=None):
"""Like :meth:`loads` but loads from a file."""
Expand Down
14 changes: 4 additions & 10 deletions src/itsdangerous/signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,9 @@ class HMACAlgorithm(SigningAlgorithm):
"""Provides signature generation using HMACs."""

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

def __init__(self, digest_method=None):
if digest_method is None:
Expand Down Expand Up @@ -77,14 +74,11 @@ class Signer(object):
"""

#: The digest method to use for the signer. This defaults to
#: SHA-512 but can be changed to any other function in the hashlib
#: SHA1 but can be changed to any other function in the hashlib
#: module.
#:
#: .. versionchanged:: 1.0
#: The default was changed from SHA-1 to SHA-512.
#:
#: .. versionadded:: 0.14
default_digest_method = staticmethod(hashlib.sha512)
default_digest_method = staticmethod(hashlib.sha1)

#: Controls how the key is derived. The default is Django-style
#: concatenation. Possible values are ``concat``, ``django-concat``
Expand Down
19 changes: 12 additions & 7 deletions src/itsdangerous/timed.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,18 @@ def loads(self, s, max_age=None, return_timestamp=False, salt=None):
raised. All arguments are forwarded to the signer's
:meth:`~TimestampSigner.unsign` method.
"""
base64d, timestamp = self.make_signer(salt).unsign(
s, max_age, return_timestamp=True
)
payload = self.load_payload(base64d)
if return_timestamp:
return payload, timestamp
return payload
s = want_bytes(s)
last_exception = None
for signer in self.iter_unsigners(salt):
try:
base64d, timestamp = signer.unsign(s, max_age, return_timestamp=True)
payload = self.load_payload(base64d)
if return_timestamp:
return payload, timestamp
return payload
except BadSignature as err:
last_exception = err
raise last_exception

def loads_unsafe(self, s, max_age=None, salt=None):
load_kwargs = {"max_age": max_age}
Expand Down
34 changes: 34 additions & 0 deletions tests/test_itsdangerous/test_serializer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import hashlib
import pickle
from functools import partial
from io import BytesIO
Expand Down Expand Up @@ -131,3 +132,36 @@ def test_serializer_kwargs(self, serializer_factory):
return

assert serializer.loads(serializer.dumps({(): 1})) == {}

def test_fallback_signers(self):
serializer = Serializer(
secret_key="foo", signer_kwargs={"digest_method": hashlib.sha512}
)
value = serializer.dumps([1, 2, 3])
fallback_serializer = Serializer(
secret_key="foo",
signer_kwargs={"digest_method": hashlib.sha1},
fallback_signers=[{"digest_method": hashlib.sha512}],
)
assert fallback_serializer.loads(value) == [1, 2, 3]

def test_digests(self):
default_value = Serializer(
secret_key="dev key", salt="dev salt", signer_kwargs={}
).dumps([42])
sha1_value = Serializer(
secret_key="dev key",
salt="dev salt",
signer_kwargs={"digest_method": hashlib.sha1},
).dumps([42])
sha512_value = Serializer(
secret_key="dev key",
salt="dev salt",
signer_kwargs={"digest_method": hashlib.sha512},
).dumps([42])
assert default_value == sha1_value
assert sha1_value == "[42].-9cNi0CxsSB3hZPNCe9a2eEs1ZM"
assert sha512_value == (
"[42].MKCz_0nXQqv7wKpfHZcRtJRmpT2T5uvs9YQsJEhJimqxc"
"9bCLxG31QzS5uC8OVBI1i6jyOLAFNoKaF5ckO9L5Q"
)
13 changes: 13 additions & 0 deletions tests/test_itsdangerous/test_timed.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import hashlib
from datetime import datetime
from datetime import timedelta
from functools import partial
Expand Down Expand Up @@ -84,3 +85,15 @@ def test_max_age(self, serializer, value, ts, freeze):
def test_return_payload(self, serializer, value, ts):
signed = serializer.dumps(value)
assert serializer.loads(signed, return_timestamp=True) == (value, ts)

def test_fallback_signers(self):
serializer = TimedSerializer(
secret_key="foo", signer_kwargs={"digest_method": hashlib.sha512}
)
value = serializer.dumps([1, 2, 3])
fallback_serializer = TimedSerializer(
secret_key="foo",
signer_kwargs={"digest_method": hashlib.sha1},
fallback_signers=[{"digest_method": hashlib.sha512}],
)
assert fallback_serializer.loads(value) == [1, 2, 3]