Skip to main content

Cryptographic Signing for Data Integrity

Cryptographic signing in this project provides a mechanism to verify the integrity of data exchanged over untrusted channels, such as HTTP cookies or URL parameters. Unlike encryption, which hides data, signing ensures that the data has not been tampered with since it was generated. This is achieved using Hash-based Message Authentication Codes (HMAC).

The Foundation of Integrity: The Signer Class

The Signer class in django/core/signing.py is the primary tool for generating and verifying signatures. It combines a value with a secret key and a salt to produce a signature that is appended to the original value.

HMAC and Algorithms

By default, Signer uses the SHA-256 algorithm to generate signatures. The signature is created by the signature method, which utilizes django.utils.crypto.salted_hmac.

from django.core.signing import Signer

signer = Signer()
signed_value = signer.sign("my-data")
# Result: 'my-data:signature_string'

The implementation defaults to using settings.SECRET_KEY as the signing key. To prevent timing attacks, the unsign method uses django.utils.crypto.constant_time_compare when verifying the provided signature against the expected one.

Namespace Isolation with Salts

A critical design choice in the Signer implementation is the use of salts. Salts provide namespace isolation, ensuring that a signed string created for one part of the application cannot be reused in another.

In Signer.__init__, if no salt is provided, it defaults to a string based on the module and class name:

self.salt = salt or "%s.%s" % (
self.__class__.__module__,
self.__class__.__name__,
)

This prevents a signed session ID from being valid if passed to a password reset view, even if both use the same SECRET_KEY.

Temporal Integrity with TimestampSigner

For data that should only be valid for a limited time—such as password reset tokens or invitation links—the TimestampSigner class extends Signer to include a signed timestamp.

The sign method in TimestampSigner appends a base62-encoded timestamp to the value before signing the entire string:

def sign(self, value):
value = "%s%s%s" % (value, self.sep, self.timestamp())
return super().sign(value)

When verifying with unsign, you can provide a max_age (in seconds or as a timedelta). The method extracts the timestamp, decodes it, and compares it against the current time. If the signature is too old, it raises a SignatureExpired exception.

Signing Complex Objects

While Signer handles strings, the dumps and loads functions (and the sign_object method) allow for signing complex Python objects like dictionaries or lists.

Serialization and Encoding

The JSONSerializer class is used by default to convert objects into a compact JSON format. It uses separators=(",", ":") to eliminate whitespace, minimizing the payload size.

class JSONSerializer:
def dumps(self, obj):
return json.dumps(obj, separators=(",", ":")).encode("latin-1")

The resulting JSON is then base64-encoded to ensure it is URL-safe.

Compression Tradeoffs

The sign_object method includes an optional compress parameter. When enabled, it uses zlib to compress the serialized data. However, the implementation includes a check to ensure compression is actually beneficial:

if compress:
compressed = zlib.compress(data)
if len(compressed) < (len(data) - 1):
data = compressed
is_compressed = True

If the data is compressed, a . is prepended to the base64 string. This flag is included in the signed portion of the string, protecting the application against "zip bomb" attacks where an attacker might try to force the server to decompress a malicious payload.

Security and Key Rotation

The signing module is designed to handle security transitions gracefully through key rotation. The Signer class accepts a fallback_keys argument, which defaults to settings.SECRET_KEY_FALLBACKS.

During the unsign process, if the primary SECRET_KEY fails to verify the signature, the implementation iterates through the fallback keys:

def unsign(self, signed_value):
# ... extraction logic ...
for key in [self.key, *self.fallback_keys]:
if constant_time_compare(sig, self.signature(value, key)):
return value
raise BadSignature('Signature "%s" does not match' % sig)

This allows developers to rotate the primary SECRET_KEY without immediately invalidating all existing signed data (like user sessions), provided the old key is moved to the fallback list.

Practical Application: Signed Cookies

A major use case for this module is in django/http/request.py, where get_signed_cookie uses a specialized signer to retrieve data from the client. This ensures that users cannot modify their own cookie data (e.g., changing a user ID or a shopping cart total) without the server detecting the tampering via a BadSignature exception.