auth.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  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. from email.utils import formatdate
  18. import logging
  19. from asn1crypto import keys, pem
  20. from ccscli.compat import json
  21. from ccscli.compat import OrderedDict
  22. from ccscli.compat import urlsplit
  23. from ccscli.exceptions import NoCredentialsError
  24. import rsa
  25. LOG = logging.getLogger('ccscli.auth')
  26. class BaseSigner(object):
  27. def add_auth(self, request):
  28. raise NotImplementedError("add_auth")
  29. class RSAv1Auth(BaseSigner):
  30. """
  31. RSA signing with a SHA-256 hash returning a base64 encoded signature.
  32. """
  33. AUTH_METHOD_NAME = 'rsav1'
  34. def __init__(self, credentials):
  35. self.credentials = credentials
  36. def _sign_string(self, string_to_sign):
  37. try:
  38. # We expect the private key to be the an PKCS8 pem formatted string.
  39. pem_bytes = self.credentials.private_key.encode('utf-8')
  40. if pem.detect(pem_bytes):
  41. _, _, der_bytes = pem.unarmor(pem_bytes)
  42. # In PKCS8 the key is wrapped in a container that describes it
  43. info = keys.PrivateKeyInfo.load(der_bytes, strict=True)
  44. # The unwrapped key is equivalent to pkcs1 contents
  45. key = rsa.PrivateKey.load_pkcs1(info.unwrap().dump(), 'DER')
  46. else:
  47. raise Exception('Not a PEM file')
  48. except:
  49. message = \
  50. "Failed to import private key from: '%s'. The private key is " \
  51. "corrupted or it is not in PKCS8 PEM format. The private key " \
  52. "was extracted either from 'env' (environment variables), " \
  53. "'shared-credentials-file' (a profile in the shared " \
  54. "credential file, by default under ~/.ccs/credentials), or " \
  55. "'auth-config-file' (a file containing the credentials whose " \
  56. "location was supplied on the command line.)" % \
  57. self.credentials.method
  58. LOG.debug(message, exc_info=True)
  59. raise Exception(message)
  60. # We sign the hash.
  61. signature = rsa.sign(string_to_sign.encode('utf-8'), key, 'SHA-256')
  62. return urlsafe_b64encode(signature).strip().decode('utf-8')
  63. def _canonical_standard_headers(self, headers):
  64. interesting_headers = ['content-type', 'x-ccs-date']
  65. hoi = []
  66. if 'x-ccs-date' in headers:
  67. raise Exception("x-ccs-date found in headers!")
  68. headers['x-ccs-date'] = self._get_date()
  69. for ih in interesting_headers:
  70. found = False
  71. for key in headers:
  72. lk = key.lower()
  73. if headers[key] is not None and lk == ih:
  74. hoi.append(headers[key].strip())
  75. found = True
  76. if not found:
  77. hoi.append('')
  78. return '\n'.join(hoi)
  79. def _canonical_string(self, method, split, headers):
  80. cs = method.upper() + '\n'
  81. cs += self._canonical_standard_headers(headers) + '\n'
  82. cs += split.path + '\n'
  83. cs += RSAv1Auth.AUTH_METHOD_NAME
  84. return cs
  85. def _get_signature(self, method, split, headers):
  86. string_to_sign = self._canonical_string(method, split, headers)
  87. LOG.debug('StringToSign:\n%s', string_to_sign)
  88. return self._sign_string(string_to_sign)
  89. def add_auth(self, request):
  90. if self.credentials is None:
  91. raise NoCredentialsError
  92. LOG.debug("Calculating signature using RSAv1Auth.")
  93. LOG.debug('HTTP request method: %s', request.method)
  94. split = urlsplit(request.url)
  95. signature = self._get_signature(request.method,
  96. split,
  97. request.headers)
  98. self._inject_signature(request, signature)
  99. def _get_date(self):
  100. return formatdate(usegmt=True)
  101. def _inject_signature(self, request, signature):
  102. if 'x-ccs-auth' in request.headers:
  103. raise Exception("x-ccs-auth found in headers!")
  104. request.headers['x-ccs-auth'] = self._get_signature_header(signature)
  105. def _get_signature_header(self, signature):
  106. auth_params = OrderedDict()
  107. auth_params['access_key_id'] = self.credentials.access_key_id
  108. auth_params['auth_method'] = RSAv1Auth.AUTH_METHOD_NAME
  109. encoded_auth_params = json.dumps(auth_params).encode('utf-8')
  110. return "%s.%s" % (
  111. urlsafe_b64encode(encoded_auth_params).strip().decode('utf-8'),
  112. signature)
  113. AUTH_TYPE_MAPS = {
  114. RSAv1Auth.AUTH_METHOD_NAME: RSAv1Auth,
  115. }