Криптографическая подпись

Золотое правило безопасности веб-приложений - никогда не доверять данным из ненадежных источников. Иногда полезно передавать данные через ненадежный носитель. Криптографически подписанные значения можно передавать по ненадежному каналу, будучи уверенным, что любое вмешательство будет обнаружено.

Django предоставляет как низкоуровневый API для подписи значений, так и высокоуровневый API для установки и чтения подписанных cookies, что является одним из наиболее распространенных способов использования подписи в веб-приложениях.

Вы также можете счесть подписание полезным для следующего:

  • Генерация URL-адресов «восстановить мой аккаунт» для отправки пользователям, потерявшим пароль.
  • Убедитесь, что данные, хранящиеся в скрытых полях формы, не были подделаны.
  • Генерация одноразовых секретных URL-адресов для предоставления временного доступа к защищенному ресурсу, например, к загружаемому файлу, за который пользователь заплатил.

Защита SECRET_KEY и SECRET_KEY_FALLBACKS

Когда вы создаете новый проект Django, используя startproject, файл settings.py генерируется автоматически и получает случайное значение SECRET_KEY. Это значение является ключом к защите подписанных данных - очень важно, чтобы вы сохранили его в безопасности, иначе злоумышленники могут использовать его для генерации собственных подписанных значений.

SECRET_KEY_FALLBACKS можно использовать для вращения секретных ключей. Значения не будут использоваться для подписи данных, но если они указаны, то будут использоваться для проверки подписанных данных и должны храниться в безопасности.

Использование низкоуровневого API

Методы подписи в Django находятся в модуле django.core.signing. Чтобы подписать значение, сначала создайте экземпляр Signer:

>>> from django.core.signing import Signer
>>> signer = Signer()
>>> value = signer.sign("My string")
>>> value
'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'

Подпись добавляется в конец строки после двоеточия. Получить исходное значение можно с помощью метода unsign:

>>> original = signer.unsign(value)
>>> original
'My string'

Если в sign передать нестроковое значение, то перед подписанием оно будет преобразовано в строку, и в результате unsign будет получено строковое значение:

>>> signed = signer.sign(2.5)
>>> original = signer.unsign(signed)
>>> original
'2.5'

Если необходимо защитить список, кортеж или словарь, то это можно сделать с помощью методов sign_object() и unsign_object():

>>> signed_obj = signer.sign_object({"message": "Hello!"})
>>> signed_obj
'eyJtZXNzYWdlIjoiSGVsbG8hIn0:Xdc-mOFDjs22KsQAqfVfi8PQSPdo3ckWJxPWwQOFhR4'
>>> obj = signer.unsign_object(signed_obj)
>>> obj
{'message': 'Hello!'}

Более подробную информацию см. в разделе Защита сложных структур данных.

Если сигнатура или значение были изменены каким-либо образом, то будет вызвано исключение django.core.signing.BadSignature:

>>> from django.core import signing
>>> value += "m"
>>> try:
...     original = signer.unsign(value)
... except signing.BadSignature:
...     print("Tampering detected!")
...

По умолчанию класс Signer использует для генерации сигнатур настройку SECRET_KEY. Вы можете использовать другой секрет, передав его в конструктор Signer:

>>> signer = Signer(key="my-other-secret")
>>> value = signer.sign("My string")
>>> value
'My string:EkfQJafvGyiofrdGnuthdxImIJw'
class Signer(*, key=None, sep=':', salt=None, algorithm=None, fallback_keys=None)[исходный код]

Возвращает подписывающее устройство, которое использует key для генерации подписей и sep для разделения значений. sep не может находиться в URL safe base64 alphabet. Этот алфавит содержит буквенно-цифровые символы, дефисы и знаки подчеркивания. algorithm должен быть алгоритмом, поддерживаемым hashlib, по умолчанию это 'sha256'. fallback_keys - список дополнительных значений, используемых для проверки подписанных данных, по умолчанию SECRET_KEY_FALLBACKS.

Не рекомендуется, начиная с версии 4.2: Поддержка передачи позиционных аргументов устарела.

Использование аргумента salt

Если вы не хотите, чтобы каждое вхождение конкретной строки имело один и тот же сигнатурный хэш, вы можете использовать дополнительный аргумент salt в классе Signer. Использование соли приведет к тому, что хэш-функция подписи будет засеяна как солью, так и вашим SECRET_KEY:

