summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaude Paroz <claude@2xlibre.net>2020-02-13 20:55:48 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-04-15 12:49:14 +0200
commit71c4fb7beb8e3293243140e4bd74e53989196440 (patch)
tree543c9d7c08e5f2a5ab8e7b7a63d55cea3da983f3
parent4bb33bb07450ea9b623877417c28e6489246f51c (diff)
Refs #27468 -- Changed default Signer algorithm to SHA-256.
-rw-r--r--django/core/signing.py27
-rw-r--r--docs/internals/deprecation.txt3
-rw-r--r--docs/releases/3.1.txt8
-rw-r--r--docs/topics/signing.txt15
-rw-r--r--tests/signing/tests.py47
5 files changed, 86 insertions, 14 deletions
diff --git a/django/core/signing.py b/django/core/signing.py
index da45ce11b2..652694bb99 100644
--- a/django/core/signing.py
+++ b/django/core/signing.py
@@ -68,8 +68,8 @@ def b64_decode(s):
return base64.urlsafe_b64decode(s + pad)
-def base64_hmac(salt, value, key):
- return b64_encode(salted_hmac(salt, value, key).digest()).decode()
+def base64_hmac(salt, value, key, algorithm='sha1'):
+ return b64_encode(salted_hmac(salt, value, key, algorithm=algorithm).digest()).decode()
def get_cookie_signer(salt='django.core.signing.get_cookie_signer'):
@@ -92,8 +92,9 @@ class JSONSerializer:
def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer, compress=False):
"""
- Return URL-safe, hmac/SHA1 signed base64 compressed JSON string. If key is
- None, use settings.SECRET_KEY instead.
+ Return URL-safe, hmac signed base64 compressed JSON string. If key is
+ None, use settings.SECRET_KEY instead. The hmac algorithm is the default
+ Signer algorithm.
If compress is True (not the default), check if compressing using zlib can
save some space. Prepend a '.' to signify compression. This is included
@@ -143,8 +144,10 @@ def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, ma
class Signer:
+ # RemovedInDjango40Warning.
+ legacy_algorithm = 'sha1'
- def __init__(self, key=None, sep=':', salt=None):
+ def __init__(self, key=None, sep=':', salt=None, algorithm='sha256'):
self.key = key or settings.SECRET_KEY
self.sep = sep
if _SEP_UNSAFE.match(self.sep):
@@ -153,9 +156,14 @@ class Signer:
'only A-z0-9-_=)' % sep,
)
self.salt = salt or '%s.%s' % (self.__class__.__module__, self.__class__.__name__)
+ self.algorithm = algorithm
def signature(self, value):
- return base64_hmac(self.salt + 'signer', value, self.key)
+ return base64_hmac(self.salt + 'signer', value, self.key, algorithm=self.algorithm)
+
+ def _legacy_signature(self, value):
+ # RemovedInDjango40Warning.
+ return base64_hmac(self.salt + 'signer', value, self.key, algorithm=self.legacy_algorithm)
def sign(self, value):
return '%s%s%s' % (value, self.sep, self.signature(value))
@@ -164,7 +172,12 @@ class Signer:
if self.sep not in signed_value:
raise BadSignature('No "%s" found in value' % self.sep)
value, sig = signed_value.rsplit(self.sep, 1)
- if constant_time_compare(sig, self.signature(value)):
+ if (
+ constant_time_compare(sig, self.signature(value)) or (
+ self.legacy_algorithm and
+ constant_time_compare(sig, self._legacy_signature(value))
+ )
+ ):
return value
raise BadSignature('Signature "%s" does not match' % sig)
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index 257b9a3bf4..76b379ef5a 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -54,6 +54,9 @@ details on these changes.
* Support for the pre-Django 3.1 encoding format of sessions will be removed.
+* Support for the pre-Django 3.1 ``django.core.signing.Signer`` signatures
+ (encoded with the SHA-1 algorithm) will be removed.
+
* The ``get_request`` argument for
``django.utils.deprecation.MiddlewareMixin.__init__()`` will be required and
won't accept ``None``.
diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt
index 384fa43dfb..55f539d534 100644
--- a/docs/releases/3.1.txt
+++ b/docs/releases/3.1.txt
@@ -404,6 +404,14 @@ Security
origins. If you need the previous behavior, explicitly set
:setting:`SECURE_REFERRER_POLICY` to ``None``.
+* The default :class:`django.core.signing.Signer` algorithm is changed to the
+ SHA-256. Support for signatures made with the old SHA-1 algorithm remains
+ until Django 4.0.
+
+ Also, the new ``algorithm`` parameter of the
+ :class:`~django.core.signing.Signer` allows customizing the hashing
+ algorithm.
+
Serialization
~~~~~~~~~~~~~
diff --git a/docs/topics/signing.txt b/docs/topics/signing.txt
index 5fa537c579..7927333914 100644
--- a/docs/topics/signing.txt
+++ b/docs/topics/signing.txt
@@ -81,12 +81,17 @@ generate signatures. You can use a different secret by passing it to the
>>> value
'My string:EkfQJafvGyiofrdGnuthdxImIJw'
-.. class:: Signer(key=None, sep=':', salt=None)
+.. class:: Signer(key=None, sep=':', salt=None, algorithm='sha256')
Returns a signer which uses ``key`` to generate signatures and ``sep`` to
separate values. ``sep`` cannot be in the `URL safe base64 alphabet
<https://tools.ietf.org/html/rfc4648#section-5>`_. This alphabet contains
- alphanumeric characters, hyphens, and underscores.
+ alphanumeric characters, hyphens, and underscores. ``algorithm`` must be an
+ algorithm supported by :py:mod:`hashlib`.
+
+ .. versionchanged:: 3.1
+
+ The ``algorithm`` parameter was added.
Using the ``salt`` argument
---------------------------
@@ -139,7 +144,7 @@ created within a specified period of time::
>>> signer.unsign(value, max_age=timedelta(seconds=20))
'hello'
-.. class:: TimestampSigner(key=None, sep=':', salt=None)
+.. class:: TimestampSigner(key=None, sep=':', salt=None, algorithm='sha256')
.. method:: sign(value)
@@ -151,6 +156,10 @@ created within a specified period of time::
otherwise raises ``SignatureExpired``. The ``max_age`` parameter can
accept an integer or a :py:class:`datetime.timedelta` object.
+ .. versionchanged:: 3.1
+
+ The ``algorithm`` parameter was added.
+
Protecting complex data structures
----------------------------------
diff --git a/tests/signing/tests.py b/tests/signing/tests.py
index d0767c0703..6b7268179d 100644
--- a/tests/signing/tests.py
+++ b/tests/signing/tests.py
@@ -3,6 +3,7 @@ import datetime
from django.core import signing
from django.test import SimpleTestCase
from django.test.utils import freeze_time
+from django.utils.crypto import InvalidAlgorithm
class TestSigner(SimpleTestCase):
@@ -18,7 +19,12 @@ class TestSigner(SimpleTestCase):
):
self.assertEqual(
signer.signature(s),
- signing.base64_hmac(signer.salt + 'signer', s, 'predictable-secret')
+ signing.base64_hmac(
+ signer.salt + 'signer',
+ s,
+ 'predictable-secret',
+ algorithm=signer.algorithm,
+ )
)
self.assertNotEqual(signer.signature(s), signer2.signature(s))
@@ -27,12 +33,39 @@ class TestSigner(SimpleTestCase):
signer = signing.Signer('predictable-secret', salt='extra-salt')
self.assertEqual(
signer.signature('hello'),
- signing.base64_hmac('extra-salt' + 'signer', 'hello', 'predictable-secret')
+ signing.base64_hmac(
+ 'extra-salt' + 'signer',
+ 'hello',
+ 'predictable-secret',
+ algorithm=signer.algorithm,
+ )
)
self.assertNotEqual(
signing.Signer('predictable-secret', salt='one').signature('hello'),
signing.Signer('predictable-secret', salt='two').signature('hello'))
+ def test_custom_algorithm(self):
+ signer = signing.Signer('predictable-secret', algorithm='sha512')
+ self.assertEqual(
+ signer.signature('hello'),
+ 'Usf3uVQOZ9m6uPfVonKR-EBXjPe7bjMbp3_Fq8MfsptgkkM1ojidN0BxYaT5HAEN1'
+ 'VzO9_jVu7R-VkqknHYNvw',
+ )
+
+ def test_invalid_algorithm(self):
+ signer = signing.Signer('predictable-secret', algorithm='whatever')
+ msg = "'whatever' is not an algorithm accepted by the hashlib module."
+ with self.assertRaisesMessage(InvalidAlgorithm, msg):
+ signer.sign('hello')
+
+ def test_legacy_signature(self):
+ # RemovedInDjango40Warning: pre-Django 3.1 signatures won't be
+ # supported.
+ signer = signing.Signer()
+ sha1_sig = 'foo:l-EMM5FtewpcHMbKFeQodt3X9z8'
+ self.assertNotEqual(signer.sign('foo'), sha1_sig)
+ self.assertEqual(signer.unsign(sha1_sig), 'foo')
+
def test_sign_unsign(self):
"sign/unsign should be reversible"
signer = signing.Signer('predictable-secret')
@@ -115,13 +148,19 @@ class TestSigner(SimpleTestCase):
binary_key = b'\xe7' # Set some binary (non-ASCII key)
s = signing.Signer(binary_key)
- self.assertEqual('foo:6NB0fssLW5RQvZ3Y-MTerq2rX7w', s.sign('foo'))
+ self.assertEqual(
+ 'foo:EE4qGC5MEKyQG5msxYA0sBohAxLC0BJf8uRhemh0BGU',
+ s.sign('foo'),
+ )
def test_valid_sep(self):
separators = ['/', '*sep*', ',']
for sep in separators:
signer = signing.Signer('predictable-secret', sep=sep)
- self.assertEqual('foo%ssH9B01cZcJ9FoT_jEVkRkNULrl8' % sep, signer.sign('foo'))
+ self.assertEqual(
+ 'foo%sjZQoX_FtSO70jX9HLRGg2A_2s4kdDBxz1QoO_OpEQb0' % sep,
+ signer.sign('foo'),
+ )
def test_invalid_sep(self):
"""should warn on invalid separator"""