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