Skip to content

Commit fd9ceec

Browse files
Zuulopenstack-gerrit
Zuul
authored andcommitted
Merge "s3request: refactor to introduce SigChecker classes"
2 parents cb97944 + a93e420 commit fd9ceec

File tree

3 files changed

+136
-108
lines changed

3 files changed

+136
-108
lines changed

swift/common/middleware/s3api/s3request.py

Lines changed: 118 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -172,21 +172,118 @@ def chunk_update(self, chunk, eof, *args, **kwargs):
172172
return chunk
173173

174174

175-
class SigV4Mixin(object):
176-
"""
177-
A request class mixin to provide S3 signature v4 functionality
178-
"""
175+
class BaseSigChecker:
176+
def __init__(self, req):
177+
self.req = req
178+
self.signature = req.signature
179+
self.string_to_sign = self._string_to_sign()
180+
self._secret = None
181+
182+
def _string_to_sign(self):
183+
raise NotImplementedError
184+
185+
def _derive_secret(self, secret):
186+
return utf8encode(secret)
187+
188+
def _check_signature(self):
189+
raise NotImplementedError
179190

180191
def check_signature(self, secret):
181-
secret = utf8encode(secret)
182-
user_signature = self.signature
183-
derived_secret = b'AWS4' + secret
184-
for scope_piece in self.scope.values():
192+
self._secret = self._derive_secret(secret)
193+
return self._check_signature()
194+
195+
196+
class SigCheckerV2(BaseSigChecker):
197+
def _string_to_sign(self):
198+
"""
199+
Create 'StringToSign' value in Amazon terminology for v2.
200+
"""
201+
buf = [swob.wsgi_to_bytes(wsgi_str) for wsgi_str in [
202+
self.req.method,
203+
_header_strip(self.req.headers.get('Content-MD5')) or '',
204+
_header_strip(self.req.headers.get('Content-Type')) or '']]
205+
206+
if 'headers_raw' in self.req.environ: # eventlet >= 0.19.0
207+
# See https://github.com/eventlet/eventlet/commit/67ec999
208+
amz_headers = defaultdict(list)
209+
for key, value in self.req.environ['headers_raw']:
210+
key = key.lower()
211+
if not key.startswith('x-amz-'):
212+
continue
213+
amz_headers[key.strip()].append(value.strip())
214+
amz_headers = dict((key, ','.join(value))
215+
for key, value in amz_headers.items())
216+
else: # mostly-functional fallback
217+
amz_headers = dict((key.lower(), value)
218+
for key, value in self.req.headers.items()
219+
if key.lower().startswith('x-amz-'))
220+
221+
if self.req._is_header_auth:
222+
if 'x-amz-date' in amz_headers:
223+
buf.append(b'')
224+
elif 'Date' in self.req.headers:
225+
buf.append(swob.wsgi_to_bytes(self.req.headers['Date']))
226+
elif self.req._is_query_auth:
227+
buf.append(swob.wsgi_to_bytes(self.req.params['Expires']))
228+
else:
229+
# Should have already raised NotS3Request in _parse_auth_info,
230+
# but as a sanity check...
231+
raise AccessDenied(reason='not_s3')
232+
233+
for key, value in sorted(amz_headers.items()):
234+
buf.append(swob.wsgi_to_bytes("%s:%s" % (key, value)))
235+
236+
path = self.req._canonical_uri()
237+
if self.req.query_string:
238+
path += '?' + self.req.query_string
239+
params = []
240+
if '?' in path:
241+
path, args = path.split('?', 1)
242+
for key, value in sorted(self.req.params.items()):
243+
if key in ALLOWED_SUB_RESOURCES:
244+
params.append('%s=%s' % (key, value) if value else key)
245+
if params:
246+
buf.append(swob.wsgi_to_bytes('%s?%s' % (path, '&'.join(params))))
247+
else:
248+
buf.append(swob.wsgi_to_bytes(path))
249+
return b'\n'.join(buf)
250+
251+
def _check_signature(self):
252+
valid_signature = base64.b64encode(hmac.new(
253+
self._secret, self.string_to_sign, sha1
254+
).digest()).strip().decode('ascii')
255+
return streq_const_time(self.signature, valid_signature)
256+
257+
258+
class SigCheckerV4(BaseSigChecker):
259+
def __init__(self, req):
260+
super().__init__(req)
261+
self._all_chunk_signatures_valid = True
262+
263+
def _string_to_sign(self):
264+
return b'\n'.join([
265+
b'AWS4-HMAC-SHA256',
266+
self.req.timestamp.amz_date_format.encode('ascii'),
267+
'/'.join(self.req.scope.values()).encode('utf8'),
268+
sha256(self.req._canonical_request()).hexdigest().encode('ascii')])
269+
270+
def _derive_secret(self, secret):
271+
derived_secret = b'AWS4' + super()._derive_secret(secret)
272+
for scope_piece in self.req.scope.values():
185273
derived_secret = hmac.new(
186274
derived_secret, scope_piece.encode('utf8'), sha256).digest()
275+
return derived_secret
276+
277+
def _check_signature(self):
187278
valid_signature = hmac.new(
188-
derived_secret, self.string_to_sign, sha256).hexdigest()
189-
return streq_const_time(user_signature, valid_signature)
279+
self._secret, self.string_to_sign, sha256).hexdigest()
280+
return streq_const_time(self.signature, valid_signature)
281+
282+
283+
class SigV4Mixin(object):
284+
"""
285+
A request class mixin to provide S3 signature v4 functionality
286+
"""
190287

