summaryrefslogtreecommitdiff
path: root/src/itsdangerous/signer.py
blob: 6bddc0396e28c4898f720080e249850ade3492ab (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import hashlib
import hmac

from ._compat import constant_time_compare
from .encoding import _base64_alphabet
from .encoding import base64_decode
from .encoding import base64_encode
from .encoding import want_bytes
from .exc import BadSignature


class SigningAlgorithm(object):
    """Subclasses must implement :meth:`get_signature` to provide
    signature generation functionality.
    """

    def get_signature(self, key, value):
        """Returns the signature for the given key and value."""
        raise NotImplementedError()

    def verify_signature(self, key, value, sig):
        """Verifies the given signature matches the expected
        signature.
        """
        return constant_time_compare(sig, self.get_signature(key, value))


class NoneAlgorithm(SigningAlgorithm):
    """Provides an algorithm that does not perform any signing and
    returns an empty signature.
    """

    def get_signature(self, key, value):
        return b""


class HMACAlgorithm(SigningAlgorithm):
    """Provides signature generation using HMACs."""

    #: The digest method to use with the MAC algorithm. This defaults to
    #: SHA1, but can be changed to any other function in the hashlib
    #: module.
    default_digest_method = staticmethod(hashlib.sha1)

    def __init__(self, digest_method=None):
        if digest_method is None:
            digest_method = self.default_digest_method
        self.digest_method = digest_method

    def get_signature(self, key, value):
        mac = hmac.new(key, msg=value, digestmod=self.digest_method)
        return mac.digest()


class Signer(object):
    """This class can sign and unsign bytes, validating the signature
    provided.

    Salt can be used to namespace the hash, so that a signed string is
    only valid for a given namespace. Leaving this at the default value
    or re-using a salt value across different parts of your application
    where the same signed value in one part can mean something different
    in another part is a security risk.

    See :ref:`the-salt` for an example of what the salt is doing and how
    you can utilize it.

    .. versionadded:: 0.14
        ``key_derivation`` and ``digest_method`` were added as arguments
        to the class constructor.

    .. versionadded:: 0.18
        ``algorithm`` was added as an argument to the class constructor.
    """

    #: The digest method to use for the signer.  This defaults to
    #: SHA1 but can be changed to any other function in the hashlib
    #: module.
    #:
    #: .. versionadded:: 0.14
    default_digest_method = staticmethod(hashlib.sha1)

    #: Controls how the key is derived. The default is Django-style
    #: concatenation. Possible values are ``concat``, ``django-concat``
    #: and ``hmac``. This is used for deriving a key from the secret key
    #: with an added salt.
    #:
    #: .. versionadded:: 0.14
    default_key_derivation = "django-concat"

    def __init__(
        self,
        secret_key,
        salt=None,
        sep=".",
        key_derivation=None,
        digest_method=None,
        algorithm=None,
    ):
        self.secret_key = want_bytes(secret_key)
        self.sep = want_bytes(sep)
        if self.sep in _base64_alphabet:
            raise ValueError(
                "The given separator cannot be used because it may be"
                " contained in the signature itself. Alphanumeric"
                " characters and `-_=` must not be used."
            )
        self.salt = "itsdangerous.Signer" if salt is None else salt
        if key_derivation is None:
            key_derivation = self.default_key_derivation
        self.key_derivation = key_derivation
        if digest_method is None:
            digest_method = self.default_digest_method
        self.digest_method = digest_method
        if algorithm is None:
            algorithm = HMACAlgorithm(self.digest_method)
        self.algorithm = algorithm

    def derive_key(self):
        """This method is called to derive the key. The default key
        derivation choices can be overridden here. Key derivation is not
        intended to be used as a security method to make a complex key
        out of a short password. Instead you should use large random
        secret keys.
        """
        salt = want_bytes(self.salt)
        if self.key_derivation == "concat":
            return self.digest_method(salt + self.secret_key).digest()
        elif self.key_derivation == "django-concat":
            return self.digest_method(salt + b"signer" + self.secret_key).digest()
        elif self.key_derivation == "hmac":
            mac = hmac.new(self.secret_key, digestmod=self.digest_method)
            mac.update(salt)
            return mac.digest()
        elif self.key_derivation == "none":
            return self.secret_key
        else:
            raise TypeError("Unknown key derivation method")

    def get_signature(self, value):
        """Returns the signature for the given value."""
        value = want_bytes(value)
        key = self.derive_key()
        sig = self.algorithm.get_signature(key, value)
        return base64_encode(sig)

    def sign(self, value):
        """Signs the given string."""
        return want_bytes(value) + want_bytes(self.sep) + self.get_signature(value)

    def verify_signature(self, value, sig):
        """Verifies the signature for the given value."""
        key = self.derive_key()
        try:
            sig = base64_decode(sig)
        except Exception:
            return False
        return self.algorithm.verify_signature(key, value, sig)

    def unsign(self, signed_value):
        """Unsigns the given string."""
        signed_value = want_bytes(signed_value)
        sep = want_bytes(self.sep)
        if sep not in signed_value:
            raise BadSignature("No %r found in value" % self.sep)
        value, sig = signed_value.rsplit(sep, 1)
        if self.verify_signature(value, sig):
            return value
        raise BadSignature("Signature %r does not match" % sig, payload=value)

    def validate(self, signed_value):
        """Only validates the given signed value. Returns ``True`` if
        the signature exists and is valid.
        """
        try:
            self.unsign(signed_value)
            return True
        except BadSignature:
            return False