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

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

View File

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

View File

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

View File

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