Signer.__create_string_to_sign()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 18
rs 9.4285
c 0
b 0
f 0
1
# -*- coding: utf-8 -*-
2
3
"""
4
Copyright (c) 2015-2016 WePay.
5
6
Based on a stripped-down version of the AWS Signature v4 implementation.
7
8
http://opensource.org/licenses/Apache2.0
9
"""
10
11
from __future__ import print_function
12
import hashlib
13
import hmac
14
import six
15
16
17
class Signer(object):
18
    """
19
    The Signer class is designed for those who are signing data on behalf of a public-private keypair.
20
21
    In principle, the "client party" has public key (i.e., `client_id`) has a matching private key
22
    (i.e., `client_secret`) that can be verified by both the signer, as well as the client, but
23
    by nobody else as we don't want to make forgeries possible.
24
25
    The "signing party" has a simple an identifier which acts as an additional piece of entropy in the
26
    algorithm, and can help differentiate between multiple signing parties if the client party does
27
    something like try to use the same public-private keypair independently of a signing party
28
    (as is common with GPG signing).
29
30
    For example, in the original AWS implementation, the "self key" for AWS was "AWS4".
31
    """
32
33
    def __init__(self, client_id, client_secret, options=None):
34
        """
35
        Constructs a new instance of this class.
36
37
        @param client_id [String] A string which is the public portion of the keypair identifying the client party. The
38
            pairing of the public and private portions of the keypair should only be known to the client party and the
39
            signing party.
40
        @param client_secret [String] A string which is the private portion of the keypair identifying the client party.
41
            The pairing of the public and private portions of the keypair should only be known to the client party and
42
            the signing party.
43
        @option options [String] self_key (WePay) A string which identifies the signing party and adds additional
44
            entropy.
45
        @option options [String] hash_algo (sha512) The hash algorithm to use for signing.
46
        """
47
48
        if options is None:
49
            options = {}
50
51
        self.client_id = "{client_id}".format(client_id=client_id)
52
        self.client_secret = "{client_secret}".format(client_secret=client_secret)
53
54
        merged_options = options.copy()
55
        merged_options.update({
56
            "self_key":  "WePay",
57
            "hash_algo": hashlib.sha512,
58
        })
59
60
        self.self_key = merged_options["self_key"]
61
        self.hash_algo = merged_options["hash_algo"]
62
63
    def sign(self, payload):
64
        """
65
        Sign the payload to produce a signature for its contents.
66
67
        @param payload [Hash] The data to generate a signature for.
68
        @option payload [required, String] token The one-time-use token.
69
        @option payload [required, String] page The WePay URL to access.
70
        @option payload [required, String] redirect_uri The partner URL to return to once the action is completed.
71
        @return [String] The signature for the payload contents.
72
        """
73
74
        merged_payload = payload.copy()
75
        merged_payload.update({
76
            'client_id':     self.client_id,
77
            'client_secret': self.client_secret,
78
        })
79
80
        scope = self.__create_scope()
81
        context = self.__create_context(merged_payload)
82
        s2s = self.__create_string_to_sign(scope, context)
83
        signing_key = self.__get_signing_salt()
84
85
        signature = hmac.new(
86
            signing_key,
87
            six.u(s2s).encode('utf-8'),
88
            self.hash_algo
89
        ).hexdigest()
90
91
        return signature
92
93
    def generate_query_string_params(self, payload):
94
        """
95
        Signs and generates the query string URL parameters to use when making a request.
96
97
        If the `client_secret` key is provided, then it will be automatically excluded from the result.
98
99
        @param  payload [Hash] The data to generate a signature for.
100
        @option payload [required, String] token The one-time-use token.
101
        @option payload [required, String] page The WePay URL to access.
102
        @option payload [required, String] redirect_uri The partner URL to return to once the action is completed.
103
        @return [String] The query string parameters to append to the end of a URL.
104
        """
105
106
        payload.pop('client_secret', None)
107
108
        signed_token = self.sign(payload)
109
        payload['client_id'] = self.client_id
110
        payload['stoken'] = signed_token
111
        qsa = []
112
113
        payload_keys = list(six.viewkeys(payload))
114
        payload_keys.sort()
115
116
        for key in payload_keys:
117
            qsa.append("{}={}".format(key, payload[key]))
118
119
        return "&".join(qsa)
120
121
    # --------------------------------------------------------------------------
122
    # Private
123
124
    def __create_string_to_sign(self, scope, context):
125
        """
126
        Creates the string-to-sign based on a variety of factors.
127
128
        @param scope [String] The results of a call to the `__create_scope()` method.
129
        @param context [String] The results of a call to the `__create_context()` method.
130
        @return [String] The final string to be signed.
131
        """
132
133
        scope_hash = hashlib.new(self.hash_algo().name, scope.encode('utf-8')).hexdigest()
134
        context_hash = hashlib.new(self.hash_algo().name, context.encode('utf-8')).hexdigest()
135
136
        return "SIGNER-HMAC-{hash_algo}\n{self_key}\n{client_id}\n{scope_hash}\n{context_hash}".format(
137
            hash_algo=self.hash_algo().name.upper(),
138
            self_key=self.self_key,
139
            client_id=self.client_id,
140
            scope_hash=scope_hash,
141
            context_hash=context_hash
142
        )
143
144
    @staticmethod
145
    def __create_context(payload):
146
        """
147
        An array of key-value pairs representing the data that you want to sign.
148
        All values must be `scalar`.
149
150
        @param  payload [Hash] The data that you want to sign.
151
        @option payload [String] self_key (WePay) A string which identifies the signing party and adds additional
152
            entropy.
153
        @return [String] A canonical string representation of the data to sign.
154
        """
155
156
        canonical_payload = []
157
158
        for k in six.viewkeys(payload):
159
            val = "{}".format(payload[k]).lower()
160
            key = "{}".format(k).lower()
161
            canonical_payload.append("{}={}\n".format(key, val))
162
163
        canonical_payload.sort()
164
165
        sorted_keys = list(six.viewkeys(payload))
166
        sorted_keys.sort()
167
168
        signed_headers_string = ";".join(sorted_keys)
169
        canonical_payload_string = "".join(canonical_payload) + "\n" + signed_headers_string
170
171
        return canonical_payload_string
172
173
    def __get_signing_salt(self):
174
        """
175
        Gets the salt value that should be used for signing.
176
177
        @return [String] The signing salt.
178
        """
179
180
        self_key_sign = hmac.new(
181
            six.b(self.client_secret),
182
            six.u(self.self_key).encode('utf-8'),
183
            self.hash_algo
184
        ).digest()
185
186
        client_id_sign = hmac.new(
187
            self_key_sign,
188
            six.u(self.client_id).encode('utf-8'),
189
            self.hash_algo
190
        ).digest()
191
192
        signer_sign = hmac.new(
193
            client_id_sign,
194
            six.u('signer').encode('utf-8'),
195
            self.hash_algo
196
        ).digest()
197
198
        return signer_sign
199
200
    def __create_scope(self):
201
        """
202
        Creates the "scope" in which the signature is valid.
203
204
        @return [String] The string which represents the scope in which the signature is valid.
205
        """
206
207
        return "{self_key}/{client_id}/signer".format(
208
            self_key=self.self_key,
209
            client_id=self.client_id
210
        )
211