Refs #27468 -- Changed default Signer algorithm to SHA-256.
This commit is contained in:
parent
4bb33bb074
commit
71c4fb7beb
|
@ -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)
|
||||
|
||||
|
|
|
@ -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``.
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -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
|
||||
----------------------------------
|
||||
|
||||
|
|
|
@ -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"""
|
||||
|
|
Loading…
Reference in New Issue