191288
@property
192289
def _is_query_auth(self):
@@ -507,16 +604,6 @@ def scope(self):
507604
('terminal', 'aws4_request'),
508605
])
509606

510-
def _string_to_sign(self):
511-
"""
512-
Create 'StringToSign' value in Amazon terminology for v4.
513-
"""
514-
return b'\n'.join([
515-
b'AWS4-HMAC-SHA256',
516-
self.timestamp.amz_date_format.encode('ascii'),
517-
'/'.join(self.scope.values()).encode('utf8'),
518-
sha256(self._canonical_request()).hexdigest().encode('ascii')])
519-
520607
def signature_does_not_match_kwargs(self):
521608
kwargs = super(SigV4Mixin, self).signature_does_not_match_kwargs()
522609
cr = self._canonical_request()
@@ -566,13 +653,18 @@ def __init__(self, env, app=None, conf=None):
566653
self.bucket_in_host = self._parse_host()
567654
self.container_name, self.object_name = self._parse_uri()
568655
self._validate_headers()
569-
# Lock in string-to-sign now, before we start messing with query params
570-
self.string_to_sign = self._string_to_sign()
656+
if isinstance(self, SigV4Mixin):
657+
# this is a deliberate but only partial shift away from the
658+
# 'inherit and override from mixin' pattern towards a 'compose
659+
# adapters' pattern.
660+
self.sig_checker = SigCheckerV4(self)
661+
else:
662+
self.sig_checker = SigCheckerV2(self)
571663
self.environ['s3api.auth_details'] = {
572664
'access_key': self.access_key,
573665
'signature': self.signature,
574-
'string_to_sign': self.string_to_sign,
575-
'check_signature': self.check_signature,
666+
'string_to_sign': self.sig_checker.string_to_sign,
667+
'check_signature': self.sig_checker.check_signature,
576668
}
577669
self.account = None
578670
self.user_id = None
@@ -633,14 +725,6 @@ def validate_part_number(self, parts_count=None, check_max=True):
633725

634726
return part_number
635727

