auth.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. # Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
  2. #
  3. # Modifications made by Cloudera are:
  4. # Copyright (c) 2016 Cloudera, Inc. All rights reserved.
  5. #
  6. # Licensed under the Apache License, Version 2.0 (the "License"). You
  7. # may not use this file except in compliance with the License. A copy of
  8. # the License is located at
  9. #
  10. # http://aws.amazon.com/apache2.0/
  11. #
  12. # or in the "license" file accompanying this file. This file is
  13. # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
  14. # ANY KIND, either express or implied. See the License for the specific
  15. # language governing permissions and limitations under the License.
  16. from base64 import urlsafe_b64encode
  17. try:
  18. from collections import OrderedDict
  19. except ImportError:
  20. from ordereddict import OrderedDict # Python 2.6
  21. from email.utils import formatdate
  22. import json
  23. import logging
  24. from urlparse import urlsplit
  25. from Crypto.Hash import SHA256
  26. from Crypto.PublicKey import RSA
  27. from Crypto.Signature import PKCS1_v1_5
  28. LOG = logging.getLogger('ccscli.auth')
  29. class BaseSigner(object):
  30. def add_auth(self, request):
  31. raise NotImplementedError("add_auth")
  32. class RSAv1Auth(BaseSigner):
  33. """
  34. RSA signing with a SHA-256 hash returning a base64 encoded signature.
  35. """
  36. AUTH_METHOD_NAME = 'rsav1'
  37. def __init__(self, credentials):
  38. self.credentials = credentials
  39. def sign_string(self, string_to_sign):
  40. try:
  41. # We expect the private key to be the an PKCS8 pem formatted string.
  42. key = RSA.importKey(self.credentials.private_key)
  43. except:
  44. message = \
  45. "Failed to import private key from: '%s'. The private key is " \
  46. "corrupted or it is not in PKCS8 PEM format. The private key " \
  47. "was extracted either from 'env' (environment variables), " \
  48. "'shared-credentials-file' (a profile in the shared " \
  49. "credential file, by default under ~/.ccs/credentials), or " \
  50. "'auth-config-file' (a file containing the credentials whose " \
  51. "location was supplied on the command line.)" % \
  52. self.credentials.method
  53. LOG.debug(message, exc_info=True)
  54. raise Exception(message)
  55. # We sign the hash.
  56. h = SHA256.new(string_to_sign.encode('utf-8'))
  57. signer = PKCS1_v1_5.new(key)
  58. return urlsafe_b64encode(signer.sign(h)).strip().decode('utf-8')
  59. def canonical_standard_headers(self, headers):
  60. interesting_headers = ['content-type', 'x-ccs-date']
  61. hoi = []
  62. if 'x-ccs-date' in headers:
  63. raise Exception("x-ccs-date found in headers!")
  64. headers['x-ccs-date'] = self._get_date()
  65. for ih in interesting_headers:
  66. found = False
  67. for key in headers:
  68. lk = key.lower()
  69. if headers[key] is not None and lk == ih:
  70. hoi.append(headers[key].strip())
  71. found = True
  72. if not found:
  73. hoi.append('')
  74. return '\n'.join(hoi)
  75. def canonical_string(self, method, split, headers):
  76. cs = method.upper() + '\n'
  77. cs += self.canonical_standard_headers(headers) + '\n'
  78. cs += split.path + '\n'
  79. cs += RSAv1Auth.AUTH_METHOD_NAME
  80. return cs
  81. def get_signature(self, method, split, headers):
  82. string_to_sign = self.canonical_string(method, split, headers)
  83. LOG.debug('StringToSign:\n%s', string_to_sign)
  84. return self.sign_string(string_to_sign)
  85. def add_auth(self, request):
  86. if self.credentials is None:
  87. return
  88. LOG.debug("Calculating signature using RSAv1Auth.")
  89. LOG.debug('HTTP request method: %s', request.method)
  90. split = urlsplit(request.url)
  91. signature = self.get_signature(request.method,
  92. split,
  93. request.headers)
  94. self._inject_signature(request, signature)
  95. def _get_date(self):
  96. return formatdate(usegmt=True)
  97. def _inject_signature(self, request, signature):
  98. if 'x-ccs-auth' in request.headers:
  99. raise Exception("x-ccs-auth found in headers!")
  100. request.headers['x-ccs-auth'] = self._get_signature_header(signature)
  101. def _get_signature_header(self, signature):
  102. auth_params = OrderedDict()
  103. auth_params['access_key_id'] = self.credentials.access_key_id
  104. auth_params['auth_method'] = RSAv1Auth.AUTH_METHOD_NAME
  105. encoded_auth_params = json.dumps(auth_params).encode('utf-8')
  106. return "%s.%s" % (
  107. urlsafe_b64encode(encoded_auth_params).strip().decode('utf-8'),
  108. signature)
  109. AUTH_TYPE_MAPS = {
  110. RSAv1Auth.AUTH_METHOD_NAME: RSAv1Auth,
  111. }