1# -*- coding: utf-8 -*-
2# MinIO Python Library for Amazon S3 Compatible Cloud Storage, (C) 2015 MinIO, Inc.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16
17"""
18minio.signer
19~~~~~~~~~~~~~~~
20
21This module implements all helpers for AWS Signature version '4' support.
22
23:copyright: (c) 2015 by MinIO, Inc.
24:license: Apache 2.0, see LICENSE for more details.
25
26"""
27
28import collections
29import hashlib
30import hmac
31
32from datetime import datetime
33from .error import InvalidArgumentError
34from .compat import urlsplit, queryencode
35from .helpers import get_sha256_hexdigest
36from .fold_case_dict import FoldCaseDict
37
38# Signature version '4' algorithm.
39_SIGN_V4_ALGORITHM = 'AWS4-HMAC-SHA256'
40
41# Hardcoded S3 header value for X-Amz-Content-Sha256
42_UNSIGNED_PAYLOAD = u'UNSIGNED-PAYLOAD'
43
44def post_presign_signature(date, region, secret_key, policy_str):
45    """
46    Calculates signature version '4' for POST policy string.
47
48    :param date: datetime formatted date.
49    :param region: region of the bucket for the policy.
50    :param secret_key: Amazon S3 secret access key.
51    :param policy_str: policy string.
52    :return: hexlified sha256 signature digest.
53    """
54    signing_key = generate_signing_key(date, region, secret_key)
55    signature = hmac.new(signing_key, policy_str.encode('utf-8'),
56                         hashlib.sha256).hexdigest()
57
58    return signature
59
60
61def presign_v4(method, url, access_key, secret_key, session_token=None,
62               region=None, headers=None, expires=None, response_headers=None,
63               request_date=None):
64    """
65    Calculates signature version '4' for regular presigned URLs.
66
67    :param method: Method to be presigned examples 'PUT', 'GET'.
68    :param url: URL to be presigned.
69    :param access_key: Access key id for your AWS s3 account.
70    :param secret_key: Secret access key for your AWS s3 account.
71    :param session_token: Session token key set only for temporary
72       access credentials.
73    :param region: region of the bucket, it is optional.
74    :param headers: any additional HTTP request headers to
75       be presigned, it is optional.
76    :param expires: final expiration of the generated URL. Maximum is 7days.
77    :param response_headers: Specify additional query string parameters.
78    :param request_date: the date of the request.
79    """
80
81    # Validate input arguments.
82    if not access_key or not secret_key:
83        raise InvalidArgumentError('Invalid access_key and secret_key.')
84
85    if region is None:
86        region = 'us-east-1'
87
88    if headers is None:
89        headers = {}
90
91    if expires is None:
92        expires = '604800'
93
94    if request_date is None:
95        request_date = datetime.utcnow()
96
97    parsed_url = urlsplit(url)
98    content_hash_hex = _UNSIGNED_PAYLOAD
99    host = parsed_url.netloc
100    headers['Host'] = host
101    iso8601Date = request_date.strftime("%Y%m%dT%H%M%SZ")
102
103    headers_to_sign = headers
104    # Construct queries.
105    query = {}
106    query['X-Amz-Algorithm'] = _SIGN_V4_ALGORITHM
107    query['X-Amz-Credential'] = generate_credential_string(access_key,
108                                                           request_date,
109                                                           region)
110    query['X-Amz-Date'] = iso8601Date
111    query['X-Amz-Expires'] = str(expires)
112    if session_token:
113        query['X-Amz-Security-Token'] = session_token
114
115    signed_headers = get_signed_headers(headers_to_sign)
116    query['X-Amz-SignedHeaders'] = ';'.join(signed_headers)
117
118    if response_headers is not None:
119        query.update(response_headers)
120
121    # URL components.
122    url_components = [parsed_url.geturl()]
123    if query is not None:
124        ordered_query = collections.OrderedDict(sorted(query.items()))
125        query_components = []
126        for component_key in ordered_query:
127            single_component = [component_key]
128            if ordered_query[component_key] is not None:
129                single_component.append('=')
130                single_component.append(
131                    queryencode(ordered_query[component_key])
132                )
133            else:
134                single_component.append('=')
135            query_components.append(''.join(single_component))
136
137        query_string = '&'.join(query_components)
138        if query_string:
139            url_components.append('?')
140            url_components.append(query_string)
141    new_url = ''.join(url_components)
142    # new url constructor block ends.
143    new_parsed_url = urlsplit(new_url)
144
145    canonical_request = generate_canonical_request(method,
146                                                   new_parsed_url,
147                                                   headers_to_sign,
148                                                   signed_headers,
149                                                   content_hash_hex)
150    string_to_sign = generate_string_to_sign(request_date, region,
151                                             canonical_request)
152    signing_key = generate_signing_key(request_date, region, secret_key)
153    signature = hmac.new(signing_key, string_to_sign.encode('utf-8'),
154                         hashlib.sha256).hexdigest()
155    new_parsed_url = urlsplit(new_url + "&X-Amz-Signature="+signature)
156    return new_parsed_url.geturl()
157
158
159
160def get_signed_headers(headers):
161    """
162    Get signed headers.
163
164    :param headers: input dictionary to be sorted.
165    """
166    signed_headers = []
167    for header in headers:
168        signed_headers.append(header.lower().strip())
169    return sorted(signed_headers)
170
171
172def sign_v4(method, url, region, headers=None,
173            access_key=None,
174            secret_key=None,
175            session_token=None,
176            content_sha256=None):
177    """
178    Signature version 4.
179
180    :param method: HTTP method used for signature.
181    :param url: Final url which needs to be signed.
182    :param region: Region should be set to bucket region.
183    :param headers: Optional headers for the method.
184    :param access_key: Optional access key, if not
185       specified no signature is needed.
186    :param secret_key: Optional secret key, if not
187       specified no signature is needed.
188    :param session_token: Optional session token, set
189       only for temporary credentials.
190    :param content_sha256: Optional body sha256.
191    """
192
193    # If no access key or secret key is provided return headers.
194    if not access_key or not secret_key:
195        return headers
196
197    if headers is None:
198        headers = FoldCaseDict()
199
200    if region is None:
201        region = 'us-east-1'
202
203    parsed_url = urlsplit(url)
204    secure = parsed_url.scheme == 'https'
205    if secure:
206        content_sha256 = _UNSIGNED_PAYLOAD
207    if content_sha256 is None:
208        # with no payload, calculate sha256 for 0 length data.
209        content_sha256 = get_sha256_hexdigest('')
210
211    host = parsed_url.netloc
212    headers['Host'] = host
213
214    date = datetime.utcnow()
215    headers['X-Amz-Date'] = date.strftime("%Y%m%dT%H%M%SZ")
216    headers['X-Amz-Content-Sha256'] = content_sha256
217    if session_token:
218        headers['X-Amz-Security-Token'] = session_token
219
220    headers_to_sign = headers
221
222    signed_headers = get_signed_headers(headers_to_sign)
223    canonical_req = generate_canonical_request(method,
224                                               parsed_url,
225                                               headers_to_sign,
226                                               signed_headers,
227                                               content_sha256)
228
229    string_to_sign = generate_string_to_sign(date, region,
230                                             canonical_req)
231    signing_key = generate_signing_key(date, region, secret_key)
232    signature = hmac.new(signing_key, string_to_sign.encode('utf-8'),
233                         hashlib.sha256).hexdigest()
234
235    authorization_header = generate_authorization_header(access_key,
236                                                         date,
237                                                         region,
238                                                         signed_headers,
239                                                         signature)
240
241    headers['Authorization'] = authorization_header
242    return headers
243
244
245def generate_canonical_request(method, parsed_url, headers, signed_headers, content_sha256):
246    """
247    Generate canonical request.
248
249    :param method: HTTP method.
250    :param parsed_url: Parsed url is input from :func:`urlsplit`
251    :param headers: HTTP header dictionary.
252    :param content_sha256: Content sha256 hexdigest string.
253    """
254    lines = [method, parsed_url.path, parsed_url.query]
255
256    # Headers added to canonical request.
257    header_lines = []
258    for header in signed_headers:
259        value = headers[header.title()]
260        value = str(value).strip()
261        header_lines.append(header + ':' + str(value))
262
263    lines = lines + header_lines
264    lines.append('')
265
266    lines.append(';'.join(signed_headers))
267    lines.append(content_sha256)
268    return '\n'.join(lines)
269
270
271def generate_string_to_sign(date, region, canonical_request):
272    """
273    Generate string to sign.
274
275    :param date: Date is input from :meth:`datetime.datetime`
276    :param region: Region should be set to bucket region.
277    :param canonical_request: Canonical request generated previously.
278    """
279    formatted_date_time = date.strftime("%Y%m%dT%H%M%SZ")
280
281    canonical_request_hasher = hashlib.sha256()
282    canonical_request_hasher.update(canonical_request.encode('utf-8'))
283    canonical_request_sha256 = canonical_request_hasher.hexdigest()
284    scope = generate_scope_string(date, region)
285
286    return '\n'.join([_SIGN_V4_ALGORITHM,
287                      formatted_date_time,
288                      scope,
289                      canonical_request_sha256])
290
291
292def generate_signing_key(date, region, secret_key):
293    """
294    Generate signing key.
295
296    :param date: Date is input from :meth:`datetime.datetime`
297    :param region: Region should be set to bucket region.
298    :param secret_key: Secret access key.
299    """
300    formatted_date = date.strftime("%Y%m%d")
301
302    key1_string = 'AWS4' + secret_key
303    key1 = key1_string.encode('utf-8')
304    key2 = hmac.new(key1, formatted_date.encode('utf-8'),
305                    hashlib.sha256).digest()
306    key3 = hmac.new(key2, region.encode('utf-8'), hashlib.sha256).digest()
307    key4 = hmac.new(key3, 's3'.encode('utf-8'), hashlib.sha256).digest()
308
309    return hmac.new(key4, 'aws4_request'.encode('utf-8'),
310                    hashlib.sha256).digest()
311
312
313def generate_scope_string(date, region):
314    """
315    Generate scope string.
316
317    :param date: Date is input from :meth:`datetime.datetime`
318    :param region: Region should be set to bucket region.
319    """
320    formatted_date = date.strftime("%Y%m%d")
321    scope = '/'.join([formatted_date,
322                      region,
323                      's3',
324                      'aws4_request'])
325    return scope
326
327
328def generate_credential_string(access_key, date, region):
329    """
330    Generate credential string.
331
332    :param access_key: Server access key.
333    :param date: Date is input from :meth:`datetime.datetime`
334    :param region: Region should be set to bucket region.
335    """
336    return access_key + '/' + generate_scope_string(date, region)
337
338
339def generate_authorization_header(access_key, date, region,
340                                  signed_headers, signature):
341    """
342    Generate authorization header.
343
344    :param access_key: Server access key.
345    :param date: Date is input from :meth:`datetime.datetime`
346    :param region: Region should be set to bucket region.
347    :param signed_headers: Signed headers.
348    :param signature: Calculated signature.
349    """
350    signed_headers_string = ';'.join(signed_headers)
351    credential = generate_credential_string(access_key, date, region)
352    auth_header = [_SIGN_V4_ALGORITHM, 'Credential=' + credential + ',',
353                   'SignedHeaders=' + signed_headers_string + ',',
354                   'Signature=' + signature]
355    return ' '.join(auth_header)
356