636-
def check_signature(self, secret):
637-
secret = utf8encode(secret)
638-
user_signature = self.signature
639-
valid_signature = base64.b64encode(hmac.new(
640-
secret, self.string_to_sign, sha1
641-
).digest()).strip().decode('ascii')
642-
return streq_const_time(user_signature, valid_signature)
643-
644728
@property
645729
def timestamp(self):
646730
"""
@@ -1041,70 +1125,14 @@ def _canonical_uri(self):
10411125
raw_path_info = '/' + self.bucket_in_host + raw_path_info
10421126
return raw_path_info
10431127

1044-
def _string_to_sign(self):
1045-
"""
1046-
Create 'StringToSign' value in Amazon terminology for v2.
1047-
"""
1048-
amz_headers = {}
1049-
1050-
buf = [swob.wsgi_to_bytes(wsgi_str) for wsgi_str in [
1051-
self.method,
1052-
_header_strip(self.headers.get('Content-MD5')) or '',
1053-
_header_strip(self.headers.get('Content-Type')) or '']]
1054-
1055-
if 'headers_raw' in self.environ: # eventlet >= 0.19.0
1056-
# See https://github.com/eventlet/eventlet/commit/67ec999
1057-
amz_headers = defaultdict(list)
1058-
for key, value in self.environ['headers_raw']:
1059-
key = key.lower()
1060-
if not key.startswith('x-amz-'):
1061-
continue
1062-
amz_headers[key.strip()].append(value.strip())
1063-
amz_headers = dict((key, ','.join(value))
1064-
for key, value in amz_headers.items())
1065-
else: # mostly-functional fallback
1066-
amz_headers = dict((key.lower(), value)
1067-
for key, value in self.headers.items()
1068-
if key.lower().startswith('x-amz-'))
1069-
1070-
if self._is_header_auth:
1071-
if 'x-amz-date' in amz_headers:
1072-
buf.append(b'')
1073-
elif 'Date' in self.headers:
1074-
buf.append(swob.wsgi_to_bytes(self.headers['Date']))
1075-
elif self._is_query_auth:
1076-
buf.append(swob.wsgi_to_bytes(self.params['Expires']))
1077-
else:
1078-
# Should have already raised NotS3Request in _parse_auth_info,
1079-
# but as a sanity check...
1080-
raise AccessDenied(reason='not_s3')
1081-
1082-
for key, value in sorted(amz_headers.items()):
1083-
buf.append(swob.wsgi_to_bytes("%s:%s" % (key, value)))
1084-
1085-
path = self._canonical_uri()
1086-
if self.query_string:
1087-
path += '?' + self.query_string
1088-
params = []
1089-
if '?' in path:
1090-
path, args = path.split('?', 1)
1091-
for key, value in sorted(self.params.items()):
1092-
if key in ALLOWED_SUB_RESOURCES:
1093-
params.append('%s=%s' % (key, value) if value else key)
1094-
if params:
1095-
buf.append(swob.wsgi_to_bytes('%s?%s' % (path, '&'.join(params))))
1096-
else:
1097-
buf.append(swob.wsgi_to_bytes(path))
1098-
return b'\n'.join(buf)
1099-
11001128
def signature_does_not_match_kwargs(self):
11011129
return {
11021130
'a_w_s_access_key_id': self.access_key,
1103-
'string_to_sign': self.string_to_sign,
1131+
'string_to_sign': self.sig_checker.string_to_sign,
11041132
'signature_provided': self.signature,
11051133
'string_to_sign_bytes': ' '.join(
11061134
format(ord(c), '02x')
1107-
for c in self.string_to_sign.decode('latin1')),
1135+
for c in self.sig_checker.string_to_sign.decode('latin1')),
11081136
}
11091137

11101138
@property

test/unit/common/middleware/s3api/test_s3api.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,7 @@ def test_token_generation(self):
706706
date_header = self.get_date_header()
707707
req.headers['Date'] = date_header
708708
with mock.patch('swift.common.middleware.s3api.s3request.'
709-
'S3Request.check_signature') as mock_cs:
709+
'SigCheckerV2.check_signature') as mock_cs:
710710
status, headers, body = self.call_s3api(req)
711711
self.assertIn('swift.backend_path', req.environ)
712712
self.assertEqual(
@@ -737,7 +737,7 @@ def test_non_ascii_user(self):
737737
date_header = self.get_date_header()
738738
req.headers['Date'] = date_header
739739
with mock.patch('swift.common.middleware.s3api.s3request.'
740-
'S3Request.check_signature') as mock_cs:
740+
'SigCheckerV2.check_signature') as mock_cs:
741741
status, headers, body = self.call_s3api(req)
742742
self.assertIn('swift.backend_path', req.environ)
743743
self.assertEqual(
@@ -1300,9 +1300,9 @@ def verify(hash_val, path, environ):
13001300
patch.object(swift.common.middleware.s3api.s3request,
13011301
'SERVICE', 'host'):
13021302
req = _get_req(path, environ)
1303-
hash_in_sts = req._string_to_sign().split(b'\n')[3]
1303+
hash_in_sts = req.sig_checker._string_to_sign().split(b'\n')[3]
13041304
self.assertEqual(hash_val, hash_in_sts.decode('ascii'))
1305-
self.assertTrue(req.check_signature(
1305+
self.assertTrue(req.sig_checker.check_signature(
13061306
'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'))
13071307

13081308
# all next data got from aws4_testsuite from Amazon

test/unit/common/middleware/s3api/test_s3request.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -791,8 +791,8 @@ def _test_check_signature_sigv2(self, secret):
791791
b'Tue, 27 Mar 2007 19:36:42 +0000',
792792
b'/johnsmith/photos/puppy.jpg',
793793
])
794-
self.assertEqual(expected_sts, sigv2_req._string_to_sign())
795-
self.assertTrue(sigv2_req.check_signature(secret))
794+
self.assertEqual(expected_sts, sigv2_req.sig_checker.string_to_sign)
795+
self.assertTrue(sigv2_req.sig_checker.check_signature(secret))
796796

797797
req = Request.blank('/photos/puppy.jpg', method='PUT', headers={
798798
'Content-Type': 'image/jpeg',
@@ -811,8 +811,8 @@ def _test_check_signature_sigv2(self, secret):
811811
b'Tue, 27 Mar 2007 21:15:45 +0000',
812812
b'/johnsmith/photos/puppy.jpg',
813813
])
814-
self.assertEqual(expected_sts, sigv2_req._string_to_sign())
815-
self.assertTrue(sigv2_req.check_signature(secret))
814+
self.assertEqual(expected_sts, sigv2_req.sig_checker.string_to_sign)
815+
self.assertTrue(sigv2_req.sig_checker.check_signature(secret))
816816

817817
req = Request.blank(
818818
'/?prefix=photos&max-keys=50&marker=puppy',
@@ -832,12 +832,12 @@ def _test_check_signature_sigv2(self, secret):
832832
b'Tue, 27 Mar 2007 19:42:41 +0000',
833833
b'/johnsmith/',
834834
])
835-
self.assertEqual(expected_sts, sigv2_req._string_to_sign())
836-
self.assertTrue(sigv2_req.check_signature(secret))
835+
self.assertEqual(expected_sts, sigv2_req.sig_checker.string_to_sign)
836+
self.assertTrue(sigv2_req.sig_checker.check_signature(secret))
837837

838838
with patch('swift.common.middleware.s3api.s3request.streq_const_time',
839839
return_value=True) as mock_eq:
840-
self.assertTrue(sigv2_req.check_signature(secret))
840+
self.assertTrue(sigv2_req.sig_checker.check_signature(secret))
841841
mock_eq.assert_called_once()
842842

843843
def test_check_signature_sigv2(self):
@@ -861,7 +861,7 @@ def test_check_signature_multi_bytes_secret_failure(self):
861861
'storage_domains': ['s3.amazonaws.com']}))
862862
# This is a failure case with utf-8 non-ascii multi-bytes charactor
863863
# but we expect to return just False instead of exceptions
864-
self.assertFalse(sigv2_req.check_signature(
864+
self.assertFalse(sigv2_req.sig_checker.check_signature(
865865
u'\u30c9\u30e9\u30b4\u30f3'))
866866

867867
# Test v4 check_signature with multi bytes invalid secret
@@ -877,12 +877,12 @@ def test_check_signature_multi_bytes_secret_failure(self):
877877
})
878878
sigv4_req = SigV4Request(
879879
req.environ, Config({'storage_domains': ['s3.amazonaws.com']}))
880-
self.assertFalse(sigv4_req.check_signature(
880+
self.assertFalse(sigv4_req.sig_checker.check_signature(
881881
u'\u30c9\u30e9\u30b4\u30f3'))
882882

883883
with patch('swift.common.middleware.s3api.s3request.streq_const_time',
884884
return_value=False) as mock_eq:
885-
self.assertFalse(sigv4_req.check_signature(
885+
self.assertFalse(sigv4_req.sig_checker.check_signature(
886886
u'\u30c9\u30e9\u30b4\u30f3'))
887887
mock_eq.assert_called_once()
888888

@@ -908,7 +908,7 @@ def test_check_signature_sigv4_unsigned_payload(self):
908908
sigv4_req = SigV4Request(req.environ)
909909
self.assertTrue(
910910
sigv4_req._canonical_request().endswith(b'UNSIGNED-PAYLOAD'))
911-
self.assertTrue(sigv4_req.check_signature('secret'))
911+
self.assertTrue(sigv4_req.sig_checker.check_signature('secret'))
912912

913913
@patch.object(S3Request, '_validate_dates', lambda *a: None)
914914
def test_check_signature_sigv4_url_encode(self):
@@ -935,7 +935,7 @@ def test_check_signature_sigv4_url_encode(self):
935935
canonical_req = sigv4_req._canonical_request()
936936
self.assertIn(b'PUT\n/test/~/file%2C1_1%3A1-1\n', canonical_req)
937937
self.assertTrue(canonical_req.endswith(b'UNSIGNED-PAYLOAD'))
938-
self.assertTrue(sigv4_req.check_signature('secret'))
938+
self.assertTrue(sigv4_req.sig_checker.check_signature('secret'))
939939

940940
@patch.object(S3Request, '_validate_dates', lambda *a: None)
941941
def test_check_sigv4_req_zero_content_length_sha256(self):
@@ -979,7 +979,7 @@ def test_check_sigv4_req_zero_content_length_sha256(self):
979979
sigv4_req = SigV4Request(req.environ)
980980
self.assertTrue(
981981
sigv4_req._canonical_request().endswith(sha256_of_nothing))
982-
self.assertTrue(sigv4_req.check_signature('secret'))
982+
self.assertTrue(sigv4_req.sig_checker.check_signature('secret'))
983983

984984
# uppercase sha256 -- signature changes, but content's valid
985985
headers = {
@@ -998,7 +998,7 @@ def test_check_sigv4_req_zero_content_length_sha256(self):
998998
sigv4_req = SigV4Request(req.environ)
999999
self.assertTrue(
10001000
sigv4_req._canonical_request().endswith(sha256_of_nothing.upper()))
1001-
self.assertTrue(sigv4_req.check_signature('secret'))
1001+
self.assertTrue(sigv4_req.sig_checker.check_signature('secret'))
10021002

10031003
@patch.object(S3Request, '_validate_dates', lambda *a: None)
10041004
def test_v4_req_xmz_content_sha256_mismatch(self):

0 commit comments

Comments
 (0)