Signatures cryptographiques

La règle d’or de la sécurité des applications Web est de ne jamais se fier à des données de provenance douteuse. Il peut parfois être pratique de transférer des données par un canal non sécurisé. Les valeurs signées cryptographiquement peuvent être transmises de manière fiable au travers d’un canal non sécurisé en sachant que toute modification de donnée sera détectée.

Django offre à la fois une API de bas niveau pour la signature de valeurs et une API de haut niveau pour la génération et la lecture de cookies signés, l’un des usages les plus courants de la signature dans les applications Web.

La signature peut également être utile dans les situations suivantes :

  • Génération d’URL de « récupération de compte » pour l’envoi à des utilisateurs qui ont perdu leur mot de passe.
  • Garantie de non-altération des données stockées dans des champs de formulaire cachés.
  • Génération d’URL à usage unique pour autoriser un accès temporaire à une ressource protégée, par exemple pour un fichier téléchargeable payé par un utilisateur.

Protection de SECRET_KEY

Lorsque vous créez un nouveau projet Django avec startproject, le fichier settings.py est généré automatiquement et contient une valeur aléatoire pour le réglage SECRET_KEY. Cette valeur est la clé de la sécurisation des données signées, il est donc essentiel de garder cette information secrète, faute de quoi des attaquants pourraient l’utiliser pour générer leurs propres valeurs signées.

Utilisation de l’API de bas niveau

Les méthodes de signature de Django se trouvent dans le module django.core.signing. Pour signer une valeur, commencez par créer une instance de Signer:

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

La signature est collée à la suite de la chaîne de caractères, en utilisant les deux-points comme séparateur. Vous pouvez récupérer la valeur originale au moyen de la méthode unsign:

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

Si vous passez une valeur non textuelle à sign, la valeur sera d’abord forcée à une chaîne avant d’être signée et le résultat de unsign donnera cette valeur textuelle

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

If you wish to protect a list, tuple, or dictionary you can do so using the sign_object() and unsign_object() methods:

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

See Protection de structures de données complexes for more details.

Si la signature ou la valeur a été modifiée d’une manière ou d’une autre, une exception django.core.signing.BadSignature sera levée :

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

Par défaut, la classe Signer utilise le réglage SECRET_KEY pour générer des signatures. Il est possible d’utiliser un clé secrète différente en la transmettant au constructeur de Signer:

>>> signer = Signer('my-other-secret')
>>> value = signer.sign('My string')
>>> value
'My string:EkfQJafvGyiofrdGnuthdxImIJw'
class Signer(key=None, sep=':', salt=None, algorithm=None)

Renvoie un objet signataire utilisant key pour générer des signatures et sep pour séparer les valeurs. sep ne peut pas se trouver dans l’alphabet base64 adapté aux URL. Cet alphabet contient les caractères alphanumériques, le tiret et le soulignement. algorithm doit être un algorithme pris en charge par hashlib, sa valeur par défaut étant 'sha256'.

Changed in Django 3.1:

Le paramètre algorithm a été ajouté.

Changed in Django 3.2:

The sign_object() and unsign_object() methods were added.

Utilisation du paramètre salt

Si vous ne voulez pas que plusieurs occurrences d’une chaîne donnée aient toutes la même empreinte de signature, vous pouvez utiliser le paramètre facultatif salt de la classe Signer. L’utilisation d’un sel (salt) combiné à la clé SECRET_KEY va nourrir et renforcer la fonction de hachage pour la signature.

>>> 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!'}

Un tel emploi du sel place les différentes signatures dans des espaces de noms différents. Une signature provenant d’un espace de noms (une valeur de sel particulière) ne peut pas être utilisée pour valider la même chaîne de texte dans un autre espace de noms utilisant une valeur de sel différente. Il en résulte qu’un attaquant ne peut pas utiliser une chaîne signée générée dans une partie du code comme point d’entrée pour une autre partie du code qui génère (et vérifie) les signatures en employant un sel différent.

Au contraire de SECRET_KEY, le paramètre salt n’a pas besoin de rester secret.

Changed in Django 3.2:

The sign_object() and unsign_object() methods were added.

Vérification de valeurs horodatées

TimestampSigner est une sous-classe de Signer qui ajoute un horodatage signé à la valeur. Cela permet de confirmer qu’une valeur signée a été créée dans un espace de temps défini :

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

Signe la valeur value et lui ajoute l’horodatage actuel.

unsign(value, max_age=None)

Vérifie si la valeur value a été signée il y a moins de max_age secondes, sinon génère SignatureExpired. Le paramètre max_age accepte un nombre entier ou un objet datetime.timedelta.

sign_object(obj, serializer=JSONSerializer, compress=False)
New in Django 3.2.

Encode, optionally compress, append current timestamp, and sign complex data structure (e.g. list, tuple, or dictionary).

unsign_object(signed_obj, serializer=JSONSerializer, max_age=None)
New in Django 3.2.

Checks if signed_obj was signed less than max_age seconds ago, otherwise raises SignatureExpired. The max_age parameter can accept an integer or a datetime.timedelta object.

Changed in Django 3.1:

Le paramètre algorithm a été ajouté.

Protection de structures de données complexes

If you wish to protect a list, tuple or dictionary you can do so using the Signer.sign_object() and unsign_object() methods, or signing module’s dumps() or loads() functions (which are shortcuts for TimestampSigner(salt='django.core.signing').sign_object()/unsign_object()). These use JSON serialization under the hood. JSON ensures that even if your SECRET_KEY is stolen an attacker will not be able to execute arbitrary commands by exploiting the pickle format:

>>> 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'}

En raison de la nature de JSON (il n’existe pas de distinction native entre les listes et les tuples), même si vous passez un tuple, vous obtiendrez une liste dans le résultat de 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)

Renvoie une chaîne JSON utilisable dans une URL, signée et compressée en base64. L’objet sérialisé est signé par TimestampSigner.

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

Inverse de dumps(), génère BadSignature si la vérification de la signature échoue. Vérifie max_age (en secondes) si ce paramètre est défini.

Changed in Django 3.2:

The sign_object() and unsign_object() methods were added.

Back to Top