Working example of SignatureVersion 4 for AWS EC2 Signed-off-by: Tomaz Muraus <[email protected]>
Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/3caed8b0 Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/3caed8b0 Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/3caed8b0 Branch: refs/heads/trunk Commit: 3caed8b02d97383e70dd6b9314f98506dc653493 Parents: 659d64c Author: Gertjan Oude Lohuis <[email protected]> Authored: Mon Feb 2 15:00:57 2015 +0100 Committer: Tomaz Muraus <[email protected]> Committed: Fri Mar 6 15:58:12 2015 +0100 ---------------------------------------------------------------------- libcloud/common/aws.py | 74 ++++++++++++++++++++++++++++++++---- libcloud/compute/drivers/ec2.py | 11 ++++++ 2 files changed, 78 insertions(+), 7 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/3caed8b0/libcloud/common/aws.py ---------------------------------------------------------------------- diff --git a/libcloud/common/aws.py b/libcloud/common/aws.py index 1d3dfb4..9e5a19e 100644 --- a/libcloud/common/aws.py +++ b/libcloud/common/aws.py @@ -14,6 +14,8 @@ # limitations under the License. import base64 +from datetime import datetime +import hashlib import hmac import time from hashlib import sha256 @@ -133,16 +135,74 @@ class AWSTokenConnection(ConnectionUserAndKey): class SignedAWSConnection(AWSTokenConnection): def add_default_params(self, params): - params['SignatureVersion'] = '2' - params['SignatureMethod'] = 'HmacSHA256' - params['AWSAccessKeyId'] = self.user_id 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) + + if self.signature_version == 2: + params['SignatureVersion'] = '2' + params['SignatureMethod'] = 'HmacSHA256' + params['AWSAccessKeyId'] = self.user_id + params['Timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', + time.gmtime()) + params['Signature'] = self._get_aws_auth_param(params, self.key, + self.action) return params + def pre_connect_hook(self, params, headers): + if self.signature_version == 4: + 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) + + return params, headers + + def _get_authorization_v4_header(self, params, headers, dt): + # TODO: according to AWS spec (and RFC 2616 Section 4.2.) excess whitespace + # from inside non-quoted strings should be stripped. Now we only strip the + # start and end of the string. See http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html + canonical_headers = '\n'.join([':'.join([k.lower(), v.strip()]) + for k, v in sorted(headers.items())]) + canonical_headers += '\n' + + signed_headers = ';'.join([k.lower() for k in sorted(headers.keys())]) + + # For self.method == GET + request_params = '&'.join(["%s=%s" % (urlquote(k, safe=''), urlquote(v, safe='-_~')) + for k, v in sorted(params.items())]) + payload_hash = hashlib.sha256('').hexdigest() + + canonical_request = '\n'.join([self.method, + self.action, + request_params, + canonical_headers, + signed_headers, + payload_hash]) + + credential_scope = '/'.join([dt.strftime('%Y%m%d'), + self.driver.region_name, + self.service_name, + 'aws4_request']) + string_to_sign = '\n'.join(['AWS4-HMAC-SHA256', + dt.strftime('%Y%m%dT%H%M%SZ'), + credential_scope, + hashlib.sha256(canonical_request).hexdigest()]) + + # Key derivation functions. See: + # http://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-python + def getSignatureKey(key, date_stamp, regionName, serviceName): + def sign(key, msg): + return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest() + + signed_date = sign(('AWS4' + key).encode('utf-8'), date_stamp) + signed_region = sign(signed_date, regionName) + signed_service = sign(signed_region, serviceName) + return sign(signed_service, 'aws4_request') + + signing_key = getSignatureKey(self.key, dt.strftime('%Y%m%d'), self.driver.region_name, self.service_name) + signature = hmac.new(signing_key, string_to_sign.encode('utf-8'), hashlib.sha256).hexdigest() + + return 'AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s' % \ + (self.user_id, credential_scope, signed_headers, signature) + def _get_aws_auth_param(self, params, secret_key, path='/'): """ Creates the signature required for AWS, per http://git-wip-us.apache.org/repos/asf/libcloud/blob/3caed8b0/libcloud/compute/drivers/ec2.py ---------------------------------------------------------------------- diff --git a/libcloud/compute/drivers/ec2.py b/libcloud/compute/drivers/ec2.py index b32fcc7..d05e74f 100644 --- a/libcloud/compute/drivers/ec2.py +++ b/libcloud/compute/drivers/ec2.py @@ -1674,6 +1674,15 @@ class EC2Connection(SignedAWSConnection): version = API_VERSION host = REGION_DETAILS['us-east-1']['endpoint'] responseCls = EC2Response + signature_version = 2 + service_name = 'ec2' + + +class EC2V4Connection(EC2Connection): + """ + Represents a single connection to an EC2 Endpoint using signature version 4. + """ + signature_version = 4 class ExEC2AvailabilityZone(object): @@ -5541,6 +5550,8 @@ class EC2EUNodeDriver(EC2NodeDriver): """ Driver class for EC2 in the Western Europe Region. """ + + connectionCls = EC2V4Connection name = 'Amazon EC2 (eu-west-1)' _region = 'eu-west-1'
