Refs #27468 -- Changed default Signer algorithm to SHA-256.

This commit is contained in:
Claude Paroz 2020-02-13 20:55:48 +01:00 committed by Mariusz Felisiak
parent 4bb33bb074
commit 71c4fb7beb
5 changed files with 86 additions and 14 deletions

View File

@ -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)

View File

@ -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``.

View File

@ -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
~~~~~~~~~~~~~

View File

@ -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
----------------------------------

View File

@ -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"""