~ubuntu-branches/ubuntu/trusty/python-keystoneclient/trusty-proposed

« back to all changes in this revision

Viewing changes to keystoneclient/contrib/ec2/utils.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short
  • Date: 2013-05-29 13:07:56 UTC
  • mfrom: (1.1.19)
  • Revision ID: package-import@ubuntu.com-20130529130756-3h7dh05a39n9uvq5
Tags: 1:0.2.4-0ubuntu1
* New upstream release. 
* debian/control: Add python-d2to1 and python-pbr
* debian/control: Add testrepository, dropped python-nose
* debian/control: Add python-six

Show diffs side-by-side

added added

removed removed

Lines of Context:
32
32
    """
33
33
 
34
34
    def __init__(self, secret_key):
35
 
        secret_key = secret_key.encode()
36
 
        self.hmac = hmac.new(secret_key, digestmod=hashlib.sha1)
 
35
        self.secret_key = secret_key.encode()
 
36
        self.hmac = hmac.new(self.secret_key, digestmod=hashlib.sha1)
37
37
        if hashlib.sha256:
38
 
            self.hmac_256 = hmac.new(secret_key, digestmod=hashlib.sha256)
 
38
            self.hmac_256 = hmac.new(self.secret_key, digestmod=hashlib.sha256)
 
39
 
 
40
    def _v4_creds(self, credentials):
 
41
        """
 
42
        Detect if the credentials are for a v4 signed request, since AWS
 
43
        removed the SignatureVersion field from the v4 request spec...
 
44
        This expects a dict of the request headers to be passed in the
 
45
        credentials dict, since the recommended way to pass v4 creds is
 
46
        via the 'Authorization' header
 
47
        see http://docs.aws.amazon.com/general/latest/gr/
 
48
            sigv4-signed-request-examples.html
 
49
 
 
50
        Alternatively X-Amz-Algorithm can be specified as a query parameter,
 
51
        and the authentication data can also passed as query parameters.
 
52
 
 
53
        Note a hash of the request body is also required in the credentials
 
54
        for v4 auth to work in the body_hash key, calculated via:
 
55
        hashlib.sha256(req.body).hexdigest()
 
56
        """
 
57
        try:
 
58
            auth_str = credentials['headers']['Authorization']
 
59
            if auth_str.startswith('AWS4-HMAC-SHA256'):
 
60
                return True
 
61
        except KeyError:
 
62
            # Alternatively the Authorization data can be passed via
 
63
            # the query params list, check X-Amz-Algorithm=AWS4-HMAC-SHA256
 
64
            try:
 
65
                if (credentials['params']['X-Amz-Algorithm'] ==
 
66
                    'AWS4-HMAC-SHA256'):
 
67
                    return True
 
68
            except KeyError:
 
69
                pass
 
70
 
 
71
        return False
39
72
 
40
73
    def generate(self, credentials):
41
74
        """Generate auth string according to what SignatureVersion is given."""
42
 
        if credentials['params']['SignatureVersion'] == '0':
 
75
        signature_version = credentials['params'].get('SignatureVersion')
 
76
        if signature_version == '0':
43
77
            return self._calc_signature_0(credentials['params'])
44
 
        if credentials['params']['SignatureVersion'] == '1':
 
78
        if signature_version == '1':
45
79
            return self._calc_signature_1(credentials['params'])
46
 
        if credentials['params']['SignatureVersion'] == '2':
 
80
        if signature_version == '2':
47
81
            return self._calc_signature_2(credentials['params'],
48
82
                                          credentials['verb'],
49
83
                                          credentials['host'],
50
84
                                          credentials['path'])
51
 
        raise Exception('Unknown Signature Version: %s' %
52
 
                        credentials['params']['SignatureVersion'])
 
85
        if self._v4_creds(credentials):
 
86
            return self._calc_signature_4(credentials['params'],
 
87
                                          credentials['verb'],
 
88
                                          credentials['host'],
 
89
                                          credentials['path'],
 
90
                                          credentials['headers'],
 
91
                                          credentials['body_hash'])
 
92
 
 
93
        if signature_version is not None:
 
94
            raise Exception('Unknown signature version: %s' %
 
95
                            signature_version)
 
96
        else:
 
97
            raise Exception('Unexpected signature format')
53
98
 
54
99
    @staticmethod
55
100
    def _get_utf8_value(value):
77
122
            self.hmac.update(val)
78
123
        return base64.b64encode(self.hmac.digest())
79
124
 
 
125
    @staticmethod
 
126
    def _canonical_qs(params):
 
127
        """
 
128
        Construct a sorted, correctly encoded query string as required for
 
129
        _calc_signature_2 and _calc_signature_4
 
