Skip to content

Commit 4310331

Browse files
committed
encryption: Stop being cutesy with os.path.join()
Turns out, we *care* about the path, and object paths *don't follow filesystem semantics*! Be explicit: /<account>/<container>/<object> Bump the key version number so we know whether we can trust the full path or not. Change-Id: Ide9d44cc18575306363126a93d91f662c6ee23e0 Related-Bug: 1813725
1 parent 4cb9469 commit 4310331

File tree

3 files changed

+109
-28
lines changed

3 files changed

+109
-28
lines changed

swift/common/middleware/crypto/keymaster.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
# limitations under the License.
1515
import hashlib
1616
import hmac
17-
import os
1817

1918
from swift.common.exceptions import UnknownSecretIdError
2019
from swift.common.middleware.crypto.crypto_utils import CRYPTO_KEY_CALLBACK
@@ -48,8 +47,8 @@ def __init__(self, keymaster, account, container, obj):
4847
self._keys = {}
4948
self.alternate_fetch_keys = None
5049

51-
def _make_key_id(self, path, secret_id):
52-
key_id = {'v': '1', 'path': path}
50+
def _make_key_id(self, path, secret_id, version):
51+
key_id = {'v': version, 'path': path}
5352
if secret_id:
5453
# stash secret_id so that decrypter can pass it back to get the
5554
# same keys
@@ -75,22 +74,32 @@ def fetch_crypto_keys(self, key_id=None, *args, **kwargs):
7574
"""
7675
if key_id:
7776
secret_id = key_id.get('secret_id')
77+
version = key_id['v']
78+
if version not in ('1', '2'):
79+
raise ValueError('Unknown key_id version: %s' % version)
7880
else:
7981
secret_id = self.keymaster.active_secret_id
80-
if secret_id in self._keys:
81-
return self._keys[secret_id]
82+
# v1 had a bug where we would claim the path was just the object
83+
# name if the object started with a slash. Bump versions to
84+
# establish that we can trust the path.
85+
version = '2'
86+
if (secret_id, version) in self._keys:
87+
return self._keys[(secret_id, version)]
8288

8389
keys = {}
84-
account_path = os.path.join(os.sep, self.account)
90+
account_path = '/' + self.account
8591

8692
try:
8793
if self.container:
88-
path = os.path.join(account_path, self.container)
94+
path = account_path + '/' + self.container
8995
keys['container'] = self.keymaster.create_key(
9096
path, secret_id=secret_id)
9197

9298
if self.obj:
93-
path = os.path.join(path, self.obj)
99+
if self.obj.startswith('/') and version == '1':
100+
path = self.obj
101+
else:
102+
path = path + '/' + self.obj
94103
keys['object'] = self.keymaster.create_key(
95104
path, secret_id=secret_id)
96105

@@ -104,21 +113,21 @@ def fetch_crypto_keys(self, key_id=None, *args, **kwargs):
104113
# metadata had its keys generated. Currently we have no need to
105114
# do that, so we are simply persisting this information for
106115
# future use.
107-
keys['id'] = self._make_key_id(path, secret_id)
116+
keys['id'] = self._make_key_id(path, secret_id, version)
108117
# pass back a list of key id dicts for all other secret ids in
109118
# case the caller is interested, in which case the caller can
110119
# call this method again for different secret ids; this avoided
111120
# changing the return type of the callback or adding another
112121
# callback. Note that the caller should assume no knowledge of
113122
# the content of these key id dicts.
114-
keys['all_ids'] = [self._make_key_id(path, id_)
123+
keys['all_ids'] = [self._make_key_id(path, id_, version)
115124
for id_ in self.keymaster.root_secret_ids]
116125
if self.alternate_fetch_keys:
117126
alternate_keys = self.alternate_fetch_keys(
118127
key_id=None, *args, **kwargs)
119128
keys['all_ids'].extend(alternate_keys.get('all_ids', []))
120129

121-
self._keys[secret_id] = keys
130+
self._keys[(secret_id, version)] = keys
122131

123132
return keys
124133
except UnknownSecretIdError:

test/unit/common/middleware/crypto/test_encryption.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ def _test_ondisk_data_after_write_with_crypto(self, policy_name):
479479
'/a/%s/%s' % (self.container_name, self.object_name),
480480
meta['key_id']['path'])
481481
self.assertIn('v', meta['key_id'])
482-
self.assertEqual('1', meta['key_id']['v'])
482+
self.assertEqual('2', meta['key_id']['v'])
483483
self.assertIn('cipher', meta)
484484
self.assertEqual(Crypto.cipher, meta['cipher'])
485485

test/unit/common/middleware/crypto/test_keymaster.py

Lines changed: 88 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ def setUp(self):
5151
def test_object_path(self):
5252
self.verify_keys_for_path(
5353
'/a/c/o', expected_keys=('object', 'container'))
54+
self.verify_keys_for_path(
55+
'/a/c//o', expected_keys=('object', 'container'))
5456

5557
def test_container_path(self):
5658
self.verify_keys_for_path(
@@ -79,7 +81,7 @@ def verify_keys_for_path(self, path, expected_keys, key_id=None):
7981
self.assertIn('id', keys)
8082
id = keys.pop('id')
8183
self.assertEqual(path, id['path'])
82-
self.assertEqual('1', id['v'])
84+
self.assertEqual('2', id['v'])
8385
keys.pop('all_ids')
8486
self.assertListEqual(sorted(expected_keys), sorted(keys.keys()),
8587
'%s %s got keys %r, but expected %r'
@@ -203,7 +205,7 @@ def test_chained_keymasters(self):
203205
keys = copy.deepcopy(req.environ[CRYPTO_KEY_CALLBACK](key_id=None))
204206
self.assertIn('id', keys)
205207
self.assertEqual(keys.pop('id'), {
206-
'v': '1',
208+
'v': '2',
207209
'path': '/a/c',
208210
'secret_id': '22',
209211
})
@@ -227,7 +229,7 @@ def test_chained_keymasters(self):
227229
at_least_one_old_style_id = True
228230
self.assertEqual(key_id, {
229231
'path': '/a/c',
230-
'v': '1',
232+
'v': '2',
231233
})
232234
self.assertTrue(at_least_one_old_style_id)
233235
self.assertEqual(len(all_keys), 3)
@@ -245,7 +247,7 @@ def test_chained_keymasters(self):
245247
keys = req.environ.get(CRYPTO_KEY_CALLBACK)(key_id=None)
246248
self.assertIn('id', keys)
247249
self.assertEqual(keys.pop('id'), {
248-
'v': '1',
250+
'v': '2',
249251
'path': '/a/c/o',
250252
'secret_id': '22',
251253
})
@@ -267,7 +269,7 @@ def test_chained_keymasters(self):
267269
self.assertIn(key_id.pop('secret_id'), {'22', 'my_secret_id'})
268270
self.assertEqual(key_id, {
269271
'path': '/a/c/o',
270-
'v': '1',
272+
'v': '2',
271273
})
272274
self.assertTrue(at_least_one_old_style_id)
273275
self.assertEqual(len(all_keys), 3)
@@ -346,8 +348,9 @@ def test_correct_root_secret_used(self):
346348

347349
# secret_id passed to fetch_crypto_keys callback
348350
for secret_id in ('my_secret_id', None):
349-
keys = self.verify_keys_for_path('/a/c/o', ('container', 'object'),
350-
key_id={'secret_id': secret_id})
351+
keys = self.verify_keys_for_path(
352+
'/a/c/o', ('container', 'object'),
353+
key_id={'secret_id': secret_id, 'v': '2', 'path': '/a/c/o'})
351354
expected_keys = {
352355
'container': hmac.new(secrets[secret_id], b'/a/c',
353356
digestmod=hashlib.sha256).digest(),
@@ -381,11 +384,11 @@ def mock_create_key(path, secret_id=None):
381384
digestmod=hashlib.sha256).digest(),
382385
'object': hmac.new(secrets['22'], b'/a/c/o',
383386
digestmod=hashlib.sha256).digest(),
384-
'id': {'path': '/a/c/o', 'secret_id': '22', 'v': '1'},
387+
'id': {'path': '/a/c/o', 'secret_id': '22', 'v': '2'},
385388
'all_ids': [
386-
{'path': '/a/c/o', 'v': '1'},
387-
{'path': '/a/c/o', 'secret_id': '22', 'v': '1'},
388-
{'path': '/a/c/o', 'secret_id': 'my_secret_id', 'v': '1'}]}
389+
{'path': '/a/c/o', 'v': '2'},
390+
{'path': '/a/c/o', 'secret_id': '22', 'v': '2'},
391+
{'path': '/a/c/o', 'secret_id': 'my_secret_id', 'v': '2'}]}
389392
self.assertEqual(expected_keys, keys)
390393
self.assertEqual([('/a/c', '22'), ('/a/c/o', '22')], calls)
391394
with mock.patch.object(self.app, 'create_key', mock_create_key):
@@ -394,22 +397,91 @@ def mock_create_key(path, secret_id=None):
394397
self.assertEqual([('/a/c', '22'), ('/a/c/o', '22')], calls)
395398
self.assertEqual(expected_keys, keys)
396399
with mock.patch.object(self.app, 'create_key', mock_create_key):
397-
keys = context.fetch_crypto_keys(key_id={'secret_id': None})
400+
keys = context.fetch_crypto_keys(key_id={
401+
'secret_id': None, 'v': '2', 'path': '/a/c/o'})
398402
expected_keys = {
399403
'container': hmac.new(secrets[None], b'/a/c',
400404
digestmod=hashlib.sha256).digest(),
401405
'object': hmac.new(secrets[None], b'/a/c/o',
402406
digestmod=hashlib.sha256).digest(),
403-
'id': {'path': '/a/c/o', 'v': '1'},
407+
'id': {'path': '/a/c/o', 'v': '2'},
404408
'all_ids': [
405-
{'path': '/a/c/o', 'v': '1'},
406-
{'path': '/a/c/o', 'secret_id': '22', 'v': '1'},
407-
{'path': '/a/c/o', 'secret_id': 'my_secret_id', 'v': '1'}]}
409+
{'path': '/a/c/o', 'v': '2'},
410+
{'path': '/a/c/o', 'secret_id': '22', 'v': '2'},
411+
{'path': '/a/c/o', 'secret_id': 'my_secret_id', 'v': '2'}]}
408412
self.assertEqual(expected_keys, keys)
409413
self.assertEqual([('/a/c', '22'), ('/a/c/o', '22'),
410414
('/a/c', None), ('/a/c/o', None)],
411415
calls)
412416

417+
def test_v1_keys(self):
418+
secrets = {None: os.urandom(32),
419+
'22': os.urandom(33)}
420+
conf = {}
421+
for secret_id, secret in secrets.items():
422+
opt = ('encryption_root_secret%s' %
423+
(('_%s' % secret_id) if secret_id else ''))
424+
conf[opt] = base64.b64encode(secret)
425+
conf['active_root_secret_id'] = '22'
426+
self.app = keymaster.KeyMaster(self.swift, conf)
427+
orig_create_key = self.app.create_key
428+
calls = []
429+
430+
def mock_create_key(path, secret_id=None):
431+
calls.append((path, secret_id))
432+
return orig_create_key(path, secret_id)
433+
434+
context = keymaster.KeyMasterContext(self.app, 'a', 'c', 'o')
435+
for version in ('1', '2'):
436+
with mock.patch.object(self.app, 'create_key', mock_create_key):
437+
keys = context.fetch_crypto_keys(key_id={
438+
'v': version, 'path': '/a/c/o'})
439+
expected_keys = {
440+
'container': hmac.new(secrets[None], b'/a/c',
441+
digestmod=hashlib.sha256).digest(),
442+
'object': hmac.new(secrets[None], b'/a/c/o',
443+
digestmod=hashlib.sha256).digest(),
444+
'id': {'path': '/a/c/o', 'v': version},
445+
'all_ids': [
446+
{'path': '/a/c/o', 'v': version},
447+
{'path': '/a/c/o', 'secret_id': '22', 'v': version}]}
448+
self.assertEqual(expected_keys, keys)
449+
self.assertEqual([('/a/c', None), ('/a/c/o', None)], calls)
450+
del calls[:]
451+
452+
context = keymaster.KeyMasterContext(self.app, 'a', 'c', '/o')
453+
with mock.patch.object(self.app, 'create_key', mock_create_key):
454+
keys = context.fetch_crypto_keys(key_id={
455+
'v': '1', 'path': '/o'})
456+
expected_keys = {
457+
'container': hmac.new(secrets[None], b'/a/c',
458+
digestmod=hashlib.sha256).digest(),
459+
'object': hmac.new(secrets[None], b'/o',
460+
digestmod=hashlib.sha256).digest(),
461+
'id': {'path': '/o', 'v': '1'},
462+
'all_ids': [
463+
{'path': '/o', 'v': '1'},
464+
{'path': '/o', 'secret_id': '22', 'v': '1'}]}
465+
self.assertEqual(expected_keys, keys)
466+
self.assertEqual([('/a/c', None), ('/o', None)], calls)
467+
del calls[:]
468+
469+
context = keymaster.KeyMasterContext(self.app, 'a', 'c', '/o')
470+
with mock.patch.object(self.app, 'create_key', mock_create_key):
471+
keys = context.fetch_crypto_keys(key_id={
472+
'v': '2', 'path': '/a/c//o'})
473+
expected_keys = {
474+
'container': hmac.new(secrets[None], b'/a/c',
475+
digestmod=hashlib.sha256).digest(),
476+
'object': hmac.new(secrets[None], b'/a/c//o',
477+
digestmod=hashlib.sha256).digest(),
478+
'id': {'path': '/a/c//o', 'v': '2'},
479+
'all_ids': [
480+
{'path': '/a/c//o', 'v': '2'},
481+
{'path': '/a/c//o', 'secret_id': '22', 'v': '2'}]}
482+
self.assertEqual(expected_keys, keys)
483+
self.assertEqual([('/a/c', None), ('/a/c//o', None)], calls)
484+
413485
@mock.patch('swift.common.middleware.crypto.keymaster.readconf')
414486
def test_keymaster_config_path(self, mock_readconf):
415487
for secret in (os.urandom(32), os.urandom(33), os.urandom(50)):

0 commit comments

Comments
 (0)