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

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

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

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

  • Генерация URL для восстановления аккаунта пользователя, которые будут отправлены пользователю, потерявшему свой пароль.

  • Проверка целостности данных, спрятанных в скрытом поле формы.

  • Генерация одноразового секретного URL для обеспечения временного доступа к защищённому ресурсу, например на скачивание файла за который заплатил пользователь.

Защита SECRET_KEY

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

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

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

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

Сигнатура добавляется в конец строки, отделённая двоеточием. Вы можете получить оригинальное значение с помощью метода unsing:

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

Если подпись или значение были изменены любым способом, то будет вызвано исключение 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('my-other-secret')
>>> value = signer.sign('My string')
>>> value
'My string:EkfQJafvGyiofrdGnuthdxImIJw'
class Signer(key=None, sep=':', salt=None)

Возвращает класс для подписи, который использует key для генерации подписи и sep для разделения значений. sep не может быть частью URL safe base64 таблицы, которая содержит буквы, цифры, дефис и подчеркивание.

Использование “соли”

Если требуется, чтобы подпись одинаковой строки всегда создавала разные сигнатуры, вы можете использовать необязательный аргумент salt конструктора. Функция подписи воспользуется “солью” и SECRET_KEY:

>>> signer = Signer()
>>> signer.sign('My string')
'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
>>> signer = Signer(salt='extra')
>>> signer.sign('My string')
'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw'
>>> signer.unsign('My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw')
'My string'

Использование “соли” размещает разные сигнатуры в разных именованных областях. Сигнатура из одной области (определённая значением “соли”) не может быть использована для проверки исходной строки текста в другой именованной области (другое значение “соли”). Результатом является невозможность использовать атакующим строки, подписанные в одном месте, в качестве источника данных в другом.

В отличие от значения 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)
sign(value)

Подписывает value и добавляет текущее время к нему.

unsign(value, max_age=None)

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

Изменено в Django 1.8:

Ранее параметр the max_age принимал только целые значения.

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

Если вам потребуется защитить список, кортеж или словарь, вы можете это сделать с помощью функций dumps и loads модуля. Они имитируют поведение функций модуля Pickle, но используют JSON сериализацию. JSON гарантирует, что даже украв ваш SECRET_KEY, атакующий не сможет выполнить определённые команды взломав pickle формат:

>>> from django.core import signing
>>> value = signing.dumps({"foo": "bar"})
>>> value
'eyJmb28iOiJiYXIifQ:1NMg1b:zGcDE4-TCkaeGzLeW9UQwZesciI'
>>> 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', compress=False)

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

loads(string, key=None, salt='django.core.signing', max_age=None)

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