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)
|
return base64.urlsafe_b64decode(s + pad)
|
||||||
|
|
||||||
|
|
||||||
def base64_hmac(salt, value, key):
|
def base64_hmac(salt, value, key, algorithm='sha1'):
|
||||||
return b64_encode(salted_hmac(salt, value, key).digest()).decode()
|
return b64_encode(salted_hmac(salt, value, key, algorithm=algorithm).digest()).decode()
|
||||||
|
|
||||||
|
|
||||||
def get_cookie_signer(salt='django.core.signing.get_cookie_signer'):
|
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):
|
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
|
Return URL-safe, hmac signed base64 compressed JSON string. If key is
|
||||||
None, use settings.SECRET_KEY instead.
|
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
|
If compress is True (not the default), check if compressing using zlib can
|
||||||
save some space. Prepend a '.' to signify compression. This is included
|
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:
|
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.key = key or settings.SECRET_KEY
|
||||||
self.sep = sep
|
self.sep = sep
|
||||||
if _SEP_UNSAFE.match(self.sep):
|
if _SEP_UNSAFE.match(self.sep):
|
||||||
|
@ -153,9 +156,14 @@ class Signer:
|
||||||
'only A-z0-9-_=)' % sep,
|
'only A-z0-9-_=)' % sep,
|
||||||
)
|
)
|
||||||
self.salt = salt or '%s.%s' % (self.__class__.__module__, self.__class__.__name__)
|
self.salt = salt or '%s.%s' % (self.__class__.__module__, self.__class__.__name__)
|
||||||
|
self.algorithm = algorithm
|
||||||
|
|
||||||
def signature(self, value):
|
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):
|
def sign(self, value):
|
||||||
return '%s%s%s' % (value, self.sep, self.signature(value))
|
return '%s%s%s' % (value, self.sep, self.signature(value))
|
||||||
|
@ -164,7 +172,12 @@ class Signer:
|
||||||
if self.sep not in signed_value:
|
if self.sep not in signed_value:
|
||||||
raise BadSignature('No "%s" found in value' % self.sep)
|
raise BadSignature('No "%s" found in value' % self.sep)
|
||||||
value, sig = signed_value.rsplit(self.sep, 1)
|
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
|
return value
|
||||||
raise BadSignature('Signature "%s" does not match' % sig)
|
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 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
|
* The ``get_request`` argument for
|
||||||
``django.utils.deprecation.MiddlewareMixin.__init__()`` will be required and
|
``django.utils.deprecation.MiddlewareMixin.__init__()`` will be required and
|
||||||
won't accept ``None``.
|
won't accept ``None``.
|
||||||
|
|
|
@ -404,6 +404,14 @@ Security
|
||||||
origins. If you need the previous behavior, explicitly set
|
origins. If you need the previous behavior, explicitly set
|
||||||
:setting:`SECURE_REFERRER_POLICY` to ``None``.
|
: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
|
Serialization
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -81,12 +81,17 @@ generate signatures. You can use a different secret by passing it to the
|
||||||
>>> value
|
>>> value
|
||||||
'My string:EkfQJafvGyiofrdGnuthdxImIJw'
|
'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
|
Returns a signer which uses ``key`` to generate signatures and ``sep`` to
|
||||||
separate values. ``sep`` cannot be in the `URL safe base64 alphabet
|
separate values. ``sep`` cannot be in the `URL safe base64 alphabet
|
||||||
<https://tools.ietf.org/html/rfc4648#section-5>`_. This alphabet contains
|
<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
|
Using the ``salt`` argument
|
||||||
---------------------------
|
---------------------------
|
||||||
|
@ -139,7 +144,7 @@ created within a specified period of time::
|
||||||
>>> signer.unsign(value, max_age=timedelta(seconds=20))
|
>>> signer.unsign(value, max_age=timedelta(seconds=20))
|
||||||
'hello'
|
'hello'
|
||||||
|
|
||||||
.. class:: TimestampSigner(key=None, sep=':', salt=None)
|
.. class:: TimestampSigner(key=None, sep=':', salt=None, algorithm='sha256')
|
||||||
|
|
||||||
.. method:: sign(value)
|
.. method:: sign(value)
|
||||||
|
|
||||||
|
@ -151,6 +156,10 @@ created within a specified period of time::
|
||||||
otherwise raises ``SignatureExpired``. The ``max_age`` parameter can
|
otherwise raises ``SignatureExpired``. The ``max_age`` parameter can
|
||||||
accept an integer or a :py:class:`datetime.timedelta` object.
|
accept an integer or a :py:class:`datetime.timedelta` object.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.1
|
||||||
|
|
||||||
|
The ``algorithm`` parameter was added.
|
||||||
|
|
||||||
Protecting complex data structures
|
Protecting complex data structures
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import datetime
|
||||||
from django.core import signing
|
from django.core import signing
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
from django.test.utils import freeze_time
|
from django.test.utils import freeze_time
|
||||||
|
from django.utils.crypto import InvalidAlgorithm
|
||||||
|
|
||||||
|
|
||||||
class TestSigner(SimpleTestCase):
|
class TestSigner(SimpleTestCase):
|
||||||
|
@ -18,7 +19,12 @@ class TestSigner(SimpleTestCase):
|
||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
signer.signature(s),
|
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))
|
self.assertNotEqual(signer.signature(s), signer2.signature(s))
|
||||||
|
|
||||||
|
@ -27,12 +33,39 @@ class TestSigner(SimpleTestCase):
|
||||||
signer = signing.Signer('predictable-secret', salt='extra-salt')
|
signer = signing.Signer('predictable-secret', salt='extra-salt')
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
signer.signature('hello'),
|
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(
|
self.assertNotEqual(
|
||||||
signing.Signer('predictable-secret', salt='one').signature('hello'),
|
signing.Signer('predictable-secret', salt='one').signature('hello'),
|
||||||
signing.Signer('predictable-secret', salt='two').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):
|
def test_sign_unsign(self):
|
||||||
"sign/unsign should be reversible"
|
"sign/unsign should be reversible"
|
||||||
signer = signing.Signer('predictable-secret')
|
signer = signing.Signer('predictable-secret')
|
||||||
|
@ -115,13 +148,19 @@ class TestSigner(SimpleTestCase):
|
||||||
binary_key = b'\xe7' # Set some binary (non-ASCII key)
|
binary_key = b'\xe7' # Set some binary (non-ASCII key)
|
||||||
|
|
||||||
s = signing.Signer(binary_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):
|
def test_valid_sep(self):
|
||||||
separators = ['/', '*sep*', ',']
|
separators = ['/', '*sep*', ',']
|
||||||
for sep in separators:
|
for sep in separators:
|
||||||
signer = signing.Signer('predictable-secret', sep=sep)
|
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):
|
def test_invalid_sep(self):
|
||||||
"""should warn on invalid separator"""
|
"""should warn on invalid separator"""
|
||||||
|
|
Loading…
Reference in New Issue