1# Copyright 2010 Google Inc. 2# Copyright (c) 2011 Mitch Garnaat http://garnaat.org/ 3# Copyright (c) 2011, Eucalyptus Systems, Inc. 4# 5# Permission is hereby granted, free of charge, to any person obtaining a 6# copy of this software and associated documentation files (the 7# "Software"), to deal in the Software without restriction, including 8# without limitation the rights to use, copy, modify, merge, publish, dis- 9# tribute, sublicense, and/or sell copies of the Software, and to permit 10# persons to whom the Software is furnished to do so, subject to the fol- 11# lowing conditions: 12# 13# The above copyright notice and this permission notice shall be included 14# in all copies or substantial portions of the Software. 15# 16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- 18# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22# IN THE SOFTWARE. 23 24 25""" 26Handles authentication required to AWS and GS 27""" 28 29import base64 30import boto 31import boto.auth_handler 32import boto.exception 33import boto.plugin 34import boto.utils 35import copy 36import datetime 37from email.utils import formatdate 38import hmac 39import os 40import posixpath 41 42from boto.compat import urllib, encodebytes 43from boto.auth_handler import AuthHandler 44from boto.exception import BotoClientError 45 46try: 47 from hashlib import sha1 as sha 48 from hashlib import sha256 as sha256 49except ImportError: 50 import sha 51 sha256 = None 52 53 54# Region detection strings to determine if SigV4 should be used 55# by default. 56SIGV4_DETECT = [ 57 '.cn-', 58 # In eu-central we support both host styles for S3 59 '.eu-central', 60 '-eu-central', 61] 62 63 64class HmacKeys(object): 65 """Key based Auth handler helper.""" 66 67 def __init__(self, host, config, provider): 68 if provider.access_key is None or provider.secret_key is None: 69 raise boto.auth_handler.NotReadyToAuthenticate() 70 self.host = host 71 self.update_provider(provider) 72 73 def update_provider(self, provider): 74 self._provider = provider 75 self._hmac = hmac.new(self._provider.secret_key.encode('utf-8'), 76 digestmod=sha) 77 if sha256: 78 self._hmac_256 = hmac.new(self._provider.secret_key.encode('utf-8'), 79 digestmod=sha256) 80 else: 81 self._hmac_256 = None 82 83 def algorithm(self): 84 if self._hmac_256: 85 return 'HmacSHA256' 86 else: 87 return 'HmacSHA1' 88 89 def _get_hmac(self): 90 if self._hmac_256: 91 digestmod = sha256 92 else: 93 digestmod = sha 94 return hmac.new(self._provider.secret_key.encode('utf-8'), 95 digestmod=digestmod) 96 97 def sign_string(self, string_to_sign): 98 new_hmac = self._get_hmac() 99 new_hmac.update(string_to_sign.encode('utf-8')) 100 return encodebytes(new_hmac.digest()).decode('utf-8').strip() 101 102 def __getstate__(self): 103 pickled_dict = copy.copy(self.__dict__) 104 del pickled_dict['_hmac'] 105 del pickled_dict['_hmac_256'] 106 return pickled_dict 107 108 def __setstate__(self, dct): 109 self.__dict__ = dct 110 self.update_provider(self._provider) 111 112 113class AnonAuthHandler(AuthHandler, HmacKeys): 114 """ 115 Implements Anonymous requests. 116 """ 117 118 capability = ['anon'] 119 120 def __init__(self, host, config, provider): 121 super(AnonAuthHandler, self).__init__(host, config, provider) 122 123 def add_auth(self, http_request, **kwargs): 124 pass 125 126 127class HmacAuthV1Handler(AuthHandler, HmacKeys): 128 """ Implements the HMAC request signing used by S3 and GS.""" 129 130 capability = ['hmac-v1', 's3'] 131 132 def __init__(self, host, config, provider): 133 AuthHandler.__init__(self, host, config, provider) 134 HmacKeys.__init__(self, host, config, provider) 135 self._hmac_256 = None 136 137 def update_provider(self, provider): 138 super(HmacAuthV1Handler, self).update_provider(provider) 139 self._hmac_256 = None 140 141 def add_auth(self, http_request, **kwargs): 142 headers = http_request.headers 143 method = http_request.method 144 auth_path = http_request.auth_path 145 if 'Date' not in headers: 146 headers['Date'] = formatdate(usegmt=True) 147 148 if self._provider.security_token: 149 key = self._provider.security_token_header 150 headers[key] = self._provider.security_token 151 string_to_sign = boto.utils.canonical_string(method, auth_path, 152 headers, None, 153 self._provider) 154 boto.log.debug('StringToSign:\n%s' % string_to_sign) 155 b64_hmac = self.sign_string(string_to_sign) 156 auth_hdr = self._provider.auth_header 157 auth = ("%s %s:%s" % (auth_hdr, self._provider.access_key, b64_hmac)) 158 boto.log.debug('Signature:\n%s' % auth) 159 headers['Authorization'] = auth 160 161 162class HmacAuthV2Handler(AuthHandler, HmacKeys): 163 """ 164 Implements the simplified HMAC authorization used by CloudFront. 165 """ 166 capability = ['hmac-v2', 'cloudfront'] 167 168 def __init__(self, host, config, provider): 169 AuthHandler.__init__(self, host, config, provider) 170 HmacKeys.__init__(self, host, config, provider) 171 self._hmac_256 = None 172 173 def update_provider(self, provider): 174 super(HmacAuthV2Handler, self).update_provider(provider) 175 self._hmac_256 = None 176 177 def add_auth(self, http_request, **kwargs): 178 headers = http_request.headers 179 if 'Date' not in headers: 180 headers['Date'] = formatdate(usegmt=True) 181 if self._provider.security_token: 182 key = self._provider.security_token_header 183 headers[key] = self._provider.security_token 184 185 b64_hmac = self.sign_string(headers['Date']) 186 auth_hdr = self._provider.auth_header 187 headers['Authorization'] = ("%s %s:%s" % 188 (auth_hdr, 189 self._provider.access_key, b64_hmac)) 190 191 192class HmacAuthV3Handler(AuthHandler, HmacKeys): 193 """Implements the new Version 3 HMAC authorization used by Route53.""" 194 195 capability = ['hmac-v3', 'route53', 'ses'] 196 197 def __init__(self, host, config, provider): 198 AuthHandler.__init__(self, host, config, provider) 199 HmacKeys.__init__(self, host, config, provider) 200 201 def add_auth(self, http_request, **kwargs): 202 headers = http_request.headers 203 if 'Date' not in headers: 204 headers['Date'] = formatdate(usegmt=True) 205 206 if self._provider.security_token: 207 key = self._provider.security_token_header 208 headers[key] = self._provider.security_token 209 210 b64_hmac = self.sign_string(headers['Date']) 211 s = "AWS3-HTTPS AWSAccessKeyId=%s," % self._provider.access_key 212 s += "Algorithm=%s,Signature=%s" % (self.algorithm(), b64_hmac) 213 headers['X-Amzn-Authorization'] = s 214 215 216class HmacAuthV3HTTPHandler(AuthHandler, HmacKeys): 217 """ 218 Implements the new Version 3 HMAC authorization used by DynamoDB. 219 """ 220 221 capability = ['hmac-v3-http'] 222 223 def __init__(self, host, config, provider): 224 AuthHandler.__init__(self, host, config, provider) 225 HmacKeys.__init__(self, host, config, provider) 226 227 def headers_to_sign(self, http_request): 228 """ 229 Select the headers from the request that need to be included 230 in the StringToSign. 231 """ 232 headers_to_sign = {'Host': self.host} 233 for name, value in http_request.headers.items(): 234 lname = name.lower() 235 if lname.startswith('x-amz'): 236 headers_to_sign[name] = value 237 return headers_to_sign 238 239 def canonical_headers(self, headers_to_sign): 240 """ 241 Return the headers that need to be included in the StringToSign 242 in their canonical form by converting all header keys to lower 243 case, sorting them in alphabetical order and then joining 244 them into a string, separated by newlines. 245 """ 246 l = sorted(['%s:%s' % (n.lower().strip(), 247 headers_to_sign[n].strip()) for n in headers_to_sign]) 248 return '\n'.join(l) 249 250 def string_to_sign(self, http_request): 251 """ 252 Return the canonical StringToSign as well as a dict 253 containing the original version of all headers that 254 were included in the StringToSign. 255 """ 256 headers_to_sign = self.headers_to_sign(http_request) 257 canonical_headers = self.canonical_headers(headers_to_sign) 258 string_to_sign = '\n'.join([http_request.method, 259 http_request.auth_path, 260 '', 261 canonical_headers, 262 '', 263 http_request.body]) 264 return string_to_sign, headers_to_sign 265 266 def add_auth(self, req, **kwargs): 267 """ 268 Add AWS3 authentication to a request. 269 270 :type req: :class`boto.connection.HTTPRequest` 271 :param req: The HTTPRequest object. 272 """ 273 # This could be a retry. Make sure the previous 274 # authorization header is removed first. 275 if 'X-Amzn-Authorization' in req.headers: 276 del req.headers['X-Amzn-Authorization'] 277 req.headers['X-Amz-Date'] = formatdate(usegmt=True) 278 if self._provider.security_token: 279 req.headers['X-Amz-Security-Token'] = self._provider.security_token 280 string_to_sign, headers_to_sign = self.string_to_sign(req) 281 boto.log.debug('StringToSign:\n%s' % string_to_sign) 282 hash_value = sha256(string_to_sign.encode('utf-8')).digest() 283 b64_hmac = self.sign_string(hash_value) 284 s = "AWS3 AWSAccessKeyId=%s," % self._provider.access_key 285 s += "Algorithm=%s," % self.algorithm() 286 s += "SignedHeaders=%s," % ';'.join(headers_to_sign) 287 s += "Signature=%s" % b64_hmac 288 req.headers['X-Amzn-Authorization'] = s 289 290 291class HmacAuthV4Handler(AuthHandler, HmacKeys): 292 """ 293 Implements the new Version 4 HMAC authorization. 294 """ 295 296 capability = ['hmac-v4'] 297 298 def __init__(self, host, config, provider, 299 service_name=None, region_name=None): 300 AuthHandler.__init__(self, host, config, provider) 301 HmacKeys.__init__(self, host, config, provider) 302 # You can set the service_name and region_name to override the 303 # values which would otherwise come from the endpoint, e.g. 304 # <service>.<region>.amazonaws.com. 305 self.service_name = service_name 306 self.region_name = region_name 307 308 def _sign(self, key, msg, hex=False): 309 if not isinstance(key, bytes): 310 key = key.encode('utf-8') 311 312 if hex: 313 sig = hmac.new(key, msg.encode('utf-8'), sha256).hexdigest() 314 else: 315 sig = hmac.new(key, msg.encode('utf-8'), sha256).digest() 316 return sig 317 318 def headers_to_sign(self, http_request): 319 """ 320 Select the headers from the request that need to be included 321 in the StringToSign. 322 """ 323 host_header_value = self.host_header(self.host, http_request) 324 if http_request.headers.get('Host'): 325 host_header_value = http_request.headers['Host'] 326 headers_to_sign = {'Host': host_header_value} 327 for name, value in http_request.headers.items(): 328 lname = name.lower() 329 if lname.startswith('x-amz'): 330 if isinstance(value, bytes): 331 value = value.decode('utf-8') 332 headers_to_sign[name] = value 333 return headers_to_sign 334 335 def host_header(self, host, http_request): 336 port = http_request.port 337 secure = http_request.protocol == 'https' 338 if ((port == 80 and not secure) or (port == 443 and secure)): 339 return host 340 return '%s:%s' % (host, port) 341 342 def query_string(self, http_request): 343 parameter_names = sorted(http_request.params.keys()) 344 pairs = [] 345 for pname in parameter_names: 346 pval = boto.utils.get_utf8_value(http_request.params[pname]) 347 pairs.append(urllib.parse.quote(pname, safe='') + '=' + 348 urllib.parse.quote(pval, safe='-_~')) 349 return '&'.join(pairs) 350 351 def canonical_query_string(self, http_request): 352 # POST requests pass parameters in through the 353 # http_request.body field. 354 if http_request.method == 'POST': 355 return "" 356 l = [] 357 for param in sorted(http_request.params): 358 value = boto.utils.get_utf8_value(http_request.params[param]) 359 l.append('%s=%s' % (urllib.parse.quote(param, safe='-_.~'), 360 urllib.parse.quote(value, safe='-_.~'))) 361 return '&'.join(l) 362 363 def canonical_headers(self, headers_to_sign): 364 """ 365 Return the headers that need to be included in the StringToSign 366 in their canonical form by converting all header keys to lower 367 case, sorting them in alphabetical order and then joining 368 them into a string, separated by newlines. 369 """ 370 canonical = [] 371 372 for header in headers_to_sign: 373 c_name = header.lower().strip() 374 raw_value = str(headers_to_sign[header]) 375 if '"' in raw_value: 376 c_value = raw_value.strip() 377 else: 378 c_value = ' '.join(raw_value.strip().split()) 379 canonical.append('%s:%s' % (c_name, c_value)) 380 return '\n'.join(sorted(canonical)) 381 382 def signed_headers(self, headers_to_sign): 383 l = ['%s' % n.lower().strip() for n in headers_to_sign] 384 l = sorted(l) 385 return ';'.join(l) 386 387 def canonical_uri(self, http_request): 388 path = http_request.auth_path 389 # Normalize the path 390 # in windows normpath('/') will be '\\' so we chane it back to '/' 391 normalized = posixpath.normpath(path).replace('\\', '/') 392 # Then urlencode whatever's left. 393 encoded = urllib.parse.quote(normalized) 394 if len(path) > 1 and path.endswith('/'): 395 encoded += '/' 396 return encoded 397 398 def payload(self, http_request): 399 body = http_request.body 400 # If the body is a file like object, we can use 401 # boto.utils.compute_hash, which will avoid reading 402 # the entire body into memory. 403 if hasattr(body, 'seek') and hasattr(body, 'read'): 404 return boto.utils.compute_hash(body, hash_algorithm=sha256)[0] 405 elif not isinstance(body, bytes): 406 body = body.encode('utf-8') 407 return sha256(body).hexdigest() 408 409 def canonical_request(self, http_request): 410 cr = [http_request.method.upper()] 411 cr.append(self.canonical_uri(http_request)) 412 cr.append(self.canonical_query_string(http_request)) 413 headers_to_sign = self.headers_to_sign(http_request) 414 cr.append(self.canonical_headers(headers_to_sign) + '\n') 415 cr.append(self.signed_headers(headers_to_sign)) 416 cr.append(self.payload(http_request)) 417 return '\n'.join(cr) 418 419 def scope(self, http_request): 420 scope = [self._provider.access_key] 421 scope.append(http_request.timestamp) 422 scope.append(http_request.region_name) 423 scope.append(http_request.service_name) 424 scope.append('aws4_request') 425 return '/'.join(scope) 426 427 def split_host_parts(self, host): 428 return host.split('.') 429 430 def determine_region_name(self, host): 431 parts = self.split_host_parts(host) 432 if self.region_name is not None: 433 region_name = self.region_name 434 elif len(parts) > 1: 435 if parts[1] == 'us-gov': 436 region_name = 'us-gov-west-1' 437 else: 438 if len(parts) == 3: 439 region_name = 'us-east-1' 440 else: 441 region_name = parts[1] 442 else: 443 region_name = parts[0] 444 445 return region_name 446 447 def determine_service_name(self, host): 448 parts = self.split_host_parts(host) 449 if self.service_name is not None: 450 service_name = self.service_name 451 else: 452 service_name = parts[0] 453 return service_name 454 455 def credential_scope(self, http_request): 456 scope = [] 457 http_request.timestamp = http_request.headers['X-Amz-Date'][0:8] 458 scope.append(http_request.timestamp) 459 # The service_name and region_name either come from: 460 # * The service_name/region_name attrs or (if these values are None) 461 # * parsed from the endpoint <service>.<region>.amazonaws.com. 462 region_name = self.determine_region_name(http_request.host) 463 service_name = self.determine_service_name(http_request.host) 464 http_request.service_name = service_name 465 http_request.region_name = region_name 466 467 scope.append(http_request.region_name) 468 scope.append(http_request.service_name) 469 scope.append('aws4_request') 470 return '/'.join(scope) 471 472 def string_to_sign(self, http_request, canonical_request): 473 """ 474 Return the canonical StringToSign as well as a dict 475 containing the original version of all headers that 476 were included in the StringToSign. 477 """ 478 sts = ['AWS4-HMAC-SHA256'] 479 sts.append(http_request.headers['X-Amz-Date']) 480 sts.append(self.credential_scope(http_request)) 481 sts.append(sha256(canonical_request.encode('utf-8')).hexdigest()) 482 return '\n'.join(sts) 483 484 def signature(self, http_request, string_to_sign): 485 key = self._provider.secret_key 486 k_date = self._sign(('AWS4' + key).encode('utf-8'), 487 http_request.timestamp) 488 k_region = self._sign(k_date, http_request.region_name) 489 k_service = self._sign(k_region, http_request.service_name) 490 k_signing = self._sign(k_service, 'aws4_request') 491 return self._sign(k_signing, string_to_sign, hex=True) 492 493 def add_auth(self, req, **kwargs): 494 """ 495 Add AWS4 authentication to a request. 496 497 :type req: :class`boto.connection.HTTPRequest` 498 :param req: The HTTPRequest object. 499 """ 500 # This could be a retry. Make sure the previous 501 # authorization header is removed first. 502 if 'X-Amzn-Authorization' in req.headers: 503 del req.headers['X-Amzn-Authorization'] 504 now = datetime.datetime.utcnow() 505 req.headers['X-Amz-Date'] = now.strftime('%Y%m%dT%H%M%SZ') 506 if self._provider.security_token: 507 req.headers['X-Amz-Security-Token'] = self._provider.security_token 508 qs = self.query_string(req) 509 510 qs_to_post = qs 511 512 # We do not want to include any params that were mangled into 513 # the params if performing s3-sigv4 since it does not 514 # belong in the body of a post for some requests. Mangled 515 # refers to items in the query string URL being added to the 516 # http response params. However, these params get added to 517 # the body of the request, but the query string URL does not 518 # belong in the body of the request. ``unmangled_resp`` is the 519 # response that happened prior to the mangling. This ``unmangled_req`` 520 # kwarg will only appear for s3-sigv4. 521 if 'unmangled_req' in kwargs: 522 qs_to_post = self.query_string(kwargs['unmangled_req']) 523 524 if qs_to_post and req.method == 'POST': 525 # Stash request parameters into post body 526 # before we generate the signature. 527 req.body = qs_to_post 528 req.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8' 529 req.headers['Content-Length'] = str(len(req.body)) 530 else: 531 # Safe to modify req.path here since 532 # the signature will use req.auth_path. 533 req.path = req.path.split('?')[0] 534 535 if qs: 536 # Don't insert the '?' unless there's actually a query string 537 req.path = req.path + '?' + qs 538 canonical_request = self.canonical_request(req) 539 boto.log.debug('CanonicalRequest:\n%s' % canonical_request) 540 string_to_sign = self.string_to_sign(req, canonical_request) 541 boto.log.debug('StringToSign:\n%s' % string_to_sign) 542 signature = self.signature(req, string_to_sign) 543 boto.log.debug('Signature:\n%s' % signature) 544 headers_to_sign = self.headers_to_sign(req) 545 l = ['AWS4-HMAC-SHA256 Credential=%s' % self.scope(req)] 546 l.append('SignedHeaders=%s' % self.signed_headers(headers_to_sign)) 547 l.append('Signature=%s' % signature) 548 req.headers['Authorization'] = ','.join(l) 549 550 551class S3HmacAuthV4Handler(HmacAuthV4Handler, AuthHandler): 552 """ 553 Implements a variant of Version 4 HMAC authorization specific to S3. 554 """ 555 capability = ['hmac-v4-s3'] 556 557 def __init__(self, *args, **kwargs): 558 super(S3HmacAuthV4Handler, self).__init__(*args, **kwargs) 559 560 if self.region_name: 561 self.region_name = self.clean_region_name(self.region_name) 562 563 def clean_region_name(self, region_name): 564 if region_name.startswith('s3-'): 565 return region_name[3:] 566 567 return region_name 568 569 def canonical_uri(self, http_request): 570 # S3 does **NOT** do path normalization that SigV4 typically does. 571 # Urlencode the path, **NOT** ``auth_path`` (because vhosting). 572 path = urllib.parse.urlparse(http_request.path) 573 # Because some quoting may have already been applied, let's back it out. 574 unquoted = urllib.parse.unquote(path.path) 575 # Requote, this time addressing all characters. 576 encoded = urllib.parse.quote(unquoted) 577 return encoded 578 579 def canonical_query_string(self, http_request): 580 # Note that we just do not return an empty string for 581 # POST request. Query strings in url are included in canonical 582 # query string. 583 l = [] 584 for param in sorted(http_request.params): 585 value = boto.utils.get_utf8_value(http_request.params[param]) 586 l.append('%s=%s' % (urllib.parse.quote(param, safe='-_.~'), 587 urllib.parse.quote(value, safe='-_.~'))) 588 return '&'.join(l) 589 590 def host_header(self, host, http_request): 591 port = http_request.port 592 secure = http_request.protocol == 'https' 593 if ((port == 80 and not secure) or (port == 443 and secure)): 594 return http_request.host 595 return '%s:%s' % (http_request.host, port) 596 597 def headers_to_sign(self, http_request): 598 """ 599 Select the headers from the request that need to be included 600 in the StringToSign. 601 """ 602 host_header_value = self.host_header(self.host, http_request) 603 headers_to_sign = {'Host': host_header_value} 604 for name, value in http_request.headers.items(): 605 lname = name.lower() 606 # Hooray for the only difference! The main SigV4 signer only does 607 # ``Host`` + ``x-amz-*``. But S3 wants pretty much everything 608 # signed, except for authorization itself. 609 if lname not in ['authorization']: 610 headers_to_sign[name] = value 611 return headers_to_sign 612 613 def determine_region_name(self, host): 614 # S3's different format(s) of representing region/service from the 615 # rest of AWS makes this hurt too. 616 # 617 # Possible domain formats: 618 # - s3.amazonaws.com (Classic) 619 # - s3-us-west-2.amazonaws.com (Specific region) 620 # - bukkit.s3.amazonaws.com (Vhosted Classic) 621 # - bukkit.s3-ap-northeast-1.amazonaws.com (Vhosted specific region) 622 # - s3.cn-north-1.amazonaws.com.cn - (Beijing region) 623 # - bukkit.s3.cn-north-1.amazonaws.com.cn - (Vhosted Beijing region) 624 parts = self.split_host_parts(host) 625 626 if self.region_name is not None: 627 region_name = self.region_name 628 else: 629 # Classic URLs - s3-us-west-2.amazonaws.com 630 if len(parts) == 3: 631 region_name = self.clean_region_name(parts[0]) 632 633 # Special-case for Classic. 634 if region_name == 's3': 635 region_name = 'us-east-1' 636 else: 637 # Iterate over the parts in reverse order. 638 for offset, part in enumerate(reversed(parts)): 639 part = part.lower() 640 641 # Look for the first thing starting with 's3'. 642 # Until there's a ``.s3`` TLD, we should be OK. :P 643 if part == 's3': 644 # If it's by itself, the region is the previous part. 645 region_name = parts[-offset] 646 647 # Unless it's Vhosted classic 648 if region_name == 'amazonaws': 649 region_name = 'us-east-1' 650 651 break 652 elif part.startswith('s3-'): 653 region_name = self.clean_region_name(part) 654 break 655 656 return region_name 657 658 def determine_service_name(self, host): 659 # Should this signing mechanism ever be used for anything else, this 660 # will fail. Consider utilizing the logic from the parent class should 661 # you find yourself here. 662 return 's3' 663 664 def mangle_path_and_params(self, req): 665 """ 666 Returns a copy of the request object with fixed ``auth_path/params`` 667 attributes from the original. 668 """ 669 modified_req = copy.copy(req) 670 671 # Unlike the most other services, in S3, ``req.params`` isn't the only 672 # source of query string parameters. 673 # Because of the ``query_args``, we may already have a query string 674 # **ON** the ``path/auth_path``. 675 # Rip them apart, so the ``auth_path/params`` can be signed 676 # appropriately. 677 parsed_path = urllib.parse.urlparse(modified_req.auth_path) 678 modified_req.auth_path = parsed_path.path 679 680 if modified_req.params is None: 681 modified_req.params = {} 682 else: 683 # To keep the original request object untouched. We must make 684 # a copy of the params dictionary. Because the copy of the 685 # original request directly refers to the params dictionary 686 # of the original request. 687 copy_params = req.params.copy() 688 modified_req.params = copy_params 689 690 raw_qs = parsed_path.query 691 existing_qs = urllib.parse.parse_qs( 692 raw_qs, 693 keep_blank_values=True 694 ) 695 696 # ``parse_qs`` will return lists. Don't do that unless there's a real, 697 # live list provided. 698 for key, value in existing_qs.items(): 699 if isinstance(value, (list, tuple)): 700 if len(value) == 1: 701 existing_qs[key] = value[0] 702 703 modified_req.params.update(existing_qs) 704 return modified_req 705 706 def payload(self, http_request): 707 if http_request.headers.get('x-amz-content-sha256'): 708 return http_request.headers['x-amz-content-sha256'] 709 710 return super(S3HmacAuthV4Handler, self).payload(http_request) 711 712 def add_auth(self, req, **kwargs): 713 if 'x-amz-content-sha256' not in req.headers: 714 if '_sha256' in req.headers: 715 req.headers['x-amz-content-sha256'] = req.headers.pop('_sha256') 716 else: 717 req.headers['x-amz-content-sha256'] = self.payload(req) 718 updated_req = self.mangle_path_and_params(req) 719 return super(S3HmacAuthV4Handler, self).add_auth(updated_req, 720 unmangled_req=req, 721 **kwargs) 722 723 def presign(self, req, expires, iso_date=None): 724 """ 725 Presign a request using SigV4 query params. Takes in an HTTP request 726 and an expiration time in seconds and returns a URL. 727 728 http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html 729 """ 730 if iso_date is None: 731 iso_date = datetime.datetime.utcnow().strftime('%Y%m%dT%H%M%SZ') 732 733 region = self.determine_region_name(req.host) 734 service = self.determine_service_name(req.host) 735 736 params = { 737 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256', 738 'X-Amz-Credential': '%s/%s/%s/%s/aws4_request' % ( 739 self._provider.access_key, 740 iso_date[:8], 741 region, 742 service 743 ), 744 'X-Amz-Date': iso_date, 745 'X-Amz-Expires': expires, 746 'X-Amz-SignedHeaders': 'host' 747 } 748 749 if self._provider.security_token: 750 params['X-Amz-Security-Token'] = self._provider.security_token 751 752 headers_to_sign = self.headers_to_sign(req) 753 l = sorted(['%s' % n.lower().strip() for n in headers_to_sign]) 754 params['X-Amz-SignedHeaders'] = ';'.join(l) 755 756 req.params.update(params) 757 758 cr = self.canonical_request(req) 759 760 # We need to replace the payload SHA with a constant 761 cr = '\n'.join(cr.split('\n')[:-1]) + '\nUNSIGNED-PAYLOAD' 762 763 # Date header is expected for string_to_sign, but unused otherwise 764 req.headers['X-Amz-Date'] = iso_date 765 766 sts = self.string_to_sign(req, cr) 767 signature = self.signature(req, sts) 768 769 # Add signature to params now that we have it 770 req.params['X-Amz-Signature'] = signature 771 772 return 'https://%s%s?%s' % (req.host, req.path, 773 urllib.parse.urlencode(req.params)) 774 775 776class STSAnonHandler(AuthHandler): 777 """ 778 Provides pure query construction (no actual signing). 779 780 Used for making anonymous STS request for operations like 781 ``assume_role_with_web_identity``. 782 """ 783 784 capability = ['sts-anon'] 785 786 def _escape_value(self, value): 787 # This is changed from a previous version because this string is 788 # being passed to the query string and query strings must 789 # be url encoded. In particular STS requires the saml_response to 790 # be urlencoded when calling assume_role_with_saml. 791 return urllib.parse.quote(value) 792 793 def _build_query_string(self, params): 794 keys = list(params.keys()) 795 keys.sort(key=lambda x: x.lower()) 796 pairs = [] 797 for key in keys: 798 val = boto.utils.get_utf8_value(params[key]) 799 pairs.append(key + '=' + self._escape_value(val.decode('utf-8'))) 800 return '&'.join(pairs) 801 802 def add_auth(self, http_request, **kwargs): 803 headers = http_request.headers 804 qs = self._build_query_string( 805 http_request.params 806 ) 807 boto.log.debug('query_string in body: %s' % qs) 808 headers['Content-Type'] = 'application/x-www-form-urlencoded' 809 # This will be a POST so the query string should go into the body 810 # as opposed to being in the uri 811 http_request.body = qs 812 813 814class QuerySignatureHelper(HmacKeys): 815 """ 816 Helper for Query signature based Auth handler. 817 818 Concrete sub class need to implement _calc_sigature method. 819 """ 820 821 def add_auth(self, http_request, **kwargs): 822 headers = http_request.headers 823 params = http_request.params 824 params['AWSAccessKeyId'] = self._provider.access_key 825 params['SignatureVersion'] = self.SignatureVersion 826 params['Timestamp'] = boto.utils.get_ts() 827 qs, signature = self._calc_signature( 828 http_request.params, http_request.method, 829 http_request.auth_path, http_request.host) 830 boto.log.debug('query_string: %s Signature: %s' % (qs, signature)) 831 if http_request.method == 'POST': 832 headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8' 833 http_request.body = qs + '&Signature=' + urllib.parse.quote_plus(signature) 834 http_request.headers['Content-Length'] = str(len(http_request.body)) 835 else: 836 http_request.body = '' 837 # if this is a retried request, the qs from the previous try will 838 # already be there, we need to get rid of that and rebuild it 839 http_request.path = http_request.path.split('?')[0] 840 http_request.path = (http_request.path + '?' + qs + 841 '&Signature=' + urllib.parse.quote_plus(signature)) 842 843 844class QuerySignatureV0AuthHandler(QuerySignatureHelper, AuthHandler): 845 """Provides Signature V0 Signing""" 846 847 SignatureVersion = 0 848 capability = ['sign-v0'] 849 850 def _calc_signature(self, params, *args): 851 boto.log.debug('using _calc_signature_0') 852 hmac = self._get_hmac() 853 s = params['Action'] + params['Timestamp'] 854 hmac.update(s.encode('utf-8')) 855 keys = params.keys() 856 keys.sort(cmp=lambda x, y: cmp(x.lower(), y.lower())) 857 pairs = [] 858 for key in keys: 859 val = boto.utils.get_utf8_value(params[key]) 860 pairs.append(key + '=' + urllib.parse.quote(val)) 861 qs = '&'.join(pairs) 862 return (qs, base64.b64encode(hmac.digest())) 863 864 865class QuerySignatureV1AuthHandler(QuerySignatureHelper, AuthHandler): 866 """ 867 Provides Query Signature V1 Authentication. 868 """ 869 870 SignatureVersion = 1 871 capability = ['sign-v1', 'mturk'] 872 873 def __init__(self, *args, **kw): 874 QuerySignatureHelper.__init__(self, *args, **kw) 875 AuthHandler.__init__(self, *args, **kw) 876 self._hmac_256 = None 877 878 def _calc_signature(self, params, *args): 879 boto.log.debug('using _calc_signature_1') 880 hmac = self._get_hmac() 881 keys = params.keys() 882 keys.sort(cmp=lambda x, y: cmp(x.lower(), y.lower())) 883 pairs = [] 884 for key in keys: 885 hmac.update(key.encode('utf-8')) 886 val = boto.utils.get_utf8_value(params[key]) 887 hmac.update(val) 888 pairs.append(key + '=' + urllib.parse.quote(val)) 889 qs = '&'.join(pairs) 890 return (qs, base64.b64encode(hmac.digest())) 891 892 893class QuerySignatureV2AuthHandler(QuerySignatureHelper, AuthHandler): 894 """Provides Query Signature V2 Authentication.""" 895 896 SignatureVersion = 2 897 capability = ['sign-v2', 'ec2', 'ec2', 'emr', 'fps', 'ecs', 898 'sdb', 'iam', 'rds', 'sns', 'sqs', 'cloudformation'] 899 900 def _calc_signature(self, params, verb, path, server_name): 901 boto.log.debug('using _calc_signature_2') 902 string_to_sign = '%s\n%s\n%s\n' % (verb, server_name.lower(), path) 903 hmac = self._get_hmac() 904 params['SignatureMethod'] = self.algorithm() 905 if self._provider.security_token: 906 params['SecurityToken'] = self._provider.security_token 907 keys = sorted(params.keys()) 908 pairs = [] 909 for key in keys: 910 val = boto.utils.get_utf8_value(params[key]) 911 pairs.append(urllib.parse.quote(key, safe='') + '=' + 912 urllib.parse.quote(val, safe='-_~')) 913 qs = '&'.join(pairs) 914 boto.log.debug('query string: %s' % qs) 915 string_to_sign += qs 916 boto.log.debug('string_to_sign: %s' % string_to_sign) 917 hmac.update(string_to_sign.encode('utf-8')) 918 b64 = base64.b64encode(hmac.digest()) 919 boto.log.debug('len(b64)=%d' % len(b64)) 920 boto.log.debug('base64 encoded digest: %s' % b64) 921 return (qs, b64) 922 923 924class POSTPathQSV2AuthHandler(QuerySignatureV2AuthHandler, AuthHandler): 925 """ 926 Query Signature V2 Authentication relocating signed query 927 into the path and allowing POST requests with Content-Types. 928 """ 929 930 capability = ['mws'] 931 932 def add_auth(self, req, **kwargs): 933 req.params['AWSAccessKeyId'] = self._provider.access_key 934 req.params['SignatureVersion'] = self.SignatureVersion 935 req.params['Timestamp'] = boto.utils.get_ts() 936 qs, signature = self._calc_signature(req.params, req.method, 937 req.auth_path, req.host) 938 boto.log.debug('query_string: %s Signature: %s' % (qs, signature)) 939 if req.method == 'POST': 940 req.headers['Content-Length'] = str(len(req.body)) 941 req.headers['Content-Type'] = req.headers.get('Content-Type', 942 'text/plain') 943 else: 944 req.body = '' 945 # if this is a retried req, the qs from the previous try will 946 # already be there, we need to get rid of that and rebuild it 947 req.path = req.path.split('?')[0] 948 req.path = (req.path + '?' + qs + 949 '&Signature=' + urllib.parse.quote_plus(signature)) 950 951 952def get_auth_handler(host, config, provider, requested_capability=None): 953 """Finds an AuthHandler that is ready to authenticate. 954 955 Lists through all the registered AuthHandlers to find one that is willing 956 to handle for the requested capabilities, config and provider. 957 958 :type host: string 959 :param host: The name of the host 960 961 :type config: 962 :param config: 963 964 :type provider: 965 :param provider: 966 967 Returns: 968 An implementation of AuthHandler. 969 970 Raises: 971 boto.exception.NoAuthHandlerFound 972 """ 973 ready_handlers = [] 974 auth_handlers = boto.plugin.get_plugin(AuthHandler, requested_capability) 975 for handler in auth_handlers: 976 try: 977 ready_handlers.append(handler(host, config, provider)) 978 except boto.auth_handler.NotReadyToAuthenticate: 979 pass 980 981 if not ready_handlers: 982 checked_handlers = auth_handlers 983 names = [handler.__name__ for handler in checked_handlers] 984 raise boto.exception.NoAuthHandlerFound( 985 'No handler was ready to authenticate. %d handlers were checked.' 986 ' %s ' 987 'Check your credentials' % (len(names), str(names))) 988 989 # We select the last ready auth handler that was loaded, to allow users to 990 # customize how auth works in environments where there are shared boto 991 # config files (e.g., /etc/boto.cfg and ~/.boto): The more general, 992 # system-wide shared configs should be loaded first, and the user's 993 # customizations loaded last. That way, for example, the system-wide 994 # config might include a plugin_directory that includes a service account 995 # auth plugin shared by all users of a Google Compute Engine instance 996 # (allowing sharing of non-user data between various services), and the 997 # user could override this with a .boto config that includes user-specific 998 # credentials (for access to user data). 999 return ready_handlers[-1] 1000 1001 1002def detect_potential_sigv4(func): 1003 def _wrapper(self): 1004 if os.environ.get('EC2_USE_SIGV4', False): 1005 return ['hmac-v4'] 1006 1007 if boto.config.get('ec2', 'use-sigv4', False): 1008 return ['hmac-v4'] 1009 1010 if hasattr(self, 'region'): 1011 # If you're making changes here, you should also check 1012 # ``boto/iam/connection.py``, as several things there are also 1013 # endpoint-related. 1014 if getattr(self.region, 'endpoint', ''): 1015 for test in SIGV4_DETECT: 1016 if test in self.region.endpoint: 1017 return ['hmac-v4'] 1018 1019 return func(self) 1020 return _wrapper 1021 1022 1023def detect_potential_s3sigv4(func): 1024 def _wrapper(self): 1025 if os.environ.get('S3_USE_SIGV4', False): 1026 return ['hmac-v4-s3'] 1027 1028 if boto.config.get('s3', 'use-sigv4', False): 1029 return ['hmac-v4-s3'] 1030 1031 if hasattr(self, 'host'): 1032 # If you're making changes here, you should also check 1033 # ``boto/iam/connection.py``, as several things there are also 1034 # endpoint-related. 1035 for test in SIGV4_DETECT: 1036 if test in self.host: 1037 return ['hmac-v4-s3'] 1038 1039 return func(self) 1040 return _wrapper 1041