Add check_signature function to swift3.auth_details
This allows (some) auth middlewares to stop needing to know the details of S3 request signing. The function takes the user's secret and returns True if the signatures match, False otherwise. This offers an easy way for auth middlewares that have user secrets in-memory (such as swauth and tempauth) to add support for v4 signatures. Change-Id: Iafb6114c12deb9a40d0f8324611de27b48ed95f6
This commit is contained in:
parent
1069659fbe
commit
c2f1d012d5
@ -13,8 +13,10 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import base64
|
||||||
from email.header import Header
|
from email.header import Header
|
||||||
from hashlib import sha256, md5
|
from hashlib import sha1, sha256, md5
|
||||||
|
import hmac
|
||||||
import re
|
import re
|
||||||
import six
|
import six
|
||||||
import string
|
import string
|
||||||
@ -70,6 +72,7 @@ ALLOWED_SUB_RESOURCES = sorted([
|
|||||||
MAX_32BIT_INT = 2147483647
|
MAX_32BIT_INT = 2147483647
|
||||||
SIGV2_TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S'
|
SIGV2_TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S'
|
||||||
SIGV4_X_AMZ_DATE_FORMAT = '%Y%m%dT%H%M%SZ'
|
SIGV4_X_AMZ_DATE_FORMAT = '%Y%m%dT%H%M%SZ'
|
||||||
|
SERVICE = 's3' # useful for mocking out in tests
|
||||||
|
|
||||||
|
|
||||||
def _header_strip(value):
|
def _header_strip(value):
|
||||||
@ -108,6 +111,16 @@ class SigV4Mixin(object):
|
|||||||
A request class mixin to provide S3 signature v4 functionality
|
A request class mixin to provide S3 signature v4 functionality
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def check_signature(self, secret):
|
||||||
|
user_signature = self.signature
|
||||||
|
derived_secret = 'AWS4' + secret
|
||||||
|
for scope_piece in self.scope:
|
||||||
|
derived_secret = hmac.new(
|
||||||
|
derived_secret, scope_piece, sha256).digest()
|
||||||
|
valid_signature = hmac.new(
|
||||||
|
derived_secret, self.string_to_sign, sha256).hexdigest()
|
||||||
|
return user_signature == valid_signature
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _is_query_auth(self):
|
def _is_query_auth(self):
|
||||||
return 'X-Amz-Credential' in self.params
|
return 'X-Amz-Credential' in self.params
|
||||||
@ -336,17 +349,19 @@ class SigV4Mixin(object):
|
|||||||
cr.append(hashed_payload)
|
cr.append(hashed_payload)
|
||||||
return '\n'.join(cr).encode('utf-8')
|
return '\n'.join(cr).encode('utf-8')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scope(self):
|
||||||
|
return [self.timestamp.amz_date_format.split('T')[0],
|
||||||
|
CONF.location, SERVICE, 'aws4_request']
|
||||||
|
|
||||||
def _string_to_sign(self):
|
def _string_to_sign(self):
|
||||||
"""
|
"""
|
||||||
Create 'StringToSign' value in Amazon terminology for v4.
|
Create 'StringToSign' value in Amazon terminology for v4.
|
||||||
"""
|
"""
|
||||||
scope = (self.timestamp.amz_date_format.split('T')[0] +
|
return '\n'.join(['AWS4-HMAC-SHA256',
|
||||||
'/' + CONF.location + '/s3/aws4_request')
|
self.timestamp.amz_date_format,
|
||||||
|
'/'.join(self.scope),
|
||||||
return ('AWS4-HMAC-SHA256' + '\n'
|
sha256(self._canonical_request()).hexdigest()])
|
||||||
+ self.timestamp.amz_date_format + '\n'
|
|
||||||
+ scope + '\n'
|
|
||||||
+ sha256(self._canonical_request()).hexdigest())
|
|
||||||
|
|
||||||
|
|
||||||
def get_request_class(env):
|
def get_request_class(env):
|
||||||
@ -381,14 +396,17 @@ class Request(swob.Request):
|
|||||||
# NOTE: app is not used by this class, need for compatibility of S3acl
|
# NOTE: app is not used by this class, need for compatibility of S3acl
|
||||||
swob.Request.__init__(self, env)
|
swob.Request.__init__(self, env)
|
||||||
self._timestamp = None
|
self._timestamp = None
|
||||||
self.access_key, signature = self._parse_auth_info()
|
self.access_key, self.signature = self._parse_auth_info()
|
||||||
self.bucket_in_host = self._parse_host()
|
self.bucket_in_host = self._parse_host()
|
||||||
self.container_name, self.object_name = self._parse_uri()
|
self.container_name, self.object_name = self._parse_uri()
|
||||||
self._validate_headers()
|
self._validate_headers()
|
||||||
|
# Lock in string-to-sign now, before we start messing with query params
|
||||||
|
self.string_to_sign = self._string_to_sign()
|
||||||
self.environ['swift3.auth_details'] = {
|
self.environ['swift3.auth_details'] = {
|
||||||
'access_key': self.access_key,
|
'access_key': self.access_key,
|
||||||
'signature': signature,
|
'signature': self.signature,
|
||||||
'string_to_sign': self._string_to_sign(),
|
'string_to_sign': self.string_to_sign,
|
||||||
|
'check_signature': self.check_signature,
|
||||||
}
|
}
|
||||||
self.token = None
|
self.token = None
|
||||||
self.account = None
|
self.account = None
|
||||||
@ -403,11 +421,17 @@ class Request(swob.Request):
|
|||||||
# b626a3ca86e467fc7564eac236b9ee2efd49bdcc, the s3token is in swift3
|
# b626a3ca86e467fc7564eac236b9ee2efd49bdcc, the s3token is in swift3
|
||||||
# repo so probably we need to change s3token to support v4 format.
|
# repo so probably we need to change s3token to support v4 format.
|
||||||
self.headers['Authorization'] = 'AWS %s:%s' % (
|
self.headers['Authorization'] = 'AWS %s:%s' % (
|
||||||
self.access_key, signature)
|
self.access_key, self.signature)
|
||||||
# Avoids that swift.swob.Response replaces Location header value
|
# Avoids that swift.swob.Response replaces Location header value
|
||||||
# by full URL when absolute path given. See swift.swob for more detail.
|
# by full URL when absolute path given. See swift.swob for more detail.
|
||||||
self.environ['swift.leave_relative_location'] = True
|
self.environ['swift.leave_relative_location'] = True
|
||||||
|
|
||||||
|
def check_signature(self, secret):
|
||||||
|
user_signature = self.signature
|
||||||
|
valid_signature = base64.b64encode(hmac.new(
|
||||||
|
secret, self.string_to_sign, sha1).digest()).strip()
|
||||||
|
return user_signature == valid_signature
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timestamp(self):
|
def timestamp(self):
|
||||||
"""
|
"""
|
||||||
|
@ -18,6 +18,7 @@ from mock import patch, MagicMock
|
|||||||
from contextlib import nested
|
from contextlib import nested
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import mock
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
import copy
|
import copy
|
||||||
@ -381,14 +382,16 @@ class TestSwift3Middleware(Swift3TestCase):
|
|||||||
req.headers['Authorization'] = 'AWS test:tester:hmac'
|
req.headers['Authorization'] = 'AWS test:tester:hmac'
|
||||||
date_header = self.get_date_header()
|
date_header = self.get_date_header()
|
||||||
req.headers['Date'] = date_header
|
req.headers['Date'] = date_header
|
||||||
status, headers, body = self.call_swift3(req)
|
with mock.patch('swift3.request.Request.check_signature') as mock_cs:
|
||||||
|
status, headers, body = self.call_swift3(req)
|
||||||
_, _, headers = self.swift.calls_with_headers[-1]
|
_, _, headers = self.swift.calls_with_headers[-1]
|
||||||
self.assertEqual(req.environ['swift3.auth_details'], {
|
self.assertEqual(req.environ['swift3.auth_details'], {
|
||||||
'access_key': 'test:tester',
|
'access_key': 'test:tester',
|
||||||
'signature': 'hmac',
|
'signature': 'hmac',
|
||||||
'string_to_sign': '\n'.join([
|
'string_to_sign': '\n'.join([
|
||||||
'PUT', '', '', date_header,
|
'PUT', '', '', date_header,
|
||||||
'/bucket/object?partNumber=1&uploadId=123456789abcdef'])})
|
'/bucket/object?partNumber=1&uploadId=123456789abcdef']),
|
||||||
|
'check_signature': mock_cs})
|
||||||
|
|
||||||
def test_invalid_uri(self):
|
def test_invalid_uri(self):
|
||||||
req = Request.blank('/bucket/invalid\xffname',
|
req = Request.blank('/bucket/invalid\xffname',
|
||||||
@ -711,16 +714,20 @@ class TestSwift3Middleware(Swift3TestCase):
|
|||||||
req = SigV4Request(env)
|
req = SigV4Request(env)
|
||||||
return req
|
return req
|
||||||
|
|
||||||
def string_to_sign(path, environ):
|
|
||||||
return _get_req(path, environ)._string_to_sign()
|
|
||||||
|
|
||||||
def canonical_string(path, environ):
|
def canonical_string(path, environ):
|
||||||
return _get_req(path, environ)._canonical_request()
|
return _get_req(path, environ)._canonical_request()
|
||||||
|
|
||||||
def verify(hash_val, path, environ):
|
def verify(hash_val, path, environ):
|
||||||
s = string_to_sign(path, environ)
|
# See http://docs.aws.amazon.com/general/latest/gr
|
||||||
s = s.split('\n')[3]
|
# /signature-v4-test-suite.html for where location, service, and
|
||||||
self.assertEqual(hash_val, s)
|
# signing key came from
|
||||||
|
with patch.object(CONF, 'location', 'us-east-1'), \
|
||||||
|
patch.object(swift3.request, 'SERVICE', 'host'):
|
||||||
|
req = _get_req(path, environ)
|
||||||
|
hash_in_sts = req._string_to_sign().split('\n')[3]
|
||||||
|
self.assertEqual(hash_val, hash_in_sts)
|
||||||
|
self.assertTrue(req.check_signature(
|
||||||
|
'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'))
|
||||||
|
|
||||||
# all next data got from aws4_testsuite from Amazon
|
# all next data got from aws4_testsuite from Amazon
|
||||||
# http://docs.aws.amazon.com/general/latest/gr/samples
|
# http://docs.aws.amazon.com/general/latest/gr/samples
|
||||||
@ -733,7 +740,9 @@ class TestSwift3Middleware(Swift3TestCase):
|
|||||||
'HTTP_AUTHORIZATION': (
|
'HTTP_AUTHORIZATION': (
|
||||||
'AWS4-HMAC-SHA256 '
|
'AWS4-HMAC-SHA256 '
|
||||||
'Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, '
|
'Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, '
|
||||||
'SignedHeaders=date;host, Signature=X'),
|
'SignedHeaders=date;host, '
|
||||||
|
'Signature=b27ccfbfa7df52a200ff74193ca6e32d'
|
||||||
|
'4b48b8856fab7ebf1c595d0670a7e470'),
|
||||||
'HTTP_HOST': 'host.foo.com'}
|
'HTTP_HOST': 'host.foo.com'}
|
||||||
verify('366b91fb121d72a00f46bbe8d395f53a'
|
verify('366b91fb121d72a00f46bbe8d395f53a'
|
||||||
'102b06dfb7e79636515208ed3fa606b1',
|
'102b06dfb7e79636515208ed3fa606b1',
|
||||||
@ -745,7 +754,9 @@ class TestSwift3Middleware(Swift3TestCase):
|
|||||||
'HTTP_AUTHORIZATION': (
|
'HTTP_AUTHORIZATION': (
|
||||||
'AWS4-HMAC-SHA256 '
|
'AWS4-HMAC-SHA256 '
|
||||||
'Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, '
|
'Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, '
|
||||||
'SignedHeaders=date;host;p, Signature=X'),
|
'SignedHeaders=date;host;p, '
|
||||||
|
'Signature=debf546796015d6f6ded8626f5ce9859'
|
||||||
|
'7c33b47b9164cf6b17b4642036fcb592'),
|
||||||
'HTTP_HOST': 'host.foo.com',
|
'HTTP_HOST': 'host.foo.com',
|
||||||
'HTTP_P': 'phfft'}
|
'HTTP_P': 'phfft'}
|
||||||
verify('dddd1902add08da1ac94782b05f9278c'
|
verify('dddd1902add08da1ac94782b05f9278c'
|
||||||
@ -757,7 +768,9 @@ class TestSwift3Middleware(Swift3TestCase):
|
|||||||
'HTTP_AUTHORIZATION': (
|
'HTTP_AUTHORIZATION': (
|
||||||
'AWS4-HMAC-SHA256 '
|
'AWS4-HMAC-SHA256 '
|
||||||
'Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, '
|
'Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, '
|
||||||
'SignedHeaders=date;host, Signature=X'),
|
'SignedHeaders=date;host, '
|
||||||
|
'Signature=8d6634c189aa8c75c2e51e106b6b5121'
|
||||||
|
'bed103fdb351f7d7d4381c738823af74'),
|
||||||
'HTTP_HOST': 'host.foo.com',
|
'HTTP_HOST': 'host.foo.com',
|
||||||
'RAW_PATH_INFO': '/%E1%88%B4'}
|
'RAW_PATH_INFO': '/%E1%88%B4'}
|
||||||
|
|
||||||
@ -779,7 +792,9 @@ class TestSwift3Middleware(Swift3TestCase):
|
|||||||
'HTTP_AUTHORIZATION': (
|
'HTTP_AUTHORIZATION': (
|
||||||
'AWS4-HMAC-SHA256 '
|
'AWS4-HMAC-SHA256 '
|
||||||
'Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, '
|
'Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, '
|
||||||
'SignedHeaders=date;host, Signature=X'),
|
'SignedHeaders=date;host, '
|
||||||
|
'Signature=0dc122f3b28b831ab48ba65cb47300de'
|
||||||
|
'53fbe91b577fe113edac383730254a3b'),
|
||||||
'HTTP_HOST': 'host.foo.com'}
|
'HTTP_HOST': 'host.foo.com'}
|
||||||
verify('2f23d14fe13caebf6dfda346285c6d9c'
|
verify('2f23d14fe13caebf6dfda346285c6d9c'
|
||||||
'14f49eaca8f5ec55c627dd7404f7a727',
|
'14f49eaca8f5ec55c627dd7404f7a727',
|
||||||
@ -791,7 +806,9 @@ class TestSwift3Middleware(Swift3TestCase):
|
|||||||
'HTTP_AUTHORIZATION': (
|
'HTTP_AUTHORIZATION': (
|
||||||
'AWS4-HMAC-SHA256 '
|
'AWS4-HMAC-SHA256 '
|
||||||
'Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, '
|
'Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, '
|
||||||
'SignedHeaders=date;host;zoo, Signature=X'),
|
'SignedHeaders=date;host;zoo, '
|
||||||
|
'Signature=273313af9d0c265c531e11db70bbd653'
|
||||||
|
'f3ba074c1009239e8559d3987039cad7'),
|
||||||
'HTTP_HOST': 'host.foo.com',
|
'HTTP_HOST': 'host.foo.com',
|
||||||
'HTTP_ZOO': 'ZOOBAR'}
|
'HTTP_ZOO': 'ZOOBAR'}
|
||||||
verify('3aae6d8274b8c03e2cc96fc7d6bda4b9'
|
verify('3aae6d8274b8c03e2cc96fc7d6bda4b9'
|
||||||
@ -804,7 +821,9 @@ class TestSwift3Middleware(Swift3TestCase):
|
|||||||
'HTTP_AUTHORIZATION': (
|
'HTTP_AUTHORIZATION': (
|
||||||
'AWS4-HMAC-SHA256 '
|
'AWS4-HMAC-SHA256 '
|
||||||
'Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, '
|
'Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, '
|
||||||
'SignedHeaders=date;host;content-type, Signature=X'),
|
'SignedHeaders=date;host;content-type, '
|
||||||
|
'Signature=b105eb10c6d318d2294de9d49dd8b031'
|
||||||
|
'b55e3c3fe139f2e637da70511e9e7b71'),
|
||||||
'HTTP_HOST': 'host.foo.com',
|
'HTTP_HOST': 'host.foo.com',
|
||||||
'HTTP_X_AMZ_CONTENT_SHA256':
|
'HTTP_X_AMZ_CONTENT_SHA256':
|
||||||
'3ba8907e7a252327488df390ed517c45'
|
'3ba8907e7a252327488df390ed517c45'
|
||||||
@ -821,7 +840,9 @@ class TestSwift3Middleware(Swift3TestCase):
|
|||||||
'HTTP_AUTHORIZATION': (
|
'HTTP_AUTHORIZATION': (
|
||||||
'AWS4-HMAC-SHA256 '
|
'AWS4-HMAC-SHA256 '
|
||||||
'Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, '
|
'Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, '
|
||||||
'SignedHeaders=date;host;content-type, Signature=X'),
|
'SignedHeaders=date;host;content-type, '
|
||||||
|
'Signature=5a15b22cf462f047318703b92e6f4f38'
|
||||||
|
'884e4a7ab7b1d6426ca46a8bd1c26cbc'),
|
||||||
'HTTP_HOST': 'host.foo.com',
|
'HTTP_HOST': 'host.foo.com',
|
||||||
'HTTP_X_AMZ_CONTENT_SHA256':
|
'HTTP_X_AMZ_CONTENT_SHA256':
|
||||||
'3ba8907e7a252327488df390ed517c45'
|
'3ba8907e7a252327488df390ed517c45'
|
||||||
|
@ -690,5 +690,69 @@ class TestRequest(Swift3TestCase):
|
|||||||
self.assertEqual(uri, '/bucket/obj1')
|
self.assertEqual(uri, '/bucket/obj1')
|
||||||
self.assertEqual(req.environ['PATH_INFO'], '/bucket/obj1')
|
self.assertEqual(req.environ['PATH_INFO'], '/bucket/obj1')
|
||||||
|
|
||||||
|
@patch.object(CONF, 'storage_domain', 's3.amazonaws.com')
|
||||||
|
@patch.object(S3_Request, '_validate_headers', lambda *a: None)
|
||||||
|
def test_check_signature_sigv2(self):
|
||||||
|
# See https://web.archive.org/web/20151226025049/http://
|
||||||
|
# docs.aws.amazon.com//AmazonS3/latest/dev/RESTAuthentication.html
|
||||||
|
req = Request.blank('/photos/puppy.jpg', headers={
|
||||||
|
'Host': 'johnsmith.s3.amazonaws.com',
|
||||||
|
'Date': 'Tue, 27 Mar 2007 19:36:42 +0000',
|
||||||
|
'Authorization': ('AWS AKIAIOSFODNN7EXAMPLE:'
|
||||||
|
'bWq2s1WEIj+Ydj0vQ697zp+IXMU='),
|
||||||
|
})
|
||||||
|
sigv2_req = S3_Request(req.environ)
|
||||||
|
expected_sts = '\n'.join([
|
||||||
|
'GET',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'Tue, 27 Mar 2007 19:36:42 +0000',
|
||||||
|
'/johnsmith/photos/puppy.jpg',
|
||||||
|
])
|
||||||
|
self.assertEqual(expected_sts, sigv2_req._string_to_sign())
|
||||||
|
self.assertTrue(sigv2_req.check_signature(
|
||||||
|
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'))
|
||||||
|
|
||||||
|
req = Request.blank('/photos/puppy.jpg', method='PUT', headers={
|
||||||
|
'Content-Type': 'image/jpeg',
|
||||||
|
'Content-Length': '94328',
|
||||||
|
'Host': 'johnsmith.s3.amazonaws.com',
|
||||||
|
'Date': 'Tue, 27 Mar 2007 21:15:45 +0000',
|
||||||
|
'Authorization': ('AWS AKIAIOSFODNN7EXAMPLE:'
|
||||||
|
'MyyxeRY7whkBe+bq8fHCL/2kKUg='),
|
||||||
|
})
|
||||||
|
sigv2_req = S3_Request(req.environ)
|
||||||
|
expected_sts = '\n'.join([
|
||||||
|
'PUT',
|
||||||
|
'',
|
||||||
|
'image/jpeg',
|
||||||
|
'Tue, 27 Mar 2007 21:15:45 +0000',
|
||||||
|
'/johnsmith/photos/puppy.jpg',
|
||||||
|
])
|
||||||
|
self.assertEqual(expected_sts, sigv2_req._string_to_sign())
|
||||||
|
self.assertTrue(sigv2_req.check_signature(
|
||||||
|
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'))
|
||||||
|
|
||||||
|
req = Request.blank(
|
||||||
|
'/?prefix=photos&max-keys=50&marker=puppy',
|
||||||
|
headers={
|
||||||
|
'User-Agent': 'Mozilla/5.0',
|
||||||
|
'Host': 'johnsmith.s3.amazonaws.com',
|
||||||
|
'Date': 'Tue, 27 Mar 2007 19:42:41 +0000',
|
||||||
|
'Authorization': ('AWS AKIAIOSFODNN7EXAMPLE:'
|
||||||
|
'htDYFYduRNen8P9ZfE/s9SuKy0U='),
|
||||||
|
})
|
||||||
|
sigv2_req = S3_Request(req.environ)
|
||||||
|
expected_sts = '\n'.join([
|
||||||
|
'GET',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'Tue, 27 Mar 2007 19:42:41 +0000',
|
||||||
|
'/johnsmith/',
|
||||||
|
])
|
||||||
|
self.assertEqual(expected_sts, sigv2_req._string_to_sign())
|
||||||
|
self.assertTrue(sigv2_req.check_signature(
|
||||||
|
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user