>>> signer = Signer()
>>> signer.sign("My string")
'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
>>> signer.sign_object({"message": "Hello!"})
'eyJtZXNzYWdlIjoiSGVsbG8hIn0:Xdc-mOFDjs22KsQAqfVfi8PQSPdo3ckWJxPWwQOFhR4'
>>> signer = Signer(salt="extra")
>>> signer.sign("My string")
'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw'
>>> signer.unsign("My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw")
'My string'
>>> signer.sign_object({"message": "Hello!"})
'eyJtZXNzYWdlIjoiSGVsbG8hIn0:-UWSLCE-oUAHzhkHviYz3SOZYBjFKllEOyVZNuUtM-I'
>>> signer.unsign_object(
...     "eyJtZXNzYWdlIjoiSGVsbG8hIn0:-UWSLCE-oUAHzhkHviYz3SOZYBjFKllEOyVZNuUtM-I"
... )
{'message': 'Hello!'}

Использование соли таким образом помещает различные подписи в разные пространства имен. Подпись, полученная из одного пространства имен (определенное значение соли), не может быть использована для проверки той же строки открытого текста в другом пространстве имен, использующем другое значение соли. В результате злоумышленник не сможет использовать подписанную строку, сгенерированную в одном месте кода, в качестве входных данных для другого фрагмента кода, который генерирует (и проверяет) подписи с использованием другой соли.

В отличие от вашего SECRET_KEY, ваш аргумент соли не обязательно должен оставаться секретным.

Проверка значений с временной меткой

TimestampSigner является подклассом Signer, который добавляет к значению подписанную метку времени. Это позволяет подтвердить, что подписанное значение было создано в течение определенного периода времени:

>>> from datetime import timedelta
>>> from django.core.signing import TimestampSigner
>>> signer = TimestampSigner()
>>> value = signer.sign("hello")
>>> value
'hello:1NMg5H:oPVuCqlJWmChm1rA2lyTUtelC-c'
>>> signer.unsign(value)
'hello'
>>> signer.unsign(value, max_age=10)
SignatureExpired: Signature age 15.5289158821 > 10 seconds
>>> signer.unsign(value, max_age=20)
'hello'
>>> signer.unsign(value, max_age=timedelta(seconds=20))
'hello'
class TimestampSigner(*, key=None, sep=':', salt=None, algorithm='sha256')[исходный код]
sign(value)[исходный код]

Знак value и добавьте к нему текущую метку времени.

unsign(value, max_age=None)[исходный код]

Проверяет, если value было подписано менее max_age секунд назад, в противном случае выдает SignatureExpired. Параметр max_age может принимать целое число или объект datetime.timedelta.

sign_object(obj, serializer=JSONSerializer, compress=False)

Кодировать, по желанию сжимать, добавлять текущую метку времени и подписывать сложную структуру данных (например, список, кортеж или словарь).

unsign_object(signed_obj, serializer=JSONSerializer, max_age=None)

Проверяет, если signed_obj было подписано менее max_age секунд назад, в противном случае выдает SignatureExpired. Параметр max_age может принимать целое число или объект datetime.timedelta.

Не рекомендуется, начиная с версии 4.2: Поддержка передачи позиционных аргументов устарела.

Защита сложных структур данных

Если необходимо защитить список, кортеж или словарь, это можно сделать с помощью методов Signer.sign_object() и unsign_object(), а также функций модуля подписи dumps() или loads() (которые являются сокращениями для TimestampSigner(salt='django.core.signing').sign_object()/unsign_object()). В них используется сериализация JSON. JSON гарантирует, что даже если ваш SECRET_KEY будет украден, злоумышленник не сможет выполнить произвольную команду, используя формат pickle:

>>> from django.core import signing
>>> signer = signing.TimestampSigner()
>>> value = signer.sign_object({"foo": "bar"})
>>> value
'eyJmb28iOiJiYXIifQ:1kx6R3:D4qGKiptAqo5QW9iv4eNLc6xl4RwiFfes6oOcYhkYnc'
>>> signer.unsign_object(value)
{'foo': 'bar'}
>>> value = signing.dumps({"foo": "bar"})
>>> value
'eyJmb28iOiJiYXIifQ:1kx6Rf:LBB39RQmME-SRvilheUe5EmPYRbuDBgQp2tCAi7KGLk'
>>> signing.loads(value)
{'foo': 'bar'}

В силу природы JSON (нет собственного различия между списками и кортежами), если вы передадите кортеж, то получите список из signing.loads(object):

>>> from django.core import signing
>>> value = signing.dumps(("a", "b", "c"))
>>> signing.loads(value)
['a', 'b', 'c']
dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer, compress=False)[исходный код]

Возвращает безопасную для URL, подписанную base64 сжатую строку JSON. Сериализованный объект подписывается с помощью TimestampSigner.

loads(string, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None, fallback_keys=None)[исходный код]

Обратный вариант dumps(), повышает BadSignature, если подпись не прошла. Проверяет max_age (в секундах), если задано.

Вернуться на верх