130
        """
 
131
        keys = params.keys()
 
132
        keys.sort()
 
133
        pairs = []
 
134
        for key in keys:
 
135
            val = Ec2Signer._get_utf8_value(params[key])
 
136
            val = urllib.quote(val, safe='-_~')
 
137
            pairs.append(urllib.quote(key, safe='') + '=' + val)
 
138
        qs = '&'.join(pairs)
 
139
        return qs
 
140
 
80
141
    def _calc_signature_2(self, params, verb, server_string, path):
81
142
        """Generate AWS signature version 2 string."""
82
143
        string_to_sign = '%s\n%s\n%s\n' % (verb, server_string, path)
86
147
        else:
87
148
            current_hmac = self.hmac
88
149
            params['SignatureMethod'] = 'HmacSHA1'
89
 
        keys = params.keys()
90
 
        keys.sort()
91
 
        pairs = []
92
 
        for key in keys:
93
 
            val = self._get_utf8_value(params[key])
94
 
            val = urllib.quote(val, safe='-_~')
95
 
            pairs.append(urllib.quote(key, safe='') + '=' + val)
96
 
        qs = '&'.join(pairs)
97
 
        string_to_sign += qs
 
150
        string_to_sign += self._canonical_qs(params)
98
151
        current_hmac.update(string_to_sign)
99
152
        b64 = base64.b64encode(current_hmac.digest())
100
153
        return b64
 
154
 
 
155
    def _calc_signature_4(self, params, verb, server_string, path, headers,
 
156
                          body_hash):
 
157
        """Generate AWS signature version 4 string."""
 
158
 
 
159
        def sign(key, msg):
 
160
            return hmac.new(key, self._get_utf8_value(msg),
 
161
                            hashlib.sha256).digest()
 
162
 
 
163
        def signature_key(datestamp, region_name, service_name):
 
164
            """
 
165
            Signature key derivation, see
 
166
            http://docs.aws.amazon.com/general/latest/gr/
 
167
            signature-v4-examples.html#signature-v4-examples-python
 
168
            """
 
169
            k_date = sign(self._get_utf8_value("AWS4" + self.secret_key),
 
170
                          datestamp)
 
171
            k_region = sign(k_date, region_name)
 
172
            k_service = sign(k_region, service_name)
 
173
            k_signing = sign(k_service, "aws4_request")
 
174
            return k_signing
 
175
 
 
176
        def auth_param(param_name):
 
177
            """
 
178
            Get specified auth parameter, provided via one of:
 
179
            - the Authorization header
 
180
            - the X-Amz-* query parameters
 
181
            """
 
182
            try:
 
183
                auth_str = headers['Authorization']
 
184
                param_str = auth_str.partition(
 
185
                                '%s=' % param_name)[2].split(',')[0]
 
186
            except KeyError:
 
187
                param_str = params.get('X-Amz-%s' % param_name)
 
188
            return param_str
 
189
 
 
190
        def date_param():
 
191
            """
 
192
            Get the X-Amz-Date' value, which can be either a header or paramter
 
193
 
 
194
            Note AWS supports parsing the Date header also, but this is not
 
195
            currently supported here as it will require some format mangling
 
196
            So the X-Amz-Date value must be YYYYMMDDTHHMMSSZ format, then it
 
197
            can be used to match against the YYYYMMDD format provided in the
 
198
            credential scope.
 
199
            see:
 
200
            http://docs.aws.amazon.com/general/latest/gr/
 
201
            sigv4-date-handling.html
 
202
            """
 
203
            try:
 
204
                return headers['X-Amz-Date']
 
205
            except KeyError:
 
206
                return params.get('X-Amz-Date')
 
207
 
 
208
        def canonical_header_str():
 
209
            # Get the list of headers to include, from either
 
210
            # - the Authorization header (SignedHeaders key)
 
211
            # - the X-Amz-SignedHeaders query parameter
 
212
            headers_lower = dict((k.lower().strip(), v.strip())
 
213
                                 for (k, v) in headers.iteritems())
 
214
            header_list = []
 
215
            sh_str = auth_param('SignedHeaders')
 
216
            for h in sh_str.split(';'):
 
217
                if h not in headers_lower:
 
218
                    continue
 
219
                if h == 'host':
 
220
                    # Note we discard any port suffix
 
221
                    header_list.append('%s:%s' %
 
222
                                       (h, headers_lower[h].split(':')[0]))
 
223
                else:
 
224
                    header_list.append('%s:%s' % (h, headers_lower[h]))
 
225
            return '\n'.join(header_list) + '\n'
 
226
 
 
227
        # Create canonical request:
 
228
        # http://docs.aws.amazon.com/general/latest/gr/
 
229
        # sigv4-create-canonical-request.html
 
230
        # Get parameters and headers in expected string format
 
231
        cr = "\n".join((verb.upper(), path,
 
232
                        self._canonical_qs(params),
 
233
                        canonical_header_str(),
 
234
                        auth_param('SignedHeaders'),
 
235
                        body_hash))
 
236
 
 
237
        # Check the date, reject any request where the X-Amz-Date doesn't
 
238
        # match the credential scope
 
239
        credential = auth_param('Credential')
 
240
        credential_split = credential.split('/')
 
241
        credential_scope = '/'.join(credential_split[1:])
 
242
        credential_date = credential_split[1]
 
243
        param_date = date_param()
 
244
        if not param_date.startswith(credential_date):
 
245
            raise Exception('Request date mismatch error')
 
246
 
 
247
        # Create the string to sign
 
248
        # http://docs.aws.amazon.com/general/latest/gr/
 
249
        # sigv4-create-string-to-sign.html
 
250
        string_to_sign = '\n'.join(('AWS4-HMAC-SHA256',
 
251
                                    param_date,
 
252
                                    credential_scope,
 
253
                                    hashlib.sha256(cr).hexdigest()))
 
254
 
 
255
        # Calculate the derived key, this requires a datestamp, region
 
256
        # and service, which can be extracted from the credential scope
 
257
        (req_region, req_service) = credential_split[2:4]
 
258
        s_key = signature_key(credential_date, req_region, req_service)
 
259
        # Finally calculate the signature!
 
260
        signature = hmac.new(s_key, self._get_utf8_value(string_to_sign),
 
261
                             hashlib.sha256).hexdigest()
 
262
        return signature