Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/botocore/auth.py: 20%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Copyright (c) 2012-2013 Mitch Garnaat http://garnaat.org/
2# Copyright 2012-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"). You
5# may not use this file except in compliance with the License. A copy of
6# the License is located at
7#
8# http://aws.amazon.com/apache2.0/
9#
10# or in the "license" file accompanying this file. This file is
11# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
12# ANY KIND, either express or implied. See the License for the specific
13# language governing permissions and limitations under the License.
14import base64
15import calendar
16import datetime
17import functools
18import hmac
19import json
20import logging
21import time
22from collections.abc import Mapping
23from email.utils import formatdate
24from hashlib import sha1, sha256
25from operator import itemgetter
27from botocore.compat import (
28 HAS_CRT,
29 MD5_AVAILABLE, # noqa: F401
30 HTTPHeaders,
31 encodebytes,
32 ensure_unicode,
33 get_current_datetime,
34 parse_qs,
35 quote,
36 unquote,
37 urlsplit,
38 urlunsplit,
39)
40from botocore.exceptions import (
41 NoAuthTokenError,
42 NoCredentialsError,
43 UnknownSignatureVersionError,
44 UnsupportedSignatureVersionError,
45)
46from botocore.utils import (
47 is_valid_ipv6_endpoint_url,
48 normalize_url_path,
49 percent_encode_sequence,
50)
52logger = logging.getLogger(__name__)
55EMPTY_SHA256_HASH = (
56 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
57)
58# This is the buffer size used when calculating sha256 checksums.
59# Experimenting with various buffer sizes showed that this value generally
60# gave the best result (in terms of performance).
61PAYLOAD_BUFFER = 1024 * 1024
62ISO8601 = '%Y-%m-%dT%H:%M:%SZ'
63SIGV4_TIMESTAMP = '%Y%m%dT%H%M%SZ'
64SIGNED_HEADERS_BLACKLIST = [
65 'baggage',
66 'connection',
67 'expect',
68 'keep-alive',
69 'proxy-authenticate',
70 'proxy-authorization',
71 'te',
72 'traceparent',
73 'tracestate',
74 'trailer',
75 'transfer-encoding',
76 'upgrade',
77 'user-agent',
78 'x-amzn-trace-id',
79]
80UNSIGNED_PAYLOAD = 'UNSIGNED-PAYLOAD'
81STREAMING_UNSIGNED_PAYLOAD_TRAILER = 'STREAMING-UNSIGNED-PAYLOAD-TRAILER'
84def _host_from_url(url):
85 # Given URL, derive value for host header. Ensure that value:
86 # 1) is lowercase
87 # 2) excludes port, if it was the default port
88 # 3) excludes userinfo
89 url_parts = urlsplit(url)
90 host = url_parts.hostname # urlsplit's hostname is always lowercase
91 if is_valid_ipv6_endpoint_url(url):
92 host = f'[{host}]'
93 default_ports = {
94 'http': 80,
95 'https': 443,
96 }
97 if url_parts.port is not None:
98 if url_parts.port != default_ports.get(url_parts.scheme):
99 host = f'{host}:{url_parts.port}'
100 return host
103def _get_body_as_dict(request):
104 # For query services, request.data is form-encoded and is already a
105 # dict, but for other services such as rest-json it could be a json
106 # string or bytes. In those cases we attempt to load the data as a
107 # dict.
108 data = request.data
109 if isinstance(data, bytes):
110 data = json.loads(data.decode('utf-8'))
111 elif isinstance(data, str):
112 data = json.loads(data)
113 return data
116class BaseSigner:
117 REQUIRES_REGION = False
118 REQUIRES_TOKEN = False
120 def add_auth(self, request):
121 raise NotImplementedError("add_auth")
124class TokenSigner(BaseSigner):
125 REQUIRES_TOKEN = True
126 """
127 Signers that expect an authorization token to perform the authorization
128 """
130 def __init__(self, auth_token):
131 self.auth_token = auth_token
134class SigV2Auth(BaseSigner):
135 """
136 Sign a request with Signature V2.
137 """
139 def __init__(self, credentials):
140 self.credentials = credentials
142 def calc_signature(self, request, params):
143 logger.debug("Calculating signature using v2 auth.")
144 split = urlsplit(request.url)
145 path = split.path
146 if len(path) == 0:
147 path = '/'
148 string_to_sign = f"{request.method}\n{split.netloc}\n{path}\n"
149 lhmac = hmac.new(
150 self.credentials.secret_key.encode("utf-8"), digestmod=sha256
151 )
152 pairs = []
153 for key in sorted(params):
154 # Any previous signature should not be a part of this
155 # one, so we skip that particular key. This prevents
156 # issues during retries.
157 if key == 'Signature':
158 continue
159 value = str(params[key])
160 quoted_key = quote(key.encode('utf-8'), safe='')
161 quoted_value = quote(value.encode('utf-8'), safe='-_~')
162 pairs.append(f'{quoted_key}={quoted_value}')
163 qs = '&'.join(pairs)
164 string_to_sign += qs
165 logger.debug('String to sign: %s', string_to_sign)
166 lhmac.update(string_to_sign.encode('utf-8'))
167 b64 = base64.b64encode(lhmac.digest()).strip().decode('utf-8')
168 return (qs, b64)
170 def add_auth(self, request):
171 # The auth handler is the last thing called in the
172 # preparation phase of a prepared request.
173 # Because of this we have to parse the query params
174 # from the request body so we can update them with
175 # the sigv2 auth params.
176 if self.credentials is None:
177 raise NoCredentialsError()
178 if request.data:
179 # POST
180 params = request.data
181 else:
182 # GET
183 params = request.params
184 params['AWSAccessKeyId'] = self.credentials.access_key
185 params['SignatureVersion'] = '2'
186 params['SignatureMethod'] = 'HmacSHA256'
187 params['Timestamp'] = time.strftime(ISO8601, time.gmtime())
188 if self.credentials.token:
189 params['SecurityToken'] = self.credentials.token
190 qs, signature = self.calc_signature(request, params)
191 params['Signature'] = signature
192 return request
195class SigV3Auth(BaseSigner):
196 def __init__(self, credentials):
197 self.credentials = credentials
199 def add_auth(self, request):
200 if self.credentials is None:
201 raise NoCredentialsError()
202 if 'Date' in request.headers:
203 del request.headers['Date']
204 request.headers['Date'] = formatdate(usegmt=True)
205 if self.credentials.token:
206 if 'X-Amz-Security-Token' in request.headers:
207 del request.headers['X-Amz-Security-Token']
208 request.headers['X-Amz-Security-Token'] = self.credentials.token
209 new_hmac = hmac.new(
210 self.credentials.secret_key.encode('utf-8'), digestmod=sha256
211 )
212 new_hmac.update(request.headers['Date'].encode('utf-8'))
213 encoded_signature = encodebytes(new_hmac.digest()).strip()
214 signature = (
215 f"AWS3-HTTPS AWSAccessKeyId={self.credentials.access_key},"
216 f"Algorithm=HmacSHA256,Signature={encoded_signature.decode('utf-8')}"
217 )
218 if 'X-Amzn-Authorization' in request.headers:
219 del request.headers['X-Amzn-Authorization']
220 request.headers['X-Amzn-Authorization'] = signature
223class SigV4Auth(BaseSigner):
224 """
225 Sign a request with Signature V4.
226 """
228 REQUIRES_REGION = True
230 def __init__(self, credentials, service_name, region_name):
231 self.credentials = credentials
232 # We initialize these value here so the unit tests can have
233 # valid values. But these will get overriden in ``add_auth``
234 # later for real requests.
235 self._region_name = region_name
236 self._service_name = service_name
238 def _sign(self, key, msg, hex=False):
239 if hex:
240 sig = hmac.new(key, msg.encode('utf-8'), sha256).hexdigest()
241 else:
242 sig = hmac.new(key, msg.encode('utf-8'), sha256).digest()
243 return sig
245 def headers_to_sign(self, request):
246 """
247 Select the headers from the request that need to be included
248 in the StringToSign.
249 """
250 header_map = HTTPHeaders()
251 for name, value in request.headers.items():
252 lname = name.lower()
253 if lname not in SIGNED_HEADERS_BLACKLIST:
254 header_map[lname] = value
255 if 'host' not in header_map:
256 # TODO: We should set the host ourselves, instead of relying on our
257 # HTTP client to set it for us.
258 header_map['host'] = _host_from_url(request.url)
259 return header_map
261 def canonical_query_string(self, request):
262 # The query string can come from two parts. One is the
263 # params attribute of the request. The other is from the request
264 # url (in which case we have to re-split the url into its components
265 # and parse out the query string component).
266 if request.params:
267 return self._canonical_query_string_params(request.params)
268 else:
269 return self._canonical_query_string_url(urlsplit(request.url))
271 def _canonical_query_string_params(self, params):
272 # [(key, value), (key2, value2)]
273 key_val_pairs = []
274 if isinstance(params, Mapping):
275 params = params.items()
276 for key, value in params:
277 key_val_pairs.append(
278 (quote(key, safe='-_.~'), quote(str(value), safe='-_.~'))
279 )
280 sorted_key_vals = []
281 # Sort by the URI-encoded key names, and in the case of
282 # repeated keys, then sort by the value.
283 for key, value in sorted(key_val_pairs):
284 sorted_key_vals.append(f'{key}={value}')
285 canonical_query_string = '&'.join(sorted_key_vals)
286 return canonical_query_string
288 def _canonical_query_string_url(self, parts):
289 canonical_query_string = ''
290 if parts.query:
291 # [(key, value), (key2, value2)]
292 key_val_pairs = []
293 for pair in parts.query.split('&'):
294 key, _, value = pair.partition('=')
295 key_val_pairs.append((key, value))
296 sorted_key_vals = []
297 # Sort by the URI-encoded key names, and in the case of
298 # repeated keys, then sort by the value.
299 for key, value in sorted(key_val_pairs):
300 sorted_key_vals.append(f'{key}={value}')
301 canonical_query_string = '&'.join(sorted_key_vals)
302 return canonical_query_string
304 def canonical_headers(self, headers_to_sign):
305 """
306 Return the headers that need to be included in the StringToSign
307 in their canonical form by converting all header keys to lower
308 case, sorting them in alphabetical order and then joining
309 them into a string, separated by newlines.
310 """
311 headers = []
312 sorted_header_names = sorted(set(headers_to_sign))
313 for key in sorted_header_names:
314 value = ','.join(
315 self._header_value(v) for v in headers_to_sign.get_all(key)
316 )
317 headers.append(f'{key}:{ensure_unicode(value)}')
318 return '\n'.join(headers)
320 def _header_value(self, value):
321 # From the sigv4 docs:
322 # Lowercase(HeaderName) + ':' + Trimall(HeaderValue)
323 #
324 # The Trimall function removes excess white space before and after
325 # values, and converts sequential spaces to a single space.
326 return ' '.join(value.split())
328 def signed_headers(self, headers_to_sign):
329 headers = sorted(n.lower().strip() for n in set(headers_to_sign))
330 return ';'.join(headers)
332 def _is_streaming_checksum_payload(self, request):
333 checksum_context = request.context.get('checksum', {})
334 algorithm = checksum_context.get('request_algorithm')
335 return isinstance(algorithm, dict) and algorithm.get('in') == 'trailer'
337 def payload(self, request):
338 if self._is_streaming_checksum_payload(request):
339 return STREAMING_UNSIGNED_PAYLOAD_TRAILER
340 elif not self._should_sha256_sign_payload(request):
341 # When payload signing is disabled, we use this static string in
342 # place of the payload checksum.
343 return UNSIGNED_PAYLOAD
344 request_body = request.body
345 if request_body and hasattr(request_body, 'seek'):
346 position = request_body.tell()
347 read_chunksize = functools.partial(
348 request_body.read, PAYLOAD_BUFFER
349 )
350 checksum = sha256()
351 for chunk in iter(read_chunksize, b''):
352 checksum.update(chunk)
353 hex_checksum = checksum.hexdigest()
354 request_body.seek(position)
355 return hex_checksum
356 elif request_body:
357 # The request serialization has ensured that
358 # request.body is a bytes() type.
359 return sha256(request_body).hexdigest()
360 else:
361 return EMPTY_SHA256_HASH
363 def _should_sha256_sign_payload(self, request):
364 # Payloads will always be signed over insecure connections.
365 if not request.url.startswith('https'):
366 return True
368 # Certain operations may have payload signing disabled by default.
369 # Since we don't have access to the operation model, we pass in this
370 # bit of metadata through the request context.
371 return request.context.get('payload_signing_enabled', True)
373 def canonical_request(self, request):
374 cr = [request.method.upper()]
375 path = self._normalize_url_path(urlsplit(request.url).path)
376 cr.append(path)
377 cr.append(self.canonical_query_string(request))
378 headers_to_sign = self.headers_to_sign(request)
379 cr.append(self.canonical_headers(headers_to_sign) + '\n')
380 cr.append(self.signed_headers(headers_to_sign))
381 if 'X-Amz-Content-SHA256' in request.headers:
382 body_checksum = request.headers['X-Amz-Content-SHA256']
383 else:
384 body_checksum = self.payload(request)
385 cr.append(body_checksum)
386 return '\n'.join(cr)
388 def _normalize_url_path(self, path):
389 normalized_path = quote(normalize_url_path(path), safe='/~')
390 return normalized_path
392 def scope(self, request):
393 scope = [self.credentials.access_key]
394 scope.append(request.context['timestamp'][0:8])
395 scope.append(self._region_name)
396 scope.append(self._service_name)
397 scope.append('aws4_request')
398 return '/'.join(scope)
400 def credential_scope(self, request):
401 scope = []
402 scope.append(request.context['timestamp'][0:8])
403 scope.append(self._region_name)
404 scope.append(self._service_name)
405 scope.append('aws4_request')
406 return '/'.join(scope)
408 def string_to_sign(self, request, canonical_request):
409 """
410 Return the canonical StringToSign as well as a dict
411 containing the original version of all headers that
412 were included in the StringToSign.
413 """
414 sts = ['AWS4-HMAC-SHA256']
415 sts.append(request.context['timestamp'])
416 sts.append(self.credential_scope(request))
417 sts.append(sha256(canonical_request.encode('utf-8')).hexdigest())
418 return '\n'.join(sts)
420 def signature(self, string_to_sign, request):
421 key = self.credentials.secret_key
422 k_date = self._sign(
423 (f"AWS4{key}").encode(), request.context["timestamp"][0:8]
424 )
425 k_region = self._sign(k_date, self._region_name)
426 k_service = self._sign(k_region, self._service_name)
427 k_signing = self._sign(k_service, 'aws4_request')
428 return self._sign(k_signing, string_to_sign, hex=True)
430 def add_auth(self, request):
431 if self.credentials is None:
432 raise NoCredentialsError()
433 datetime_now = get_current_datetime()
434 request.context['timestamp'] = datetime_now.strftime(SIGV4_TIMESTAMP)
435 # This could be a retry. Make sure the previous
436 # authorization header is removed first.
437 self._modify_request_before_signing(request)
438 canonical_request = self.canonical_request(request)
439 logger.debug("Calculating signature using v4 auth.")
440 logger.debug('CanonicalRequest:\n%s', canonical_request)
441 string_to_sign = self.string_to_sign(request, canonical_request)
442 logger.debug('StringToSign:\n%s', string_to_sign)
443 signature = self.signature(string_to_sign, request)
444 logger.debug('Signature:\n%s', signature)
446 self._inject_signature_to_request(request, signature)
448 def _inject_signature_to_request(self, request, signature):
449 auth_str = [f'AWS4-HMAC-SHA256 Credential={self.scope(request)}']
450 headers_to_sign = self.headers_to_sign(request)
451 auth_str.append(
452 f"SignedHeaders={self.signed_headers(headers_to_sign)}"
453 )
454 auth_str.append(f'Signature={signature}')
455 request.headers['Authorization'] = ', '.join(auth_str)
456 return request
458 def _modify_request_before_signing(self, request):
459 if 'Authorization' in request.headers:
460 del request.headers['Authorization']
461 self._set_necessary_date_headers(request)
462 if self.credentials.token:
463 if 'X-Amz-Security-Token' in request.headers:
464 del request.headers['X-Amz-Security-Token']
465 request.headers['X-Amz-Security-Token'] = self.credentials.token
467 if not request.context.get('payload_signing_enabled', True):
468 if 'X-Amz-Content-SHA256' in request.headers:
469 del request.headers['X-Amz-Content-SHA256']
470 request.headers['X-Amz-Content-SHA256'] = UNSIGNED_PAYLOAD
472 def _set_necessary_date_headers(self, request):
473 # The spec allows for either the Date _or_ the X-Amz-Date value to be
474 # used so we check both. If there's a Date header, we use the date
475 # header. Otherwise we use the X-Amz-Date header.
476 if 'Date' in request.headers:
477 del request.headers['Date']
478 datetime_timestamp = datetime.datetime.strptime(
479 request.context['timestamp'], SIGV4_TIMESTAMP
480 )
481 request.headers['Date'] = formatdate(
482 int(calendar.timegm(datetime_timestamp.timetuple()))
483 )
484 if 'X-Amz-Date' in request.headers:
485 del request.headers['X-Amz-Date']
486 else:
487 if 'X-Amz-Date' in request.headers:
488 del request.headers['X-Amz-Date']
489 request.headers['X-Amz-Date'] = request.context['timestamp']
492class S3SigV4Auth(SigV4Auth):
493 def _modify_request_before_signing(self, request):
494 super()._modify_request_before_signing(request)
495 if 'X-Amz-Content-SHA256' in request.headers:
496 del request.headers['X-Amz-Content-SHA256']
498 request.headers['X-Amz-Content-SHA256'] = self.payload(request)
500 def _should_sha256_sign_payload(self, request):
501 # S3 allows optional body signing, so to minimize the performance
502 # impact, we opt to not SHA256 sign the body on streaming uploads,
503 # provided that we're on https.
504 client_config = request.context.get('client_config')
505 s3_config = getattr(client_config, 's3', None)
507 # The config could be None if it isn't set, or if the customer sets it
508 # to None.
509 if s3_config is None:
510 s3_config = {}
512 # The explicit configuration takes precedence over any implicit
513 # configuration.
514 sign_payload = s3_config.get('payload_signing_enabled', None)
515 if sign_payload is not None:
516 return sign_payload
518 # We require that both a checksum be present and https be enabled
519 # to implicitly disable body signing. The combination of TLS and
520 # a checksum is sufficiently secure and durable for us to be
521 # confident in the request without body signing.
522 checksum_header = 'Content-MD5'
523 checksum_context = request.context.get('checksum', {})
524 algorithm = checksum_context.get('request_algorithm')
525 if isinstance(algorithm, dict) and algorithm.get('in') == 'header':
526 checksum_header = algorithm['name']
527 if (
528 not request.url.startswith("https")
529 or checksum_header not in request.headers
530 ):
531 return True
533 # If the input is streaming we disable body signing by default.
534 if request.context.get('has_streaming_input', False):
535 return False
537 # If the S3-specific checks had no results, delegate to the generic
538 # checks.
539 return super()._should_sha256_sign_payload(request)
541 def _normalize_url_path(self, path):
542 # For S3, we do not normalize the path.
543 return path
546class S3ExpressAuth(S3SigV4Auth):
547 REQUIRES_IDENTITY_CACHE = True
549 def __init__(
550 self, credentials, service_name, region_name, *, identity_cache
551 ):
552 super().__init__(credentials, service_name, region_name)
553 self._identity_cache = identity_cache
555 def add_auth(self, request):
556 super().add_auth(request)
558 def _modify_request_before_signing(self, request):
559 super()._modify_request_before_signing(request)
560 if 'x-amz-s3session-token' not in request.headers:
561 request.headers['x-amz-s3session-token'] = self.credentials.token
562 # S3Express does not support STS' X-Amz-Security-Token
563 if 'X-Amz-Security-Token' in request.headers:
564 del request.headers['X-Amz-Security-Token']
567class S3ExpressPostAuth(S3ExpressAuth):
568 REQUIRES_IDENTITY_CACHE = True
570 def add_auth(self, request):
571 datetime_now = get_current_datetime()
572 request.context['timestamp'] = datetime_now.strftime(SIGV4_TIMESTAMP)
574 fields = {}
575 if request.context.get('s3-presign-post-fields', None) is not None:
576 fields = request.context['s3-presign-post-fields']
578 policy = {}
579 conditions = []
580 if request.context.get('s3-presign-post-policy', None) is not None:
581 policy = request.context['s3-presign-post-policy']
582 if policy.get('conditions', None) is not None:
583 conditions = policy['conditions']
585 policy['conditions'] = conditions
587 fields['x-amz-algorithm'] = 'AWS4-HMAC-SHA256'
588 fields['x-amz-credential'] = self.scope(request)
589 fields['x-amz-date'] = request.context['timestamp']
591 conditions.append({'x-amz-algorithm': 'AWS4-HMAC-SHA256'})
592 conditions.append({'x-amz-credential': self.scope(request)})
593 conditions.append({'x-amz-date': request.context['timestamp']})
595 if self.credentials.token is not None:
596 fields['X-Amz-S3session-Token'] = self.credentials.token
597 conditions.append(
598 {'X-Amz-S3session-Token': self.credentials.token}
599 )
601 # Dump the base64 encoded policy into the fields dictionary.
602 fields['policy'] = base64.b64encode(
603 json.dumps(policy).encode('utf-8')
604 ).decode('utf-8')
606 fields['x-amz-signature'] = self.signature(fields['policy'], request)
608 request.context['s3-presign-post-fields'] = fields
609 request.context['s3-presign-post-policy'] = policy
612class S3ExpressQueryAuth(S3ExpressAuth):
613 DEFAULT_EXPIRES = 300
614 REQUIRES_IDENTITY_CACHE = True
616 def __init__(
617 self,
618 credentials,
619 service_name,
620 region_name,
621 *,
622 identity_cache,
623 expires=DEFAULT_EXPIRES,
624 ):
625 super().__init__(
626 credentials,
627 service_name,
628 region_name,
629 identity_cache=identity_cache,
630 )
631 self._expires = expires
633 def _modify_request_before_signing(self, request):
634 # We automatically set this header, so if it's the auto-set value we
635 # want to get rid of it since it doesn't make sense for presigned urls.
636 content_type = request.headers.get('content-type')
637 blocklisted_content_type = (
638 'application/x-www-form-urlencoded; charset=utf-8'
639 )
640 if content_type == blocklisted_content_type:
641 del request.headers['content-type']
643 # Note that we're not including X-Amz-Signature.
644 # From the docs: "The Canonical Query String must include all the query
645 # parameters from the preceding table except for X-Amz-Signature.
646 signed_headers = self.signed_headers(self.headers_to_sign(request))
648 auth_params = {
649 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
650 'X-Amz-Credential': self.scope(request),
651 'X-Amz-Date': request.context['timestamp'],
652 'X-Amz-Expires': self._expires,
653 'X-Amz-SignedHeaders': signed_headers,
654 }
655 if self.credentials.token is not None:
656 auth_params['X-Amz-S3session-Token'] = self.credentials.token
657 # Now parse the original query string to a dict, inject our new query
658 # params, and serialize back to a query string.
659 url_parts = urlsplit(request.url)
660 # parse_qs makes each value a list, but in our case we know we won't
661 # have repeated keys so we know we have single element lists which we
662 # can convert back to scalar values.
663 query_string_parts = parse_qs(url_parts.query, keep_blank_values=True)
664 query_dict = {k: v[0] for k, v in query_string_parts.items()}
666 if request.params:
667 query_dict.update(request.params)
668 request.params = {}
669 # The spec is particular about this. It *has* to be:
670 # https://<endpoint>?<operation params>&<auth params>
671 # You can't mix the two types of params together, i.e just keep doing
672 # new_query_params.update(op_params)
673 # new_query_params.update(auth_params)
674 # percent_encode_sequence(new_query_params)
675 operation_params = ''
676 if request.data:
677 # We also need to move the body params into the query string. To
678 # do this, we first have to convert it to a dict.
679 query_dict.update(_get_body_as_dict(request))
680 request.data = ''
681 if query_dict:
682 operation_params = percent_encode_sequence(query_dict) + '&'
683 new_query_string = (
684 f"{operation_params}{percent_encode_sequence(auth_params)}"
685 )
686 # url_parts is a tuple (and therefore immutable) so we need to create
687 # a new url_parts with the new query string.
688 # <part> - <index>
689 # scheme - 0
690 # netloc - 1
691 # path - 2
692 # query - 3 <-- we're replacing this.
693 # fragment - 4
694 p = url_parts
695 new_url_parts = (p[0], p[1], p[2], new_query_string, p[4])
696 request.url = urlunsplit(new_url_parts)
698 def _inject_signature_to_request(self, request, signature):
699 # Rather than calculating an "Authorization" header, for the query
700 # param quth, we just append an 'X-Amz-Signature' param to the end
701 # of the query string.
702 request.url += f'&X-Amz-Signature={signature}'
704 def _normalize_url_path(self, path):
705 # For S3, we do not normalize the path.
706 return path
708 def payload(self, request):
709 # From the doc link above:
710 # "You don't include a payload hash in the Canonical Request, because
711 # when you create a presigned URL, you don't know anything about the
712 # payload. Instead, you use a constant string "UNSIGNED-PAYLOAD".
713 return UNSIGNED_PAYLOAD
716class SigV4QueryAuth(SigV4Auth):
717 DEFAULT_EXPIRES = 3600
719 def __init__(
720 self, credentials, service_name, region_name, expires=DEFAULT_EXPIRES
721 ):
722 super().__init__(credentials, service_name, region_name)
723 self._expires = expires
725 def _modify_request_before_signing(self, request):
726 # We automatically set this header, so if it's the auto-set value we
727 # want to get rid of it since it doesn't make sense for presigned urls.
728 content_type = request.headers.get('content-type')
729 blacklisted_content_type = (
730 'application/x-www-form-urlencoded; charset=utf-8'
731 )
732 if content_type == blacklisted_content_type:
733 del request.headers['content-type']
735 # Note that we're not including X-Amz-Signature.
736 # From the docs: "The Canonical Query String must include all the query
737 # parameters from the preceding table except for X-Amz-Signature.
738 signed_headers = self.signed_headers(self.headers_to_sign(request))
740 auth_params = {
741 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
742 'X-Amz-Credential': self.scope(request),
743 'X-Amz-Date': request.context['timestamp'],
744 'X-Amz-Expires': self._expires,
745 'X-Amz-SignedHeaders': signed_headers,
746 }
747 if self.credentials.token is not None:
748 auth_params['X-Amz-Security-Token'] = self.credentials.token
749 # Now parse the original query string to a dict, inject our new query
750 # params, and serialize back to a query string.
751 url_parts = urlsplit(request.url)
752 # parse_qs makes each value a list, but in our case we know we won't
753 # have repeated keys so we know we have single element lists which we
754 # can convert back to scalar values.
755 query_string_parts = parse_qs(url_parts.query, keep_blank_values=True)
756 query_dict = {k: v[0] for k, v in query_string_parts.items()}
758 if request.params:
759 query_dict.update(request.params)
760 request.params = {}
761 # The spec is particular about this. It *has* to be:
762 # https://<endpoint>?<operation params>&<auth params>
763 # You can't mix the two types of params together, i.e just keep doing
764 # new_query_params.update(op_params)
765 # new_query_params.update(auth_params)
766 # percent_encode_sequence(new_query_params)
767 operation_params = ''
768 if request.data:
769 # We also need to move the body params into the query string. To
770 # do this, we first have to convert it to a dict.
771 query_dict.update(_get_body_as_dict(request))
772 request.data = ''
773 if query_dict:
774 operation_params = percent_encode_sequence(query_dict) + '&'
775 new_query_string = (
776 f"{operation_params}{percent_encode_sequence(auth_params)}"
777 )
778 # url_parts is a tuple (and therefore immutable) so we need to create
779 # a new url_parts with the new query string.
780 # <part> - <index>
781 # scheme - 0
782 # netloc - 1
783 # path - 2
784 # query - 3 <-- we're replacing this.
785 # fragment - 4
786 p = url_parts
787 new_url_parts = (p[0], p[1], p[2], new_query_string, p[4])
788 request.url = urlunsplit(new_url_parts)
790 def _inject_signature_to_request(self, request, signature):
791 # Rather than calculating an "Authorization" header, for the query
792 # param quth, we just append an 'X-Amz-Signature' param to the end
793 # of the query string.
794 request.url += f'&X-Amz-Signature={signature}'
797class S3SigV4QueryAuth(SigV4QueryAuth):
798 """S3 SigV4 auth using query parameters.
800 This signer will sign a request using query parameters and signature
801 version 4, i.e a "presigned url" signer.
803 Based off of:
805 http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
807 """
809 def _normalize_url_path(self, path):
810 # For S3, we do not normalize the path.
811 return path
813 def payload(self, request):
814 # From the doc link above:
815 # "You don't include a payload hash in the Canonical Request, because
816 # when you create a presigned URL, you don't know anything about the
817 # payload. Instead, you use a constant string "UNSIGNED-PAYLOAD".
818 return UNSIGNED_PAYLOAD
821class S3SigV4PostAuth(SigV4Auth):
822 """
823 Presigns a s3 post
825 Implementation doc here:
826 http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-UsingHTTPPOST.html
827 """
829 def add_auth(self, request):
830 datetime_now = get_current_datetime()
831 request.context['timestamp'] = datetime_now.strftime(SIGV4_TIMESTAMP)
833 fields = {}
834 if request.context.get('s3-presign-post-fields', None) is not None:
835 fields = request.context['s3-presign-post-fields']
837 policy = {}
838 conditions = []
839 if request.context.get('s3-presign-post-policy', None) is not None:
840 policy = request.context['s3-presign-post-policy']
841 if policy.get('conditions', None) is not None:
842 conditions = policy['conditions']
844 policy['conditions'] = conditions
846 fields['x-amz-algorithm'] = 'AWS4-HMAC-SHA256'
847 fields['x-amz-credential'] = self.scope(request)
848 fields['x-amz-date'] = request.context['timestamp']
850 conditions.append({'x-amz-algorithm': 'AWS4-HMAC-SHA256'})
851 conditions.append({'x-amz-credential': self.scope(request)})
852 conditions.append({'x-amz-date': request.context['timestamp']})
854 if self.credentials.token is not None:
855 fields['x-amz-security-token'] = self.credentials.token
856 conditions.append({'x-amz-security-token': self.credentials.token})
858 # Dump the base64 encoded policy into the fields dictionary.
859 fields['policy'] = base64.b64encode(
860 json.dumps(policy).encode('utf-8')
861 ).decode('utf-8')
863 fields['x-amz-signature'] = self.signature(fields['policy'], request)
865 request.context['s3-presign-post-fields'] = fields
866 request.context['s3-presign-post-policy'] = policy
869class HmacV1Auth(BaseSigner):
870 # List of Query String Arguments of Interest
871 QSAOfInterest = [
872 'accelerate',
873 'acl',
874 'cors',
875 'defaultObjectAcl',
876 'location',
877 'logging',
878 'partNumber',
879 'policy',
880 'requestPayment',
881 'torrent',
882 'versioning',
883 'versionId',
884 'versions',
885 'website',
886 'uploads',
887 'uploadId',
888 'response-content-type',
889 'response-content-language',
890 'response-expires',
891 'response-cache-control',
892 'response-content-disposition',
893 'response-content-encoding',
894 'delete',
895 'lifecycle',
896 'tagging',
897 'restore',
898 'storageClass',
899 'notification',
900 'replication',
901 'requestPayment',
902 'analytics',
903 'metrics',
904 'inventory',
905 'select',
906 'select-type',
907 'object-lock',
908 ]
910 def __init__(self, credentials, service_name=None, region_name=None):
911 self.credentials = credentials
913 def sign_string(self, string_to_sign):
914 new_hmac = hmac.new(
915 self.credentials.secret_key.encode('utf-8'), digestmod=sha1
916 )
917 new_hmac.update(string_to_sign.encode('utf-8'))
918 return encodebytes(new_hmac.digest()).strip().decode('utf-8')
920 def canonical_standard_headers(self, headers):
921 interesting_headers = ['content-md5', 'content-type', 'date']
922 hoi = []
923 if 'Date' in headers:
924 del headers['Date']
925 headers['Date'] = self._get_date()
926 for ih in interesting_headers:
927 found = False
928 for key in headers:
929 lk = key.lower()
930 if headers[key] is not None and lk == ih:
931 hoi.append(headers[key].strip())
932 found = True
933 if not found:
934 hoi.append('')
935 return '\n'.join(hoi)
937 def canonical_custom_headers(self, headers):
938 hoi = []
939 custom_headers = {}
940 for key in headers:
941 lk = key.lower()
942 if headers[key] is not None:
943 if lk.startswith('x-amz-'):
944 custom_headers[lk] = ','.join(
945 v.strip() for v in headers.get_all(key)
946 )
947 sorted_header_keys = sorted(custom_headers.keys())
948 for key in sorted_header_keys:
949 hoi.append(f"{key}:{custom_headers[key]}")
950 return '\n'.join(hoi)
952 def unquote_v(self, nv):
953 """
954 TODO: Do we need this?
955 """
956 if len(nv) == 1:
957 return nv
958 else:
959 return (nv[0], unquote(nv[1]))
961 def canonical_resource(self, split, auth_path=None):
962 # don't include anything after the first ? in the resource...
963 # unless it is one of the QSA of interest, defined above
964 # NOTE:
965 # The path in the canonical resource should always be the
966 # full path including the bucket name, even for virtual-hosting
967 # style addressing. The ``auth_path`` keeps track of the full
968 # path for the canonical resource and would be passed in if
969 # the client was using virtual-hosting style.
970 if auth_path is not None:
971 buf = auth_path
972 else:
973 buf = split.path
974 if split.query:
975 qsa = split.query.split('&')
976 qsa = [a.split('=', 1) for a in qsa]
977 qsa = [
978 self.unquote_v(a) for a in qsa if a[0] in self.QSAOfInterest
979 ]
980 if len(qsa) > 0:
981 qsa.sort(key=itemgetter(0))
982 qsa = ['='.join(a) for a in qsa]
983 buf += '?'
984 buf += '&'.join(qsa)
985 return buf
987 def canonical_string(
988 self, method, split, headers, expires=None, auth_path=None
989 ):
990 cs = method.upper() + '\n'
991 cs += self.canonical_standard_headers(headers) + '\n'
992 custom_headers = self.canonical_custom_headers(headers)
993 if custom_headers:
994 cs += custom_headers + '\n'
995 cs += self.canonical_resource(split, auth_path=auth_path)
996 return cs
998 def get_signature(
999 self, method, split, headers, expires=None, auth_path=None
1000 ):
1001 if self.credentials.token:
1002 del headers['x-amz-security-token']
1003 headers['x-amz-security-token'] = self.credentials.token
1004 string_to_sign = self.canonical_string(
1005 method, split, headers, auth_path=auth_path
1006 )
1007 logger.debug('StringToSign:\n%s', string_to_sign)
1008 return self.sign_string(string_to_sign)
1010 def add_auth(self, request):
1011 if self.credentials is None:
1012 raise NoCredentialsError
1013 logger.debug("Calculating signature using hmacv1 auth.")
1014 split = urlsplit(request.url)
1015 logger.debug("HTTP request method: %s", request.method)
1016 signature = self.get_signature(
1017 request.method, split, request.headers, auth_path=request.auth_path
1018 )
1019 self._inject_signature(request, signature)
1021 def _get_date(self):
1022 return formatdate(usegmt=True)
1024 def _inject_signature(self, request, signature):
1025 if 'Authorization' in request.headers:
1026 # We have to do this because request.headers is not
1027 # normal dictionary. It has the (unintuitive) behavior
1028 # of aggregating repeated setattr calls for the same
1029 # key value. For example:
1030 # headers['foo'] = 'a'; headers['foo'] = 'b'
1031 # list(headers) will print ['foo', 'foo'].
1032 del request.headers['Authorization']
1034 auth_header = f"AWS {self.credentials.access_key}:{signature}"
1035 request.headers['Authorization'] = auth_header
1038class HmacV1QueryAuth(HmacV1Auth):
1039 """
1040 Generates a presigned request for s3.
1042 Spec from this document:
1044 http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html
1045 #RESTAuthenticationQueryStringAuth
1047 """
1049 DEFAULT_EXPIRES = 3600
1051 def __init__(self, credentials, expires=DEFAULT_EXPIRES):
1052 self.credentials = credentials
1053 self._expires = expires
1055 def _get_date(self):
1056 return str(int(time.time() + int(self._expires)))
1058 def _inject_signature(self, request, signature):
1059 query_dict = {}
1060 query_dict['AWSAccessKeyId'] = self.credentials.access_key
1061 query_dict['Signature'] = signature
1063 for header_key in request.headers:
1064 lk = header_key.lower()
1065 # For query string requests, Expires is used instead of the
1066 # Date header.
1067 if header_key == 'Date':
1068 query_dict['Expires'] = request.headers['Date']
1069 # We only want to include relevant headers in the query string.
1070 # These can be anything that starts with x-amz, is Content-MD5,
1071 # or is Content-Type.
1072 elif lk.startswith('x-amz-') or lk in (
1073 'content-md5',
1074 'content-type',
1075 ):
1076 query_dict[lk] = request.headers[lk]
1077 # Combine all of the identified headers into an encoded
1078 # query string
1079 new_query_string = percent_encode_sequence(query_dict)
1081 # Create a new url with the presigned url.
1082 p = urlsplit(request.url)
1083 if p[3]:
1084 # If there was a pre-existing query string, we should
1085 # add that back before injecting the new query string.
1086 new_query_string = f'{p[3]}&{new_query_string}'
1087 new_url_parts = (p[0], p[1], p[2], new_query_string, p[4])
1088 request.url = urlunsplit(new_url_parts)
1091class HmacV1PostAuth(HmacV1Auth):
1092 """
1093 Generates a presigned post for s3.
1095 Spec from this document:
1097 http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingHTTPPOST.html
1098 """
1100 def add_auth(self, request):
1101 fields = {}
1102 if request.context.get('s3-presign-post-fields', None) is not None:
1103 fields = request.context['s3-presign-post-fields']
1105 policy = {}
1106 conditions = []
1107 if request.context.get('s3-presign-post-policy', None) is not None:
1108 policy = request.context['s3-presign-post-policy']
1109 if policy.get('conditions', None) is not None:
1110 conditions = policy['conditions']
1112 policy['conditions'] = conditions
1114 fields['AWSAccessKeyId'] = self.credentials.access_key
1116 if self.credentials.token is not None:
1117 fields['x-amz-security-token'] = self.credentials.token
1118 conditions.append({'x-amz-security-token': self.credentials.token})
1120 # Dump the base64 encoded policy into the fields dictionary.
1121 fields['policy'] = base64.b64encode(
1122 json.dumps(policy).encode('utf-8')
1123 ).decode('utf-8')
1125 fields['signature'] = self.sign_string(fields['policy'])
1127 request.context['s3-presign-post-fields'] = fields
1128 request.context['s3-presign-post-policy'] = policy
1131class BearerAuth(TokenSigner):
1132 """
1133 Performs bearer token authorization by placing the bearer token in the
1134 Authorization header as specified by Section 2.1 of RFC 6750.
1136 https://datatracker.ietf.org/doc/html/rfc6750#section-2.1
1137 """
1139 def add_auth(self, request):
1140 if self.auth_token is None:
1141 raise NoAuthTokenError()
1143 auth_header = f'Bearer {self.auth_token.token}'
1144 if 'Authorization' in request.headers:
1145 del request.headers['Authorization']
1146 request.headers['Authorization'] = auth_header
1149def resolve_auth_type(auth_trait):
1150 for auth_type in auth_trait:
1151 if auth_type == 'smithy.api#noAuth':
1152 return AUTH_TYPE_TO_SIGNATURE_VERSION[auth_type]
1153 elif auth_type in AUTH_TYPE_TO_SIGNATURE_VERSION:
1154 signature_version = AUTH_TYPE_TO_SIGNATURE_VERSION[auth_type]
1155 if signature_version in AUTH_TYPE_MAPS:
1156 return signature_version
1157 else:
1158 raise UnknownSignatureVersionError(signature_version=auth_type)
1159 raise UnsupportedSignatureVersionError(signature_version=auth_trait)
1162def resolve_auth_scheme_preference(preference_list, auth_options):
1163 service_supported = [scheme.split('#')[-1] for scheme in auth_options]
1165 unsupported = [
1166 scheme
1167 for scheme in preference_list
1168 if scheme not in AUTH_PREF_TO_SIGNATURE_VERSION
1169 ]
1170 if unsupported:
1171 logger.debug(
1172 "Unsupported auth schemes in preference list: %r", unsupported
1173 )
1175 combined = preference_list + service_supported
1176 prioritized_schemes = [
1177 scheme
1178 for scheme in dict.fromkeys(combined)
1179 if scheme in service_supported
1180 ]
1182 for scheme in prioritized_schemes:
1183 if scheme == 'noAuth':
1184 return AUTH_PREF_TO_SIGNATURE_VERSION[scheme]
1185 sig_version = AUTH_PREF_TO_SIGNATURE_VERSION.get(scheme)
1186 if sig_version in AUTH_TYPE_MAPS:
1187 return sig_version
1189 raise UnsupportedSignatureVersionError(
1190 signature_version=', '.join(sorted(service_supported))
1191 )
1194AUTH_TYPE_MAPS = {
1195 'v2': SigV2Auth,
1196 'v3': SigV3Auth,
1197 'v3https': SigV3Auth,
1198 's3': HmacV1Auth,
1199 's3-query': HmacV1QueryAuth,
1200 's3-presign-post': HmacV1PostAuth,
1201 's3v4-presign-post': S3SigV4PostAuth,
1202 'v4-s3express': S3ExpressAuth,
1203 'v4-s3express-query': S3ExpressQueryAuth,
1204 'v4-s3express-presign-post': S3ExpressPostAuth,
1205 'bearer': BearerAuth,
1206}
1208# Define v4 signers depending on if CRT is present
1209if HAS_CRT:
1210 from botocore.crt.auth import CRT_AUTH_TYPE_MAPS
1212 AUTH_TYPE_MAPS.update(CRT_AUTH_TYPE_MAPS)
1213else:
1214 AUTH_TYPE_MAPS.update(
1215 {
1216 'v4': SigV4Auth,
1217 'v4-query': SigV4QueryAuth,
1218 's3v4': S3SigV4Auth,
1219 's3v4-query': S3SigV4QueryAuth,
1220 }
1221 )
1223AUTH_TYPE_TO_SIGNATURE_VERSION = {
1224 'aws.auth#sigv4': 'v4',
1225 'aws.auth#sigv4a': 'v4a',
1226 'smithy.api#httpBearerAuth': 'bearer',
1227 'smithy.api#noAuth': 'none',
1228}
1230# Mapping used specifically for resolving user-configured auth scheme preferences.
1231# This is similar to AUTH_TYPE_TO_SIGNATURE_VERSION, but uses simplified keys by
1232# stripping the auth trait prefixes ('smithy.api#httpBearerAuth' → 'httpBearerAuth').
1233# These simplified keys match what customers are expected to provide in configuration.
1234AUTH_PREF_TO_SIGNATURE_VERSION = {
1235 auth_scheme.split('#')[-1]: sig_version
1236 for auth_scheme, sig_version in AUTH_TYPE_TO_SIGNATURE_VERSION.items()
1237}