auth.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868
  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.
  14. import base64
  15. import calendar
  16. import datetime
  17. import functools
  18. from email.utils import formatdate
  19. from hashlib import sha1, sha256
  20. import hmac
  21. from io import BytesIO
  22. import logging
  23. from operator import itemgetter
  24. import time
  25. from botocore.compat import(
  26. encodebytes, ensure_unicode, HTTPHeaders, json, parse_qs, quote,
  27. six, unquote, urlsplit, urlunsplit, HAS_CRT, MD5_AVAILABLE
  28. )
  29. from botocore.exceptions import NoCredentialsError
  30. from botocore.utils import normalize_url_path, percent_encode_sequence
  31. logger = logging.getLogger(__name__)
  32. EMPTY_SHA256_HASH = (
  33. 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')
  34. # This is the buffer size used when calculating sha256 checksums.
  35. # Experimenting with various buffer sizes showed that this value generally
  36. # gave the best result (in terms of performance).
  37. PAYLOAD_BUFFER = 1024 * 1024
  38. ISO8601 = '%Y-%m-%dT%H:%M:%SZ'
  39. SIGV4_TIMESTAMP = '%Y%m%dT%H%M%SZ'
  40. SIGNED_HEADERS_BLACKLIST = [
  41. 'expect',
  42. 'user-agent',
  43. 'x-amzn-trace-id',
  44. ]
  45. UNSIGNED_PAYLOAD = 'UNSIGNED-PAYLOAD'
  46. def _host_from_url(url):
  47. # Given URL, derive value for host header. Ensure that value:
  48. # 1) is lowercase
  49. # 2) excludes port, if it was the default port
  50. # 3) excludes userinfo
  51. url_parts = urlsplit(url)
  52. host = url_parts.hostname # urlsplit's hostname is always lowercase
  53. default_ports = {
  54. 'http': 80,
  55. 'https': 443
  56. }
  57. if url_parts.port is not None:
  58. if url_parts.port != default_ports.get(url_parts.scheme):
  59. host = '%s:%d' % (host, url_parts.port)
  60. return host
  61. def _get_body_as_dict(request):
  62. # For query services, request.data is form-encoded and is already a
  63. # dict, but for other services such as rest-json it could be a json
  64. # string or bytes. In those cases we attempt to load the data as a
  65. # dict.
  66. data = request.data
  67. if isinstance(data, six.binary_type):
  68. data = json.loads(data.decode('utf-8'))
  69. elif isinstance(data, six.string_types):
  70. data = json.loads(data)
  71. return data
  72. class BaseSigner(object):
  73. REQUIRES_REGION = False
  74. def add_auth(self, request):
  75. raise NotImplementedError("add_auth")
  76. class SigV2Auth(BaseSigner):
  77. """
  78. Sign a request with Signature V2.
  79. """
  80. def __init__(self, credentials):
  81. self.credentials = credentials
  82. def calc_signature(self, request, params):
  83. logger.debug("Calculating signature using v2 auth.")
  84. split = urlsplit(request.url)
  85. path = split.path
  86. if len(path) == 0:
  87. path = '/'
  88. string_to_sign = '%s\n%s\n%s\n' % (request.method,
  89. split.netloc,
  90. path)
  91. lhmac = hmac.new(self.credentials.secret_key.encode('utf-8'),
  92. digestmod=sha256)
  93. pairs = []
  94. for key in sorted(params):
  95. # Any previous signature should not be a part of this
  96. # one, so we skip that particular key. This prevents
  97. # issues during retries.
  98. if key == 'Signature':
  99. continue
  100. value = six.text_type(params[key])
  101. pairs.append(quote(key.encode('utf-8'), safe='') + '=' +
  102. quote(value.encode('utf-8'), safe='-_~'))
  103. qs = '&'.join(pairs)
  104. string_to_sign += qs
  105. logger.debug('String to sign: %s', string_to_sign)
  106. lhmac.update(string_to_sign.encode('utf-8'))
  107. b64 = base64.b64encode(lhmac.digest()).strip().decode('utf-8')
  108. return (qs, b64)
  109. def add_auth(self, request):
  110. # The auth handler is the last thing called in the
  111. # preparation phase of a prepared request.
  112. # Because of this we have to parse the query params
  113. # from the request body so we can update them with
  114. # the sigv2 auth params.
  115. if self.credentials is None:
  116. raise NoCredentialsError()
  117. if request.data:
  118. # POST
  119. params = request.data
  120. else:
  121. # GET
  122. params = request.params
  123. params['AWSAccessKeyId'] = self.credentials.access_key
  124. params['SignatureVersion'] = '2'
  125. params['SignatureMethod'] = 'HmacSHA256'
  126. params['Timestamp'] = time.strftime(ISO8601, time.gmtime())
  127. if self.credentials.token:
  128. params['SecurityToken'] = self.credentials.token
  129. qs, signature = self.calc_signature(request, params)
  130. params['Signature'] = signature
  131. return request
  132. class SigV3Auth(BaseSigner):
  133. def __init__(self, credentials):
  134. self.credentials = credentials
  135. def add_auth(self, request):
  136. if self.credentials is None:
  137. raise NoCredentialsError()
  138. if 'Date' in request.headers:
  139. del request.headers['Date']
  140. request.headers['Date'] = formatdate(usegmt=True)
  141. if self.credentials.token:
  142. if 'X-Amz-Security-Token' in request.headers:
  143. del request.headers['X-Amz-Security-Token']
  144. request.headers['X-Amz-Security-Token'] = self.credentials.token
  145. new_hmac = hmac.new(self.credentials.secret_key.encode('utf-8'),
  146. digestmod=sha256)
  147. new_hmac.update(request.headers['Date'].encode('utf-8'))
  148. encoded_signature = encodebytes(new_hmac.digest()).strip()
  149. signature = ('AWS3-HTTPS AWSAccessKeyId=%s,Algorithm=%s,Signature=%s' %
  150. (self.credentials.access_key, 'HmacSHA256',
  151. encoded_signature.decode('utf-8')))
  152. if 'X-Amzn-Authorization' in request.headers:
  153. del request.headers['X-Amzn-Authorization']
  154. request.headers['X-Amzn-Authorization'] = signature
  155. class SigV4Auth(BaseSigner):
  156. """
  157. Sign a request with Signature V4.
  158. """
  159. REQUIRES_REGION = True
  160. def __init__(self, credentials, service_name, region_name):
  161. self.credentials = credentials
  162. # We initialize these value here so the unit tests can have
  163. # valid values. But these will get overriden in ``add_auth``
  164. # later for real requests.
  165. self._region_name = region_name
  166. self._service_name = service_name
  167. def _sign(self, key, msg, hex=False):
  168. if hex:
  169. sig = hmac.new(key, msg.encode('utf-8'), sha256).hexdigest()
  170. else:
  171. sig = hmac.new(key, msg.encode('utf-8'), sha256).digest()
  172. return sig
  173. def headers_to_sign(self, request):
  174. """
  175. Select the headers from the request that need to be included
  176. in the StringToSign.
  177. """
  178. header_map = HTTPHeaders()
  179. for name, value in request.headers.items():
  180. lname = name.lower()
  181. if lname not in SIGNED_HEADERS_BLACKLIST:
  182. header_map[lname] = value
  183. if 'host' not in header_map:
  184. # TODO: We should set the host ourselves, instead of relying on our
  185. # HTTP client to set it for us.
  186. header_map['host'] = _host_from_url(request.url)
  187. return header_map
  188. def canonical_query_string(self, request):
  189. # The query string can come from two parts. One is the
  190. # params attribute of the request. The other is from the request
  191. # url (in which case we have to re-split the url into its components
  192. # and parse out the query string component).
  193. if request.params:
  194. return self._canonical_query_string_params(request.params)
  195. else:
  196. return self._canonical_query_string_url(urlsplit(request.url))
  197. def _canonical_query_string_params(self, params):
  198. # [(key, value), (key2, value2)]
  199. key_val_pairs = []
  200. for key in params:
  201. value = str(params[key])
  202. key_val_pairs.append((quote(key, safe='-_.~'),
  203. quote(value, safe='-_.~')))
  204. sorted_key_vals = []
  205. # Sort by the URI-encoded key names, and in the case of
  206. # repeated keys, then sort by the value.
  207. for key, value in sorted(key_val_pairs):
  208. sorted_key_vals.append('%s=%s' % (key, value))
  209. canonical_query_string = '&'.join(sorted_key_vals)
  210. return canonical_query_string
  211. def _canonical_query_string_url(self, parts):
  212. canonical_query_string = ''
  213. if parts.query:
  214. # [(key, value), (key2, value2)]
  215. key_val_pairs = []
  216. for pair in parts.query.split('&'):
  217. key, _, value = pair.partition('=')
  218. key_val_pairs.append((key, value))
  219. sorted_key_vals = []
  220. # Sort by the URI-encoded key names, and in the case of
  221. # repeated keys, then sort by the value.
  222. for key, value in sorted(key_val_pairs):
  223. sorted_key_vals.append('%s=%s' % (key, value))
  224. canonical_query_string = '&'.join(sorted_key_vals)
  225. return canonical_query_string
  226. def canonical_headers(self, headers_to_sign):
  227. """
  228. Return the headers that need to be included in the StringToSign
  229. in their canonical form by converting all header keys to lower
  230. case, sorting them in alphabetical order and then joining
  231. them into a string, separated by newlines.
  232. """
  233. headers = []
  234. sorted_header_names = sorted(set(headers_to_sign))
  235. for key in sorted_header_names:
  236. value = ','.join(self._header_value(v) for v in
  237. headers_to_sign.get_all(key))
  238. headers.append('%s:%s' % (key, ensure_unicode(value)))
  239. return '\n'.join(headers)
  240. def _header_value(self, value):
  241. # From the sigv4 docs:
  242. # Lowercase(HeaderName) + ':' + Trimall(HeaderValue)
  243. #
  244. # The Trimall function removes excess white space before and after
  245. # values, and converts sequential spaces to a single space.
  246. return ' '.join(value.split())
  247. def signed_headers(self, headers_to_sign):
  248. l = ['%s' % n.lower().strip() for n in set(headers_to_sign)]
  249. l = sorted(l)
  250. return ';'.join(l)
  251. def payload(self, request):
  252. if not self._should_sha256_sign_payload(request):
  253. # When payload signing is disabled, we use this static string in
  254. # place of the payload checksum.
  255. return UNSIGNED_PAYLOAD
  256. request_body = request.body
  257. if request_body and hasattr(request_body, 'seek'):
  258. position = request_body.tell()
  259. read_chunksize = functools.partial(request_body.read,
  260. PAYLOAD_BUFFER)
  261. checksum = sha256()
  262. for chunk in iter(read_chunksize, b''):
  263. checksum.update(chunk)
  264. hex_checksum = checksum.hexdigest()
  265. request_body.seek(position)
  266. return hex_checksum
  267. elif request_body:
  268. # The request serialization has ensured that
  269. # request.body is a bytes() type.
  270. return sha256(request_body).hexdigest()
  271. else:
  272. return EMPTY_SHA256_HASH
  273. def _should_sha256_sign_payload(self, request):
  274. # Payloads will always be signed over insecure connections.
  275. if not request.url.startswith('https'):
  276. return True
  277. # Certain operations may have payload signing disabled by default.
  278. # Since we don't have access to the operation model, we pass in this
  279. # bit of metadata through the request context.
  280. return request.context.get('payload_signing_enabled', True)
  281. def canonical_request(self, request):
  282. cr = [request.method.upper()]
  283. path = self._normalize_url_path(urlsplit(request.url).path)
  284. cr.append(path)
  285. cr.append(self.canonical_query_string(request))
  286. headers_to_sign = self.headers_to_sign(request)
  287. cr.append(self.canonical_headers(headers_to_sign) + '\n')
  288. cr.append(self.signed_headers(headers_to_sign))
  289. if 'X-Amz-Content-SHA256' in request.headers:
  290. body_checksum = request.headers['X-Amz-Content-SHA256']
  291. else:
  292. body_checksum = self.payload(request)
  293. cr.append(body_checksum)
  294. return '\n'.join(cr)
  295. def _normalize_url_path(self, path):
  296. normalized_path = quote(normalize_url_path(path), safe='/~')
  297. return normalized_path
  298. def scope(self, request):
  299. scope = [self.credentials.access_key]
  300. scope.append(request.context['timestamp'][0:8])
  301. scope.append(self._region_name)
  302. scope.append(self._service_name)
  303. scope.append('aws4_request')
  304. return '/'.join(scope)
  305. def credential_scope(self, request):
  306. scope = []
  307. scope.append(request.context['timestamp'][0:8])
  308. scope.append(self._region_name)
  309. scope.append(self._service_name)
  310. scope.append('aws4_request')
  311. return '/'.join(scope)
  312. def string_to_sign(self, request, canonical_request):
  313. """
  314. Return the canonical StringToSign as well as a dict
  315. containing the original version of all headers that
  316. were included in the StringToSign.
  317. """
  318. sts = ['AWS4-HMAC-SHA256']
  319. sts.append(request.context['timestamp'])
  320. sts.append(self.credential_scope(request))
  321. sts.append(sha256(canonical_request.encode('utf-8')).hexdigest())
  322. return '\n'.join(sts)
  323. def signature(self, string_to_sign, request):
  324. key = self.credentials.secret_key
  325. k_date = self._sign(('AWS4' + key).encode('utf-8'),
  326. request.context['timestamp'][0:8])
  327. k_region = self._sign(k_date, self._region_name)
  328. k_service = self._sign(k_region, self._service_name)
  329. k_signing = self._sign(k_service, 'aws4_request')
  330. return self._sign(k_signing, string_to_sign, hex=True)
  331. def add_auth(self, request):
  332. if self.credentials is None:
  333. raise NoCredentialsError()
  334. datetime_now = datetime.datetime.utcnow()
  335. request.context['timestamp'] = datetime_now.strftime(SIGV4_TIMESTAMP)
  336. # This could be a retry. Make sure the previous
  337. # authorization header is removed first.
  338. self._modify_request_before_signing(request)
  339. canonical_request = self.canonical_request(request)
  340. logger.debug("Calculating signature using v4 auth.")
  341. logger.debug('CanonicalRequest:\n%s', canonical_request)
  342. string_to_sign = self.string_to_sign(request, canonical_request)
  343. logger.debug('StringToSign:\n%s', string_to_sign)
  344. signature = self.signature(string_to_sign, request)
  345. logger.debug('Signature:\n%s', signature)
  346. self._inject_signature_to_request(request, signature)
  347. def _inject_signature_to_request(self, request, signature):
  348. l = ['AWS4-HMAC-SHA256 Credential=%s' % self.scope(request)]
  349. headers_to_sign = self.headers_to_sign(request)
  350. l.append('SignedHeaders=%s' % self.signed_headers(headers_to_sign))
  351. l.append('Signature=%s' % signature)
  352. request.headers['Authorization'] = ', '.join(l)
  353. return request
  354. def _modify_request_before_signing(self, request):
  355. if 'Authorization' in request.headers:
  356. del request.headers['Authorization']
  357. self._set_necessary_date_headers(request)
  358. if self.credentials.token:
  359. if 'X-Amz-Security-Token' in request.headers:
  360. del request.headers['X-Amz-Security-Token']
  361. request.headers['X-Amz-Security-Token'] = self.credentials.token
  362. if not request.context.get('payload_signing_enabled', True):
  363. if 'X-Amz-Content-SHA256' in request.headers:
  364. del request.headers['X-Amz-Content-SHA256']
  365. request.headers['X-Amz-Content-SHA256'] = UNSIGNED_PAYLOAD
  366. def _set_necessary_date_headers(self, request):
  367. # The spec allows for either the Date _or_ the X-Amz-Date value to be
  368. # used so we check both. If there's a Date header, we use the date
  369. # header. Otherwise we use the X-Amz-Date header.
  370. if 'Date' in request.headers:
  371. del request.headers['Date']
  372. datetime_timestamp = datetime.datetime.strptime(
  373. request.context['timestamp'], SIGV4_TIMESTAMP)
  374. request.headers['Date'] = formatdate(
  375. int(calendar.timegm(datetime_timestamp.timetuple())))
  376. if 'X-Amz-Date' in request.headers:
  377. del request.headers['X-Amz-Date']
  378. else:
  379. if 'X-Amz-Date' in request.headers:
  380. del request.headers['X-Amz-Date']
  381. request.headers['X-Amz-Date'] = request.context['timestamp']
  382. class S3SigV4Auth(SigV4Auth):
  383. def _modify_request_before_signing(self, request):
  384. super(S3SigV4Auth, self)._modify_request_before_signing(request)
  385. if 'X-Amz-Content-SHA256' in request.headers:
  386. del request.headers['X-Amz-Content-SHA256']
  387. request.headers['X-Amz-Content-SHA256'] = self.payload(request)
  388. def _should_sha256_sign_payload(self, request):
  389. # S3 allows optional body signing, so to minimize the performance
  390. # impact, we opt to not SHA256 sign the body on streaming uploads,
  391. # provided that we're on https.
  392. client_config = request.context.get('client_config')
  393. s3_config = getattr(client_config, 's3', None)
  394. # The config could be None if it isn't set, or if the customer sets it
  395. # to None.
  396. if s3_config is None:
  397. s3_config = {}
  398. # The explicit configuration takes precedence over any implicit
  399. # configuration.
  400. sign_payload = s3_config.get('payload_signing_enabled', None)
  401. if sign_payload is not None:
  402. return sign_payload
  403. # We require that both content-md5 be present and https be enabled
  404. # to implicitly disable body signing. The combination of TLS and
  405. # content-md5 is sufficiently secure and durable for us to be
  406. # confident in the request without body signing.
  407. if not request.url.startswith('https') or \
  408. 'Content-MD5' not in request.headers:
  409. return True
  410. # If the input is streaming we disable body signing by default.
  411. if request.context.get('has_streaming_input', False):
  412. return False
  413. # If the S3-specific checks had no results, delegate to the generic
  414. # checks.
  415. return super(S3SigV4Auth, self)._should_sha256_sign_payload(request)
  416. def _normalize_url_path(self, path):
  417. # For S3, we do not normalize the path.
  418. return path
  419. class SigV4QueryAuth(SigV4Auth):
  420. DEFAULT_EXPIRES = 3600
  421. def __init__(self, credentials, service_name, region_name,
  422. expires=DEFAULT_EXPIRES):
  423. super(SigV4QueryAuth, self).__init__(credentials, service_name,
  424. region_name)
  425. self._expires = expires
  426. def _modify_request_before_signing(self, request):
  427. # We automatically set this header, so if it's the auto-set value we
  428. # want to get rid of it since it doesn't make sense for presigned urls.
  429. content_type = request.headers.get('content-type')
  430. blacklisted_content_type = (
  431. 'application/x-www-form-urlencoded; charset=utf-8'
  432. )
  433. if content_type == blacklisted_content_type:
  434. del request.headers['content-type']
  435. # Note that we're not including X-Amz-Signature.
  436. # From the docs: "The Canonical Query String must include all the query
  437. # parameters from the preceding table except for X-Amz-Signature.
  438. signed_headers = self.signed_headers(self.headers_to_sign(request))
  439. auth_params = {
  440. 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
  441. 'X-Amz-Credential': self.scope(request),
  442. 'X-Amz-Date': request.context['timestamp'],
  443. 'X-Amz-Expires': self._expires,
  444. 'X-Amz-SignedHeaders': signed_headers,
  445. }
  446. if self.credentials.token is not None:
  447. auth_params['X-Amz-Security-Token'] = self.credentials.token
  448. # Now parse the original query string to a dict, inject our new query
  449. # params, and serialize back to a query string.
  450. url_parts = urlsplit(request.url)
  451. # parse_qs makes each value a list, but in our case we know we won't
  452. # have repeated keys so we know we have single element lists which we
  453. # can convert back to scalar values.
  454. query_dict = dict(
  455. [(k, v[0]) for k, v in
  456. parse_qs(url_parts.query, keep_blank_values=True).items()])
  457. # The spec is particular about this. It *has* to be:
  458. # https://<endpoint>?<operation params>&<auth params>
  459. # You can't mix the two types of params together, i.e just keep doing
  460. # new_query_params.update(op_params)
  461. # new_query_params.update(auth_params)
  462. # percent_encode_sequence(new_query_params)
  463. operation_params = ''
  464. if request.data:
  465. # We also need to move the body params into the query string. To
  466. # do this, we first have to convert it to a dict.
  467. query_dict.update(_get_body_as_dict(request))
  468. request.data = ''
  469. if query_dict:
  470. operation_params = percent_encode_sequence(query_dict) + '&'
  471. new_query_string = (operation_params +
  472. percent_encode_sequence(auth_params))
  473. # url_parts is a tuple (and therefore immutable) so we need to create
  474. # a new url_parts with the new query string.
  475. # <part> - <index>
  476. # scheme - 0
  477. # netloc - 1
  478. # path - 2
  479. # query - 3 <-- we're replacing this.
  480. # fragment - 4
  481. p = url_parts
  482. new_url_parts = (p[0], p[1], p[2], new_query_string, p[4])
  483. request.url = urlunsplit(new_url_parts)
  484. def _inject_signature_to_request(self, request, signature):
  485. # Rather than calculating an "Authorization" header, for the query
  486. # param quth, we just append an 'X-Amz-Signature' param to the end
  487. # of the query string.
  488. request.url += '&X-Amz-Signature=%s' % signature
  489. class S3SigV4QueryAuth(SigV4QueryAuth):
  490. """S3 SigV4 auth using query parameters.
  491. This signer will sign a request using query parameters and signature
  492. version 4, i.e a "presigned url" signer.
  493. Based off of:
  494. http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
  495. """
  496. def _normalize_url_path(self, path):
  497. # For S3, we do not normalize the path.
  498. return path
  499. def payload(self, request):
  500. # From the doc link above:
  501. # "You don't include a payload hash in the Canonical Request, because
  502. # when you create a presigned URL, you don't know anything about the
  503. # payload. Instead, you use a constant string "UNSIGNED-PAYLOAD".
  504. return UNSIGNED_PAYLOAD
  505. class S3SigV4PostAuth(SigV4Auth):
  506. """
  507. Presigns a s3 post
  508. Implementation doc here:
  509. http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-UsingHTTPPOST.html
  510. """
  511. def add_auth(self, request):
  512. datetime_now = datetime.datetime.utcnow()
  513. request.context['timestamp'] = datetime_now.strftime(SIGV4_TIMESTAMP)
  514. fields = {}
  515. if request.context.get('s3-presign-post-fields', None) is not None:
  516. fields = request.context['s3-presign-post-fields']
  517. policy = {}
  518. conditions = []
  519. if request.context.get('s3-presign-post-policy', None) is not None:
  520. policy = request.context['s3-presign-post-policy']
  521. if policy.get('conditions', None) is not None:
  522. conditions = policy['conditions']
  523. policy['conditions'] = conditions
  524. fields['x-amz-algorithm'] = 'AWS4-HMAC-SHA256'
  525. fields['x-amz-credential'] = self.scope(request)
  526. fields['x-amz-date'] = request.context['timestamp']
  527. conditions.append({'x-amz-algorithm': 'AWS4-HMAC-SHA256'})
  528. conditions.append({'x-amz-credential': self.scope(request)})
  529. conditions.append({'x-amz-date': request.context['timestamp']})
  530. if self.credentials.token is not None:
  531. fields['x-amz-security-token'] = self.credentials.token
  532. conditions.append({'x-amz-security-token': self.credentials.token})
  533. # Dump the base64 encoded policy into the fields dictionary.
  534. fields['policy'] = base64.b64encode(
  535. json.dumps(policy).encode('utf-8')).decode('utf-8')
  536. fields['x-amz-signature'] = self.signature(fields['policy'], request)
  537. request.context['s3-presign-post-fields'] = fields
  538. request.context['s3-presign-post-policy'] = policy
  539. class HmacV1Auth(BaseSigner):
  540. # List of Query String Arguments of Interest
  541. QSAOfInterest = ['accelerate', 'acl', 'cors', 'defaultObjectAcl',
  542. 'location', 'logging', 'partNumber', 'policy',
  543. 'requestPayment', 'torrent',
  544. 'versioning', 'versionId', 'versions', 'website',
  545. 'uploads', 'uploadId', 'response-content-type',
  546. 'response-content-language', 'response-expires',
  547. 'response-cache-control', 'response-content-disposition',
  548. 'response-content-encoding', 'delete', 'lifecycle',
  549. 'tagging', 'restore', 'storageClass', 'notification',
  550. 'replication', 'requestPayment', 'analytics', 'metrics',
  551. 'inventory', 'select', 'select-type']
  552. def __init__(self, credentials, service_name=None, region_name=None):
  553. self.credentials = credentials
  554. def sign_string(self, string_to_sign):
  555. new_hmac = hmac.new(self.credentials.secret_key.encode('utf-8'),
  556. digestmod=sha1)
  557. new_hmac.update(string_to_sign.encode('utf-8'))
  558. return encodebytes(new_hmac.digest()).strip().decode('utf-8')
  559. def canonical_standard_headers(self, headers):
  560. interesting_headers = ['content-md5', 'content-type', 'date']
  561. hoi = []
  562. if 'Date' in headers:
  563. del headers['Date']
  564. headers['Date'] = self._get_date()
  565. for ih in interesting_headers:
  566. found = False
  567. for key in headers:
  568. lk = key.lower()
  569. if headers[key] is not None and lk == ih:
  570. hoi.append(headers[key].strip())
  571. found = True
  572. if not found:
  573. hoi.append('')
  574. return '\n'.join(hoi)
  575. def canonical_custom_headers(self, headers):
  576. hoi = []
  577. custom_headers = {}
  578. for key in headers:
  579. lk = key.lower()
  580. if headers[key] is not None:
  581. if lk.startswith('x-amz-'):
  582. custom_headers[lk] = ','.join(v.strip() for v in
  583. headers.get_all(key))
  584. sorted_header_keys = sorted(custom_headers.keys())
  585. for key in sorted_header_keys:
  586. hoi.append("%s:%s" % (key, custom_headers[key]))
  587. return '\n'.join(hoi)
  588. def unquote_v(self, nv):
  589. """
  590. TODO: Do we need this?
  591. """
  592. if len(nv) == 1:
  593. return nv
  594. else:
  595. return (nv[0], unquote(nv[1]))
  596. def canonical_resource(self, split, auth_path=None):
  597. # don't include anything after the first ? in the resource...
  598. # unless it is one of the QSA of interest, defined above
  599. # NOTE:
  600. # The path in the canonical resource should always be the
  601. # full path including the bucket name, even for virtual-hosting
  602. # style addressing. The ``auth_path`` keeps track of the full
  603. # path for the canonical resource and would be passed in if
  604. # the client was using virtual-hosting style.
  605. if auth_path is not None:
  606. buf = auth_path
  607. else:
  608. buf = split.path
  609. if split.query:
  610. qsa = split.query.split('&')
  611. qsa = [a.split('=', 1) for a in qsa]
  612. qsa = [self.unquote_v(a) for a in qsa
  613. if a[0] in self.QSAOfInterest]
  614. if len(qsa) > 0:
  615. qsa.sort(key=itemgetter(0))
  616. qsa = ['='.join(a) for a in qsa]
  617. buf += '?'
  618. buf += '&'.join(qsa)
  619. return buf
  620. def canonical_string(self, method, split, headers, expires=None,
  621. auth_path=None):
  622. cs = method.upper() + '\n'
  623. cs += self.canonical_standard_headers(headers) + '\n'
  624. custom_headers = self.canonical_custom_headers(headers)
  625. if custom_headers:
  626. cs += custom_headers + '\n'
  627. cs += self.canonical_resource(split, auth_path=auth_path)
  628. return cs
  629. def get_signature(self, method, split, headers, expires=None,
  630. auth_path=None):
  631. if self.credentials.token:
  632. del headers['x-amz-security-token']
  633. headers['x-amz-security-token'] = self.credentials.token
  634. string_to_sign = self.canonical_string(method,
  635. split,
  636. headers,
  637. auth_path=auth_path)
  638. logger.debug('StringToSign:\n%s', string_to_sign)
  639. return self.sign_string(string_to_sign)
  640. def add_auth(self, request):
  641. if self.credentials is None:
  642. raise NoCredentialsError
  643. logger.debug("Calculating signature using hmacv1 auth.")
  644. split = urlsplit(request.url)
  645. logger.debug('HTTP request method: %s', request.method)
  646. signature = self.get_signature(request.method, split,
  647. request.headers,
  648. auth_path=request.auth_path)
  649. self._inject_signature(request, signature)
  650. def _get_date(self):
  651. return formatdate(usegmt=True)
  652. def _inject_signature(self, request, signature):
  653. if 'Authorization' in request.headers:
  654. # We have to do this because request.headers is not
  655. # normal dictionary. It has the (unintuitive) behavior
  656. # of aggregating repeated setattr calls for the same
  657. # key value. For example:
  658. # headers['foo'] = 'a'; headers['foo'] = 'b'
  659. # list(headers) will print ['foo', 'foo'].
  660. del request.headers['Authorization']
  661. request.headers['Authorization'] = (
  662. "AWS %s:%s" % (self.credentials.access_key, signature))
  663. class HmacV1QueryAuth(HmacV1Auth):
  664. """
  665. Generates a presigned request for s3.
  666. Spec from this document:
  667. http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html
  668. #RESTAuthenticationQueryStringAuth
  669. """
  670. DEFAULT_EXPIRES = 3600
  671. def __init__(self, credentials, expires=DEFAULT_EXPIRES):
  672. self.credentials = credentials
  673. self._expires = expires
  674. def _get_date(self):
  675. return str(int(time.time() + int(self._expires)))
  676. def _inject_signature(self, request, signature):
  677. query_dict = {}
  678. query_dict['AWSAccessKeyId'] = self.credentials.access_key
  679. query_dict['Signature'] = signature
  680. for header_key in request.headers:
  681. lk = header_key.lower()
  682. # For query string requests, Expires is used instead of the
  683. # Date header.
  684. if header_key == 'Date':
  685. query_dict['Expires'] = request.headers['Date']
  686. # We only want to include relevant headers in the query string.
  687. # These can be anything that starts with x-amz, is Content-MD5,
  688. # or is Content-Type.
  689. elif lk.startswith('x-amz-') or lk in ['content-md5',
  690. 'content-type']:
  691. query_dict[lk] = request.headers[lk]
  692. # Combine all of the identified headers into an encoded
  693. # query string
  694. new_query_string = percent_encode_sequence(query_dict)
  695. # Create a new url with the presigned url.
  696. p = urlsplit(request.url)
  697. if p[3]:
  698. # If there was a pre-existing query string, we should
  699. # add that back before injecting the new query string.
  700. new_query_string = '%s&%s' % (p[3], new_query_string)
  701. new_url_parts = (p[0], p[1], p[2], new_query_string, p[4])
  702. request.url = urlunsplit(new_url_parts)
  703. class HmacV1PostAuth(HmacV1Auth):
  704. """
  705. Generates a presigned post for s3.
  706. Spec from this document:
  707. http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingHTTPPOST.html
  708. """
  709. def add_auth(self, request):
  710. fields = {}
  711. if request.context.get('s3-presign-post-fields', None) is not None:
  712. fields = request.context['s3-presign-post-fields']
  713. policy = {}
  714. conditions = []
  715. if request.context.get('s3-presign-post-policy', None) is not None:
  716. policy = request.context['s3-presign-post-policy']
  717. if policy.get('conditions', None) is not None:
  718. conditions = policy['conditions']
  719. policy['conditions'] = conditions
  720. fields['AWSAccessKeyId'] = self.credentials.access_key
  721. if self.credentials.token is not None:
  722. fields['x-amz-security-token'] = self.credentials.token
  723. conditions.append({'x-amz-security-token': self.credentials.token})
  724. # Dump the base64 encoded policy into the fields dictionary.
  725. fields['policy'] = base64.b64encode(
  726. json.dumps(policy).encode('utf-8')).decode('utf-8')
  727. fields['signature'] = self.sign_string(fields['policy'])
  728. request.context['s3-presign-post-fields'] = fields
  729. request.context['s3-presign-post-policy'] = policy
  730. AUTH_TYPE_MAPS = {
  731. 'v2': SigV2Auth,
  732. 'v3': SigV3Auth,
  733. 'v3https': SigV3Auth,
  734. 's3': HmacV1Auth,
  735. 's3-query': HmacV1QueryAuth,
  736. 's3-presign-post': HmacV1PostAuth,
  737. 's3v4-presign-post': S3SigV4PostAuth,
  738. }
  739. # Define v4 signers depending on if CRT is present
  740. if HAS_CRT:
  741. from botocore.crt.auth import CRT_AUTH_TYPE_MAPS
  742. AUTH_TYPE_MAPS.update(CRT_AUTH_TYPE_MAPS)
  743. else:
  744. AUTH_TYPE_MAPS.update({
  745. 'v4': SigV4Auth,
  746. 'v4-query': SigV4QueryAuth,
  747. 's3v4': S3SigV4Auth,
  748. 's3v4-query': S3SigV4QueryAuth,
  749. })