1from __future__ import annotations
2
3import collections.abc as cabc
4import hashlib
5import hmac
6import typing as t
7
8from .encoding import _base64_alphabet
9from .encoding import base64_decode
10from .encoding import base64_encode
11from .encoding import want_bytes
12from .exc import BadSignature
13
14
15class SigningAlgorithm:
16 """Subclasses must implement :meth:`get_signature` to provide
17 signature generation functionality.
18 """
19
20 def get_signature(self, key: bytes, value: bytes) -> bytes:
21 """Returns the signature for the given key and value."""
22 raise NotImplementedError()
23
24 def verify_signature(self, key: bytes, value: bytes, sig: bytes) -> bool:
25 """Verifies the given signature matches the expected
26 signature.
27 """
28 return hmac.compare_digest(sig, self.get_signature(key, value))
29
30
31class NoneAlgorithm(SigningAlgorithm):
32 """Provides an algorithm that does not perform any signing and
33 returns an empty signature.
34 """
35
36 def get_signature(self, key: bytes, value: bytes) -> bytes:
37 return b""
38
39
40def _lazy_sha1(string: bytes = b"") -> t.Any:
41 """Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include
42 SHA-1, in which case the import and use as a default would fail before the
43 developer can configure something else.
44 """
45 return hashlib.sha1(string)
46
47
48class HMACAlgorithm(SigningAlgorithm):
49 """Provides signature generation using HMACs."""
50
51 #: The digest method to use with the MAC algorithm. This defaults to
52 #: SHA1, but can be changed to any other function in the hashlib
53 #: module.
54 default_digest_method: t.Any = staticmethod(_lazy_sha1)
55
56 def __init__(self, digest_method: t.Any = None):
57 if digest_method is None:
58 digest_method = self.default_digest_method
59
60 self.digest_method: t.Any = digest_method
61
62 def get_signature(self, key: bytes, value: bytes) -> bytes:
63 mac = hmac.new(key, msg=value, digestmod=self.digest_method)
64 return mac.digest()
65
66
67def _make_keys_list(
68 secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes],
69) -> list[bytes]:
70 if isinstance(secret_key, (str, bytes)):
71 return [want_bytes(secret_key)]
72
73 return [want_bytes(s) for s in secret_key] # pyright: ignore
74
75
76class Signer:
77 """A signer securely signs bytes, then unsigns them to verify that
78 the value hasn't been changed.
79
80 The secret key should be a random string of ``bytes`` and should not
81 be saved to code or version control. Different salts should be used
82 to distinguish signing in different contexts. See :doc:`/concepts`
83 for information about the security of the secret key and salt.
84
85 :param secret_key: The secret key to sign and verify with. Can be a
86 list of keys, oldest to newest, to support key rotation.
87 :param salt: Extra key to combine with ``secret_key`` to distinguish
88 signatures in different contexts.
89 :param sep: Separator between the signature and value.
90 :param key_derivation: How to derive the signing key from the secret
91 key and salt. Possible values are ``concat``, ``django-concat``,
92 or ``hmac``. Defaults to :attr:`default_key_derivation`, which
93 defaults to ``django-concat``.
94 :param digest_method: Hash function to use when generating the HMAC
95 signature. Defaults to :attr:`default_digest_method`, which
96 defaults to :func:`hashlib.sha1`. Note that the security of the
97 hash alone doesn't apply when used intermediately in HMAC.
98 :param algorithm: A :class:`SigningAlgorithm` instance to use
99 instead of building a default :class:`HMACAlgorithm` with the
100 ``digest_method``.
101
102 .. versionchanged:: 2.0
103 Added support for key rotation by passing a list to
104 ``secret_key``.
105
106 .. versionchanged:: 0.18
107 ``algorithm`` was added as an argument to the class constructor.
108
109 .. versionchanged:: 0.14
110 ``key_derivation`` and ``digest_method`` were added as arguments
111 to the class constructor.
112 """
113
114 #: The default digest method to use for the signer. The default is
115 #: :func:`hashlib.sha1`, but can be changed to any :mod:`hashlib` or
116 #: compatible object. Note that the security of the hash alone
117 #: doesn't apply when used intermediately in HMAC.
118 #:
119 #: .. versionadded:: 0.14
120 default_digest_method: t.Any = staticmethod(_lazy_sha1)
121
122 #: The default scheme to use to derive the signing key from the
123 #: secret key and salt. The default is ``django-concat``. Possible
124 #: values are ``concat``, ``django-concat``, and ``hmac``.
125 #:
126 #: .. versionadded:: 0.14
127 default_key_derivation: str = "django-concat"
128
129 def __init__(
130 self,
131 secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes],
132 salt: str | bytes | None = b"itsdangerous.Signer",
133 sep: str | bytes = b".",
134 key_derivation: str | None = None,
135 digest_method: t.Any | None = None,
136 algorithm: SigningAlgorithm | None = None,
137 ):
138 #: The list of secret keys to try for verifying signatures, from
139 #: oldest to newest. The newest (last) key is used for signing.
140 #:
141 #: This allows a key rotation system to keep a list of allowed
142 #: keys and remove expired ones.
143 self.secret_keys: list[bytes] = _make_keys_list(secret_key)
144 self.sep: bytes = want_bytes(sep)
145
146 if self.sep in _base64_alphabet:
147 raise ValueError(
148 "The given separator cannot be used because it may be"
149 " contained in the signature itself. ASCII letters,"
150 " digits, and '-_=' must not be used."
151 )
152
153 if salt is not None:
154 salt = want_bytes(salt)
155 else:
156 salt = b"itsdangerous.Signer"
157
158 self.salt = salt
159
160 if key_derivation is None:
161 key_derivation = self.default_key_derivation
162
163 self.key_derivation: str = key_derivation
164
165 if digest_method is None:
166 digest_method = self.default_digest_method
167
168 self.digest_method: t.Any = digest_method
169
170 if algorithm is None:
171 algorithm = HMACAlgorithm(self.digest_method)
172
173 self.algorithm: SigningAlgorithm = algorithm
174
175 @property
176 def secret_key(self) -> bytes:
177 """The newest (last) entry in the :attr:`secret_keys` list. This
178 is for compatibility from before key rotation support was added.
179 """
180 return self.secret_keys[-1]
181
182 def derive_key(self, secret_key: str | bytes | None = None) -> bytes:
183 """This method is called to derive the key. The default key
184 derivation choices can be overridden here. Key derivation is not
185 intended to be used as a security method to make a complex key
186 out of a short password. Instead you should use large random
187 secret keys.
188
189 :param secret_key: A specific secret key to derive from.
190 Defaults to the last item in :attr:`secret_keys`.
191
192 .. versionchanged:: 2.0
193 Added the ``secret_key`` parameter.
194 """
195 if secret_key is None:
196 secret_key = self.secret_keys[-1]
197 else:
198 secret_key = want_bytes(secret_key)
199
200 if self.key_derivation == "concat":
201 return t.cast(bytes, self.digest_method(self.salt + secret_key).digest())
202 elif self.key_derivation == "django-concat":
203 return t.cast(
204 bytes, self.digest_method(self.salt + b"signer" + secret_key).digest()
205 )
206 elif self.key_derivation == "hmac":
207 mac = hmac.new(secret_key, digestmod=self.digest_method)
208 mac.update(self.salt)
209 return mac.digest()
210 elif self.key_derivation == "none":
211 return secret_key
212 else:
213 raise TypeError("Unknown key derivation method")
214
215 def get_signature(self, value: str | bytes) -> bytes:
216 """Returns the signature for the given value."""
217 value = want_bytes(value)
218 key = self.derive_key()
219 sig = self.algorithm.get_signature(key, value)
220 return base64_encode(sig)
221
222 def sign(self, value: str | bytes) -> bytes:
223 """Signs the given string."""
224 value = want_bytes(value)
225 return value + self.sep + self.get_signature(value)
226
227 def verify_signature(self, value: str | bytes, sig: str | bytes) -> bool:
228 """Verifies the signature for the given value."""
229 try:
230 sig = base64_decode(sig)
231 except Exception:
232 return False
233
234 value = want_bytes(value)
235
236 for secret_key in reversed(self.secret_keys):
237 key = self.derive_key(secret_key)
238
239 if self.algorithm.verify_signature(key, value, sig):
240 return True
241
242 return False
243
244 def unsign(self, signed_value: str | bytes) -> bytes:
245 """Unsigns the given string."""
246 signed_value = want_bytes(signed_value)
247
248 if self.sep not in signed_value:
249 raise BadSignature(f"No {self.sep!r} found in value")
250
251 value, sig = signed_value.rsplit(self.sep, 1)
252
253 if self.verify_signature(value, sig):
254 return value
255
256 raise BadSignature(f"Signature {sig!r} does not match", payload=value)
257
258 def validate(self, signed_value: str | bytes) -> bool:
259 """Only validates the given signed value. Returns ``True`` if
260 the signature exists and is valid.
261 """
262 try:
263 self.unsign(signed_value)
264 return True
265 except BadSignature:
266 return False