Fix aws request signing so it also works with the "region" argument.
Move away from class per version approach to "signature_version" argument. Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/a84f5782 Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/a84f5782 Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/a84f5782 Branch: refs/heads/trunk Commit: a84f57826fdf73c34b8ef6e98738f971dcc9a912 Parents: 882fdf8 Author: Tomaz Muraus <[email protected]> Authored: Sat Mar 7 16:36:32 2015 +0100 Committer: Tomaz Muraus <[email protected]> Committed: Sat Mar 7 16:54:53 2015 +0100 ---------------------------------------------------------------------- libcloud/common/aws.py | 171 +++++++++++++++++++++++++++-------- libcloud/compute/drivers/ec2.py | 19 ++++ 2 files changed, 154 insertions(+), 36 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/a84f5782/libcloud/common/aws.py ---------------------------------------------------------------------- diff --git a/libcloud/common/aws.py b/libcloud/common/aws.py index f1b9a11..513e821 100644 --- a/libcloud/common/aws.py +++ b/libcloud/common/aws.py @@ -30,6 +30,21 @@ from libcloud.common.types import InvalidCredsError, MalformedResponseError from libcloud.utils.py3 import b, httplib, urlquote from libcloud.utils.xml import findtext, findall +__all__ = [ + 'AWSBaseResponse', + 'AWSGenericResponse', + + 'AWSTokenConnection', + 'SignedAWSConnection', + + 'AWSRequestSignerAlgorithmV2', + 'AWSRequestSignerAlgorithmV4', + + 'AWSDriver' +] + +DEFAULT_SIGNATURE_VERSION = '2' + class AWSBaseResponse(XmlResponse): namespace = None @@ -132,17 +147,49 @@ class AWSTokenConnection(ConnectionUserAndKey): return super(AWSTokenConnection, self).add_default_headers(headers) -class SignedAWSConnection(AWSTokenConnection): +class AWSRequestSigner(object): + """ + Class which handles signing the outgoing AWS requests. + """ - def add_default_params(self, params): + def __init__(self, access_key, access_secret, version, connection): + """ + :param access_key: Access key. + :type access_key: ``str`` + + :param access_secret: Access secret. + :type access_secret: ``str`` + + :param version: API version. + :type version: ``str`` + + :param connection: Connection instance. + :type connection: :class:`Connection` + """ + self.access_key = access_key + self.access_secret = access_secret + self.version = version + # TODO: Remove cycling dependency between connection and signer + self.connection = connection + + def get_request_params(self, params, method='GET', path='/'): + return params + + def get_request_headers(self, params, headers, method='GET', path='/'): + return params, headers + + +class AWSRequestSignerAlgorithmV2(AWSRequestSigner): + def get_request_params(self, params, method='GET', path='/'): params['SignatureVersion'] = '2' params['SignatureMethod'] = 'HmacSHA256' - params['AWSAccessKeyId'] = self.user_id + params['AWSAccessKeyId'] = self.access_key params['Version'] = self.version params['Timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) - params['Signature'] = self._get_aws_auth_param(params, self.key, - self.action) + params['Signature'] = self._get_aws_auth_param(params=params, + secret_key=self.access_secret, + path=path) return params def _get_aws_auth_param(self, params, secret_key, path='/'): @@ -155,6 +202,8 @@ class SignedAWSConnection(AWSTokenConnection): HTTPRequestURI + "\n" + CanonicalizedQueryString <from the preceding step> """ + connection = self.connection + keys = list(params.keys()) keys.sort() pairs = [] @@ -165,10 +214,10 @@ class SignedAWSConnection(AWSTokenConnection): qs = '&'.join(pairs) - hostname = self.host - if (self.secure and self.port != 443) or \ - (not self.secure and self.port != 80): - hostname += ":" + str(self.port) + hostname = connection.host + if (connection.secure and connection.port != 443) or \ + (not connection.secure and connection.port != 80): + hostname += ':' + str(connection.port) string_to_sign = '\n'.join(('GET', hostname, path, qs)) @@ -180,57 +229,70 @@ class SignedAWSConnection(AWSTokenConnection): return b64_hmac.decode('utf-8') -class V4SignedAWSConnection(AWSTokenConnection): - - def add_default_params(self, params): +class AWSRequestSignerAlgorithmV4(AWSRequestSigner): + def get_request_params(self, params, method='GET', path='/'): params['Version'] = self.version return params - def pre_connect_hook(self, params, headers): + def get_request_headers(self, params, headers, method='GET', path='/'): now = datetime.utcnow() headers['X-AMZ-Date'] = now.strftime('%Y%m%dT%H%M%SZ') headers['Authorization'] = \ - self._get_authorization_v4_header(params, headers, now) + self._get_authorization_v4_header(params=params, headers=headers, + dt=now, method=method, path=path) return params, headers - def _get_authorization_v4_header(self, params, headers, dt): - assert self.method == 'GET', 'AWS Signature V4 not implemented for ' \ - 'other methods than GET' + def _get_authorization_v4_header(self, params, headers, dt, method='GET', + path='/'): + assert method == 'GET', 'AWS Signature V4 not implemented for ' \ + 'other methods than GET' + + credentials_scope = self._get_credential_scope(dt=dt) + signed_headers = self._get_signed_headers(headers=headers) + signature = self._get_signature(params=params, headers=headers, + dt=dt, method=method, path=path) return 'AWS4-HMAC-SHA256 Credential=%(u)s/%(c)s, ' \ 'SignedHeaders=%(sh)s, Signature=%(s)s' % { - 'u': self.user_id, - 'c': self._get_credential_scope(dt), - 'sh': self._get_signed_headers(headers), - 's': self._get_signature(params, headers, dt) + 'u': self.access_key, + 'c': credentials_scope, + 'sh': signed_headers, + 's': signature } - def _get_signature(self, params, headers, dt): - return _sign( - self._get_key_to_sign_with(dt), - self._get_string_to_sign(params, headers, dt), - hex=True) + def _get_signature(self, params, headers, dt, method, path): + key = self._get_key_to_sign_with(dt) + string_to_sign = self._get_string_to_sign(params=params, + headers=headers, dt=dt, + method=method, path=path) + return _sign(key=key, msg=string_to_sign, hex=True) def _get_key_to_sign_with(self, dt): return _sign( _sign( _sign( - _sign(('AWS4' + self.key), dt.strftime('%Y%m%d')), - self.driver.region_name), - self.service_name), + _sign(('AWS4' + self.access_secret), + dt.strftime('%Y%m%d')), + self.connection.driver.region_name), + self.connection.service_name), 'aws4_request') - def _get_string_to_sign(self, params, headers, dt): + def _get_string_to_sign(self, params, headers, dt, method, path): + canonical_request = self._get_canonical_request(params=params, + headers=headers, + method=method, + path=path) + return '\n'.join(['AWS4-HMAC-SHA256', dt.strftime('%Y%m%dT%H%M%SZ'), self._get_credential_scope(dt), - _hash(self._get_canonical_request(params, headers))]) + _hash(canonical_request)]) def _get_credential_scope(self, dt): return '/'.join([dt.strftime('%Y%m%d'), - self.driver.region_name, - self.service_name, + self.connection.driver.region_name, + self.connection.service_name, 'aws4_request']) def _get_signed_headers(self, headers): @@ -249,10 +311,10 @@ class V4SignedAWSConnection(AWSTokenConnection): (urlquote(k, safe=''), urlquote(str(v), safe='~')) for k, v in sorted(params.items())]) - def _get_canonical_request(self, params, headers): + def _get_canonical_request(self, params, headers, method, path): return '\n'.join([ - self.method, - self.action, + method, + path, self._get_request_params(params), self._get_canonical_headers(headers), self._get_signed_headers(headers), @@ -260,6 +322,43 @@ class V4SignedAWSConnection(AWSTokenConnection): ]) +class SignedAWSConnection(AWSTokenConnection): + def __init__(self, user_id, key, secure=True, host=None, port=None, + url=None, timeout=None, token=None, + signature_version=DEFAULT_SIGNATURE_VERSION): + super(SignedAWSConnection, self).__init__(user_id=user_id, key=key, + secure=secure, host=host, + port=port, url=url, + timeout=timeout, token=token) + self.signature_version = str(signature_version) + + if self.signature_version == '2': + signer_cls = AWSRequestSignerAlgorithmV2 + elif signature_version == '4': + signer_cls = AWSRequestSignerAlgorithmV4 + else: + raise ValueError('Unsupported signature_version: %s' % + (signature_version)) + + self.signer = signer_cls(access_key=self.user_id, + access_secret=self.key, + version=self.version, + connection=self) + + def add_default_params(self, params): + params = self.signer.get_request_params(params=params, + method=self.method, + path=self.action) + return params + + def pre_connect_hook(self, params, headers): + params, headers = self.signer.get_request_headers(params=params, + headers=headers, + method=self.method, + path=self.action) + return params, headers + + def _sign(key, msg, hex=False): if hex: return hmac.new(b(key), b(msg), hashlib.sha256).hexdigest() http://git-wip-us.apache.org/repos/asf/libcloud/blob/a84f5782/libcloud/compute/drivers/ec2.py ---------------------------------------------------------------------- diff --git a/libcloud/compute/drivers/ec2.py b/libcloud/compute/drivers/ec2.py index 03c817f..8f64705 100644 --- a/libcloud/compute/drivers/ec2.py +++ b/libcloud/compute/drivers/ec2.py @@ -36,6 +36,7 @@ from libcloud.utils.publickey import get_pubkey_comment from libcloud.utils.iso8601 import parse_date from libcloud.common.aws import (AWSBaseResponse, SignedAWSConnection, V4SignedAWSConnection) +from libcloud.common.aws import DEFAULT_SIGNATURE_VERSION from libcloud.common.types import (InvalidCredsError, MalformedResponseError, LibcloudError) from libcloud.compute.providers import Provider @@ -377,6 +378,7 @@ REGION_DETAILS = { 'endpoint': 'ec2.us-east-1.amazonaws.com', 'api_name': 'ec2_us_east', 'country': 'USA', + 'signature_version': '2', 'instance_types': [ 't1.micro', 'm1.small', @@ -421,6 +423,7 @@ REGION_DETAILS = { 'endpoint': 'ec2.us-west-1.amazonaws.com', 'api_name': 'ec2_us_west', 'country': 'USA', + 'signature_version': '2', 'instance_types': [ 't1.micro', 'm1.small', @@ -461,6 +464,7 @@ REGION_DETAILS = { 'endpoint': 'ec2.us-west-2.amazonaws.com', 'api_name': 'ec2_us_west_oregon', 'country': 'US', + 'signature_version': '2', 'instance_types': [ 't1.micro', 'm1.small', @@ -503,6 +507,7 @@ REGION_DETAILS = { 'endpoint': 'ec2.eu-west-1.amazonaws.com', 'api_name': 'ec2_eu_west', 'country': 'Ireland', + 'signature_version': '2', 'instance_types': [ 't1.micro', 'm1.small', @@ -545,6 +550,7 @@ REGION_DETAILS = { 'endpoint': 'ec2.eu-central-1.amazonaws.com', 'api_name': 'ec2_eu_central', 'country': 'Frankfurt', + 'signature_version': '4', 'instance_types': [ 'm3.medium', 'm3.large', @@ -574,6 +580,7 @@ REGION_DETAILS = { 'endpoint': 'ec2.ap-southeast-1.amazonaws.com', 'api_name': 'ec2_ap_southeast', 'country': 'Singapore', + 'signature_version': '2', 'instance_types': [ 't1.micro', 'm1.small', @@ -609,6 +616,7 @@ REGION_DETAILS = { 'endpoint': 'ec2.ap-northeast-1.amazonaws.com', 'api_name': 'ec2_ap_northeast', 'country': 'Japan', + 'signature_version': '2', 'instance_types': [ 't1.micro', 'm1.small', @@ -650,6 +658,7 @@ REGION_DETAILS = { 'endpoint': 'ec2.sa-east-1.amazonaws.com', 'api_name': 'ec2_sa_east', 'country': 'Brazil', + 'signature_version': '2', 'instance_types': [ 't1.micro', 'm1.small', @@ -675,6 +684,7 @@ REGION_DETAILS = { 'endpoint': 'ec2.ap-southeast-2.amazonaws.com', 'api_name': 'ec2_ap_southeast_2', 'country': 'Australia', + 'signature_version': '2', 'instance_types': [ 't1.micro', 'm1.small', @@ -714,6 +724,7 @@ REGION_DETAILS = { 'endpoint': 'ec2.us-gov-west-1.amazonaws.com', 'api_name': 'ec2_us_govwest', 'country': 'US', + 'signature_version': '2', 'instance_types': [ 't1.micro', 'm1.small', @@ -755,6 +766,7 @@ REGION_DETAILS = { # Nimbus clouds have 3 EC2-style instance types but their particular # RAM allocations are configured by the admin 'country': 'custom', + 'signature_version': '2', 'instance_types': [ 'm1.small', 'm1.large', @@ -4636,6 +4648,11 @@ class BaseEC2NodeDriver(NodeDriver): return self._get_boolean(res) + def _ex_connection_class_kwargs(self): + kwargs = super(BaseEC2NodeDriver, self)._ex_connection_class_kwargs() + kwargs['signature_version'] = self.signature_version + return kwargs + def _to_nodes(self, object, xpath): return [self._to_node(el) for el in object.findall(fixxpath(xpath=xpath, @@ -5560,6 +5577,8 @@ class EC2NodeDriver(BaseEC2NodeDriver): self.region_name = region self.api_name = details['api_name'] self.country = details['country'] + self.signature_version = details.pop('signature_version', + DEFAULT_SIGNATURE_VERSION) host = host or details['endpoint']
