From fafbb97aa1d579a077611889e08f63822fa384df Mon Sep 17 00:00:00 2001 From: Steven Hardy <shardy@redhat.com> Date: Fri, 26 Jul 2013 11:42:29 +0100 Subject: [PATCH] Ec2Signer : Allow signature verification for older boto versions Since the fix for bug #1197553, verification for older clients (which strip the port when formatting the request) fails. This conditionally reverts to the original behavior, by detecting the boto version via the User-Agent header, the default behavior will be the new behavior (which doesn't strip the port), but this will allow a less painful transition for clients/distros to the new boto version. Fixes bug #1205281 Change-Id: I54ac9c5ba91e697004f1346a8f2d685da488992a --- keystoneclient/contrib/ec2/utils.py | 18 ++++-- tests/test_ec2utils.py | 113 ++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 4 deletions(-) diff --git a/keystoneclient/contrib/ec2/utils.py b/keystoneclient/contrib/ec2/utils.py index ca5afa7..2839654 100644 --- a/keystoneclient/contrib/ec2/utils.py +++ b/keystoneclient/contrib/ec2/utils.py @@ -21,6 +21,7 @@ import base64 import hashlib import hmac +import re import urllib @@ -211,17 +212,26 @@ class Ec2Signer(object): # - the X-Amz-SignedHeaders query parameter headers_lower = dict((k.lower().strip(), v.strip()) for (k, v) in headers.iteritems()) + + # Boto versions < 2.9.3 strip the port component of the host:port + # header, so detect the user-agent via the header and strip the + # port if we detect an old boto version. FIXME: remove when all + # distros package boto >= 2.9.3, this is a transitional workaround + user_agent = headers_lower.get('user-agent', '') + strip_port = re.match('Boto/2.[0-9].[0-2]', user_agent) + header_list = [] sh_str = auth_param('SignedHeaders') for h in sh_str.split(';'): if h not in headers_lower: continue - if h == 'host': - # Note we discard any port suffix + + if h == 'host' and strip_port: header_list.append('%s:%s' % (h, headers_lower[h].split(':')[0])) - else: - header_list.append('%s:%s' % (h, headers_lower[h])) + continue + + header_list.append('%s:%s' % (h, headers_lower[h])) return '\n'.join(header_list) + '\n' # Create canonical request: diff --git a/tests/test_ec2utils.py b/tests/test_ec2utils.py index a3c36fa..c5edfcc 100644 --- a/tests/test_ec2utils.py +++ b/tests/test_ec2utils.py @@ -143,3 +143,116 @@ class Ec2SignerTest(testtools.TestCase): expected = ('ced6826de92d2bdeed8f846f0bf508e8' '559e98e4b0199114b84c54174deb456c') self.assertEqual(signature, expected) + + def test_generate_v4_port(self): + """ + Test v4 generator with host:port format + """ + # Create a new signer object with the AWS example key + secret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' + signer = Ec2Signer(secret) + + body_hash = ('b6359072c78d70ebee1e81adcbab4f0' + '1bf2c23245fa365ef83fe8f1f955085e2') + auth_str = ('AWS4-HMAC-SHA256 ' + 'Credential=AKIAIOSFODNN7EXAMPLE/20110909/' + 'us-east-1/iam/aws4_request,' + 'SignedHeaders=content-type;host;x-amz-date,') + headers = {'Content-type': + 'application/x-www-form-urlencoded; charset=utf-8', + 'X-Amz-Date': '20110909T233600Z', + 'Host': 'foo:8000', + 'Authorization': auth_str} + # Note the example in the AWS docs is inconsistent, previous + # examples specify no query string, but the final POST example + # does, apparently incorrectly since an empty parameter list + # aligns all steps and the final signature with the examples + params = {} + credentials = {'host': 'foo:8000', + 'verb': 'POST', + 'path': '/', + 'params': params, + 'headers': headers, + 'body_hash': body_hash} + signature = signer.generate(credentials) + + expected = ('26dd92ea79aaa49f533d13b1055acdc' + 'd7d7321460d64621f96cc79c4f4d4ab2b') + self.assertEqual(signature, expected) + + def test_generate_v4_port_strip(self): + """ + Test v4 generator with host:port format, but for an old + (<2.9.3) version of boto, where the port should be stripped + to match boto behavior + """ + # Create a new signer object with the AWS example key + secret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' + signer = Ec2Signer(secret) + + body_hash = ('b6359072c78d70ebee1e81adcbab4f0' + '1bf2c23245fa365ef83fe8f1f955085e2') + auth_str = ('AWS4-HMAC-SHA256 ' + 'Credential=AKIAIOSFODNN7EXAMPLE/20110909/' + 'us-east-1/iam/aws4_request,' + 'SignedHeaders=content-type;host;x-amz-date,') + headers = {'Content-type': + 'application/x-www-form-urlencoded; charset=utf-8', + 'X-Amz-Date': '20110909T233600Z', + 'Host': 'foo:8000', + 'Authorization': auth_str, + 'User-Agent': 'Boto/2.9.2 (linux2)'} + # Note the example in the AWS docs is inconsistent, previous + # examples specify no query string, but the final POST example + # does, apparently incorrectly since an empty parameter list + # aligns all steps and the final signature with the examples + params = {} + credentials = {'host': 'foo:8000', + 'verb': 'POST', + 'path': '/', + 'params': params, + 'headers': headers, + 'body_hash': body_hash} + signature = signer.generate(credentials) + + expected = ('9a4b2276a5039ada3b90f72ea8ec1745' + '14b92b909fb106b22ad910c5d75a54f4') + self.assertEqual(expected, signature) + + def test_generate_v4_port_nostrip(self): + """ + Test v4 generator with host:port format, but for an new + (>=2.9.3) version of boto, where the port should not be stripped + """ + # Create a new signer object with the AWS example key + secret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' + signer = Ec2Signer(secret) + + body_hash = ('b6359072c78d70ebee1e81adcbab4f0' + '1bf2c23245fa365ef83fe8f1f955085e2') + auth_str = ('AWS4-HMAC-SHA256 ' + 'Credential=AKIAIOSFODNN7EXAMPLE/20110909/' + 'us-east-1/iam/aws4_request,' + 'SignedHeaders=content-type;host;x-amz-date,') + headers = {'Content-type': + 'application/x-www-form-urlencoded; charset=utf-8', + 'X-Amz-Date': '20110909T233600Z', + 'Host': 'foo:8000', + 'Authorization': auth_str, + 'User-Agent': 'Boto/2.9.3 (linux2)'} + # Note the example in the AWS docs is inconsistent, previous + # examples specify no query string, but the final POST example + # does, apparently incorrectly since an empty parameter list + # aligns all steps and the final signature with the examples + params = {} + credentials = {'host': 'foo:8000', + 'verb': 'POST', + 'path': '/', + 'params': params, + 'headers': headers, + 'body_hash': body_hash} + signature = signer.generate(credentials) + + expected = ('26dd92ea79aaa49f533d13b1055acdc' + 'd7d7321460d64621f96cc79c4f4d4ab2b') + self.assertEqual(expected, signature)