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
This commit is contained in:
parent
4cb9469186
commit
43103319d0
@ -14,7 +14,6 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import os
|
|
||||||
|
|
||||||
from swift.common.exceptions import UnknownSecretIdError
|
from swift.common.exceptions import UnknownSecretIdError
|
||||||
from swift.common.middleware.crypto.crypto_utils import CRYPTO_KEY_CALLBACK
|
from swift.common.middleware.crypto.crypto_utils import CRYPTO_KEY_CALLBACK
|
||||||
@ -48,8 +47,8 @@ class KeyMasterContext(WSGIContext):
|
|||||||
self._keys = {}
|
self._keys = {}
|
||||||
self.alternate_fetch_keys = None
|
self.alternate_fetch_keys = None
|
||||||
|
|
||||||
def _make_key_id(self, path, secret_id):
|
def _make_key_id(self, path, secret_id, version):
|
||||||
key_id = {'v': '1', 'path': path}
|
key_id = {'v': version, 'path': path}
|
||||||
if secret_id:
|
if secret_id:
|
||||||
# stash secret_id so that decrypter can pass it back to get the
|
# stash secret_id so that decrypter can pass it back to get the
|
||||||
# same keys
|
# same keys
|
||||||
@ -75,22 +74,32 @@ class KeyMasterContext(WSGIContext):
|
|||||||
"""
|
"""
|
||||||
if key_id:
|
if key_id:
|
||||||
secret_id = key_id.get('secret_id')
|
secret_id = key_id.get('secret_id')
|
||||||
|
version = key_id['v']
|
||||||
|
if version not in ('1', '2'):
|
||||||
|
raise ValueError('Unknown key_id version: %s' % version)
|
||||||
else:
|
else:
|
||||||
secret_id = self.keymaster.active_secret_id
|
secret_id = self.keymaster.active_secret_id
|
||||||
if secret_id in self._keys:
|
# v1 had a bug where we would claim the path was just the object
|
||||||
return self._keys[secret_id]
|
# name if the object started with a slash. Bump versions to
|
||||||
|
# establish that we can trust the path.
|
||||||
|
version = '2'
|
||||||
|
if (secret_id, version) in self._keys:
|
||||||
|
return self._keys[(secret_id, version)]
|
||||||
|
|
||||||
keys = {}
|
keys = {}
|
||||||
account_path = os.path.join(os.sep, self.account)
|
account_path = '/' + self.account
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.container:
|
if self.container:
|
||||||
path = os.path.join(account_path, self.container)
|
path = account_path + '/' + self.container
|
||||||
keys['container'] = self.keymaster.create_key(
|
keys['container'] = self.keymaster.create_key(
|
||||||
path, secret_id=secret_id)
|
path, secret_id=secret_id)
|
||||||
|
|
||||||
if self.obj:
|
if self.obj:
|
||||||
path = os.path.join(path, self.obj)
|
if self.obj.startswith('/') and version == '1':
|
||||||
|
path = self.obj
|
||||||
|
else:
|
||||||
|
path = path + '/' + self.obj
|
||||||
keys['object'] = self.keymaster.create_key(
|
keys['object'] = self.keymaster.create_key(
|
||||||
path, secret_id=secret_id)
|
path, secret_id=secret_id)
|
||||||
|
|
||||||
@ -104,21 +113,21 @@ class KeyMasterContext(WSGIContext):
|
|||||||
# metadata had its keys generated. Currently we have no need to
|
# metadata had its keys generated. Currently we have no need to
|
||||||
# do that, so we are simply persisting this information for
|
# do that, so we are simply persisting this information for
|
||||||
# future use.
|
# future use.
|
||||||
keys['id'] = self._make_key_id(path, secret_id)
|
keys['id'] = self._make_key_id(path, secret_id, version)
|
||||||
# pass back a list of key id dicts for all other secret ids in
|
# pass back a list of key id dicts for all other secret ids in
|
||||||
# case the caller is interested, in which case the caller can
|
# case the caller is interested, in which case the caller can
|
||||||
# call this method again for different secret ids; this avoided
|
# call this method again for different secret ids; this avoided
|
||||||
# changing the return type of the callback or adding another
|
# changing the return type of the callback or adding another
|
||||||
# callback. Note that the caller should assume no knowledge of
|
# callback. Note that the caller should assume no knowledge of
|
||||||
# the content of these key id dicts.
|
# the content of these key id dicts.
|
||||||
keys['all_ids'] = [self._make_key_id(path, id_)
|
keys['all_ids'] = [self._make_key_id(path, id_, version)
|
||||||
for id_ in self.keymaster.root_secret_ids]
|
for id_ in self.keymaster.root_secret_ids]
|
||||||
if self.alternate_fetch_keys:
|
if self.alternate_fetch_keys:
|
||||||
alternate_keys = self.alternate_fetch_keys(
|
alternate_keys = self.alternate_fetch_keys(
|
||||||
key_id=None, *args, **kwargs)
|
key_id=None, *args, **kwargs)
|
||||||
keys['all_ids'].extend(alternate_keys.get('all_ids', []))
|
keys['all_ids'].extend(alternate_keys.get('all_ids', []))
|
||||||
|
|
||||||
self._keys[secret_id] = keys
|
self._keys[(secret_id, version)] = keys
|
||||||
|
|
||||||
return keys
|
return keys
|
||||||
except UnknownSecretIdError:
|
except UnknownSecretIdError:
|
||||||
|
@ -479,7 +479,7 @@ class TestCryptoPipelineChanges(unittest.TestCase):
|
|||||||
'/a/%s/%s' % (self.container_name, self.object_name),
|
'/a/%s/%s' % (self.container_name, self.object_name),
|
||||||
meta['key_id']['path'])
|
meta['key_id']['path'])
|
||||||
self.assertIn('v', meta['key_id'])
|
self.assertIn('v', meta['key_id'])
|
||||||
self.assertEqual('1', meta['key_id']['v'])
|
self.assertEqual('2', meta['key_id']['v'])
|
||||||
self.assertIn('cipher', meta)
|
self.assertIn('cipher', meta)
|
||||||
self.assertEqual(Crypto.cipher, meta['cipher'])
|
self.assertEqual(Crypto.cipher, meta['cipher'])
|
||||||
|
|
||||||
|
@ -51,6 +51,8 @@ class TestKeymaster(unittest.TestCase):
|
|||||||
def test_object_path(self):
|
def test_object_path(self):
|
||||||
self.verify_keys_for_path(
|
self.verify_keys_for_path(
|
||||||
'/a/c/o', expected_keys=('object', 'container'))
|
'/a/c/o', expected_keys=('object', 'container'))
|
||||||
|
self.verify_keys_for_path(
|
||||||
|
'/a/c//o', expected_keys=('object', 'container'))
|
||||||
|
|
||||||
def test_container_path(self):
|
def test_container_path(self):
|
||||||
self.verify_keys_for_path(
|
self.verify_keys_for_path(
|
||||||
@ -79,7 +81,7 @@ class TestKeymaster(unittest.TestCase):
|
|||||||
self.assertIn('id', keys)
|
self.assertIn('id', keys)
|
||||||
id = keys.pop('id')
|
id = keys.pop('id')
|
||||||
self.assertEqual(path, id['path'])
|
self.assertEqual(path, id['path'])
|
||||||
self.assertEqual('1', id['v'])
|
self.assertEqual('2', id['v'])
|
||||||
keys.pop('all_ids')
|
keys.pop('all_ids')
|
||||||
self.assertListEqual(sorted(expected_keys), sorted(keys.keys()),
|
self.assertListEqual(sorted(expected_keys), sorted(keys.keys()),
|
||||||
'%s %s got keys %r, but expected %r'
|
'%s %s got keys %r, but expected %r'
|
||||||
@ -203,7 +205,7 @@ class TestKeymaster(unittest.TestCase):
|
|||||||
keys = copy.deepcopy(req.environ[CRYPTO_KEY_CALLBACK](key_id=None))
|
keys = copy.deepcopy(req.environ[CRYPTO_KEY_CALLBACK](key_id=None))
|
||||||
self.assertIn('id', keys)
|
self.assertIn('id', keys)
|
||||||
self.assertEqual(keys.pop('id'), {
|
self.assertEqual(keys.pop('id'), {
|
||||||
'v': '1',
|
'v': '2',
|
||||||
'path': '/a/c',
|
'path': '/a/c',
|
||||||
'secret_id': '22',
|
'secret_id': '22',
|
||||||
})
|
})
|
||||||
@ -227,7 +229,7 @@ class TestKeymaster(unittest.TestCase):
|
|||||||
at_least_one_old_style_id = True
|
at_least_one_old_style_id = True
|
||||||
self.assertEqual(key_id, {
|
self.assertEqual(key_id, {
|
||||||
'path': '/a/c',
|
'path': '/a/c',
|
||||||
'v': '1',
|
'v': '2',
|
||||||
})
|
})
|
||||||
self.assertTrue(at_least_one_old_style_id)
|
self.assertTrue(at_least_one_old_style_id)
|
||||||
self.assertEqual(len(all_keys), 3)
|
self.assertEqual(len(all_keys), 3)
|
||||||
@ -245,7 +247,7 @@ class TestKeymaster(unittest.TestCase):
|
|||||||
keys = req.environ.get(CRYPTO_KEY_CALLBACK)(key_id=None)
|
keys = req.environ.get(CRYPTO_KEY_CALLBACK)(key_id=None)
|
||||||
self.assertIn('id', keys)
|
self.assertIn('id', keys)
|
||||||
self.assertEqual(keys.pop('id'), {
|
self.assertEqual(keys.pop('id'), {
|
||||||
'v': '1',
|
'v': '2',
|
||||||
'path': '/a/c/o',
|
'path': '/a/c/o',
|
||||||
'secret_id': '22',
|
'secret_id': '22',
|
||||||
})
|
})
|
||||||
@ -267,7 +269,7 @@ class TestKeymaster(unittest.TestCase):
|
|||||||
self.assertIn(key_id.pop('secret_id'), {'22', 'my_secret_id'})
|
self.assertIn(key_id.pop('secret_id'), {'22', 'my_secret_id'})
|
||||||
self.assertEqual(key_id, {
|
self.assertEqual(key_id, {
|
||||||
'path': '/a/c/o',
|
'path': '/a/c/o',
|
||||||
'v': '1',
|
'v': '2',
|
||||||
})
|
})
|
||||||
self.assertTrue(at_least_one_old_style_id)
|
self.assertTrue(at_least_one_old_style_id)
|
||||||
self.assertEqual(len(all_keys), 3)
|
self.assertEqual(len(all_keys), 3)
|
||||||
@ -346,8 +348,9 @@ class TestKeymaster(unittest.TestCase):
|
|||||||
|
|
||||||
# secret_id passed to fetch_crypto_keys callback
|
# secret_id passed to fetch_crypto_keys callback
|
||||||
for secret_id in ('my_secret_id', None):
|
for secret_id in ('my_secret_id', None):
|
||||||
keys = self.verify_keys_for_path('/a/c/o', ('container', 'object'),
|
keys = self.verify_keys_for_path(
|
||||||
key_id={'secret_id': secret_id})
|
'/a/c/o', ('container', 'object'),
|
||||||
|
key_id={'secret_id': secret_id, 'v': '2', 'path': '/a/c/o'})
|
||||||
expected_keys = {
|
expected_keys = {
|
||||||
'container': hmac.new(secrets[secret_id], b'/a/c',
|
'container': hmac.new(secrets[secret_id], b'/a/c',
|
||||||
digestmod=hashlib.sha256).digest(),
|
digestmod=hashlib.sha256).digest(),
|
||||||
@ -381,11 +384,11 @@ class TestKeymaster(unittest.TestCase):
|
|||||||
digestmod=hashlib.sha256).digest(),
|
digestmod=hashlib.sha256).digest(),
|
||||||
'object': hmac.new(secrets['22'], b'/a/c/o',
|
'object': hmac.new(secrets['22'], b'/a/c/o',
|
||||||
digestmod=hashlib.sha256).digest(),
|
digestmod=hashlib.sha256).digest(),
|
||||||
'id': {'path': '/a/c/o', 'secret_id': '22', 'v': '1'},
|
'id': {'path': '/a/c/o', 'secret_id': '22', 'v': '2'},
|
||||||
'all_ids': [
|
'all_ids': [
|
||||||
{'path': '/a/c/o', 'v': '1'},
|
{'path': '/a/c/o', 'v': '2'},
|
||||||
{'path': '/a/c/o', 'secret_id': '22', 'v': '1'},
|
{'path': '/a/c/o', 'secret_id': '22', 'v': '2'},
|
||||||
{'path': '/a/c/o', 'secret_id': 'my_secret_id', 'v': '1'}]}
|
{'path': '/a/c/o', 'secret_id': 'my_secret_id', 'v': '2'}]}
|
||||||
self.assertEqual(expected_keys, keys)
|
self.assertEqual(expected_keys, keys)
|
||||||
self.assertEqual([('/a/c', '22'), ('/a/c/o', '22')], calls)
|
self.assertEqual([('/a/c', '22'), ('/a/c/o', '22')], calls)
|
||||||
with mock.patch.object(self.app, 'create_key', mock_create_key):
|
with mock.patch.object(self.app, 'create_key', mock_create_key):
|
||||||
@ -394,22 +397,91 @@ class TestKeymaster(unittest.TestCase):
|
|||||||
self.assertEqual([('/a/c', '22'), ('/a/c/o', '22')], calls)
|
self.assertEqual([('/a/c', '22'), ('/a/c/o', '22')], calls)
|
||||||
self.assertEqual(expected_keys, keys)
|
self.assertEqual(expected_keys, keys)
|
||||||
with mock.patch.object(self.app, 'create_key', mock_create_key):
|
with mock.patch.object(self.app, 'create_key', mock_create_key):
|
||||||
keys = context.fetch_crypto_keys(key_id={'secret_id': None})
|
keys = context.fetch_crypto_keys(key_id={
|
||||||
|
'secret_id': None, 'v': '2', 'path': '/a/c/o'})
|
||||||
expected_keys = {
|
expected_keys = {
|
||||||
'container': hmac.new(secrets[None], b'/a/c',
|
'container': hmac.new(secrets[None], b'/a/c',
|
||||||
digestmod=hashlib.sha256).digest(),
|
digestmod=hashlib.sha256).digest(),
|
||||||
'object': hmac.new(secrets[None], b'/a/c/o',
|
'object': hmac.new(secrets[None], b'/a/c/o',
|
||||||
digestmod=hashlib.sha256).digest(),
|
digestmod=hashlib.sha256).digest(),
|
||||||
'id': {'path': '/a/c/o', 'v': '1'},
|
'id': {'path': '/a/c/o', 'v': '2'},
|
||||||
'all_ids': [
|
'all_ids': [
|
||||||
{'path': '/a/c/o', 'v': '1'},
|
{'path': '/a/c/o', 'v': '2'},
|
||||||
{'path': '/a/c/o', 'secret_id': '22', 'v': '1'},
|
{'path': '/a/c/o', 'secret_id': '22', 'v': '2'},
|
||||||
{'path': '/a/c/o', 'secret_id': 'my_secret_id', 'v': '1'}]}
|
{'path': '/a/c/o', 'secret_id': 'my_secret_id', 'v': '2'}]}
|
||||||
self.assertEqual(expected_keys, keys)
|
self.assertEqual(expected_keys, keys)
|
||||||
self.assertEqual([('/a/c', '22'), ('/a/c/o', '22'),
|
self.assertEqual([('/a/c', '22'), ('/a/c/o', '22'),
|
||||||
('/a/c', None), ('/a/c/o', None)],
|
('/a/c', None), ('/a/c/o', None)],
|
||||||
calls)
|
calls)
|
||||||
|
|
||||||
|
def test_v1_keys(self):
|
||||||
|
secrets = {None: os.urandom(32),
|
||||||
|
'22': os.urandom(33)}
|
||||||
|
conf = {}
|
||||||
|
for secret_id, secret in secrets.items():
|
||||||
|
opt = ('encryption_root_secret%s' %
|
||||||
|
(('_%s' % secret_id) if secret_id else ''))
|
||||||
|
conf[opt] = base64.b64encode(secret)
|
||||||
|
conf['active_root_secret_id'] = '22'
|
||||||
|
self.app = keymaster.KeyMaster(self.swift, conf)
|
||||||
|
orig_create_key = self.app.create_key
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def mock_create_key(path, secret_id=None):
|
||||||
|
calls.append((path, secret_id))
|
||||||
|
return orig_create_key(path, secret_id)
|
||||||
|
|
||||||
|
context = keymaster.KeyMasterContext(self.app, 'a', 'c', 'o')
|
||||||
|
for version in ('1', '2'):
|
||||||
|
with mock.patch.object(self.app, 'create_key', mock_create_key):
|
||||||
|
keys = context.fetch_crypto_keys(key_id={
|
||||||
|
'v': version, 'path': '/a/c/o'})
|
||||||
|
expected_keys = {
|
||||||
|
'container': hmac.new(secrets[None], b'/a/c',
|
||||||
|
digestmod=hashlib.sha256).digest(),
|
||||||
|
'object': hmac.new(secrets[None], b'/a/c/o',
|
||||||
|
digestmod=hashlib.sha256).digest(),
|
||||||
|
'id': {'path': '/a/c/o', 'v': version},
|
||||||
|
'all_ids': [
|
||||||
|
{'path': '/a/c/o', 'v': version},
|
||||||
|
{'path': '/a/c/o', 'secret_id': '22', 'v': version}]}
|
||||||
|
self.assertEqual(expected_keys, keys)
|
||||||
|
self.assertEqual([('/a/c', None), ('/a/c/o', None)], calls)
|
||||||
|
del calls[:]
|
||||||
|
|
||||||
|
context = keymaster.KeyMasterContext(self.app, 'a', 'c', '/o')
|
||||||
|
with mock.patch.object(self.app, 'create_key', mock_create_key):
|
||||||
|
keys = context.fetch_crypto_keys(key_id={
|
||||||
|
'v': '1', 'path': '/o'})
|
||||||
|
expected_keys = {
|
||||||
|
'container': hmac.new(secrets[None], b'/a/c',
|
||||||
|
digestmod=hashlib.sha256).digest(),
|
||||||
|
'object': hmac.new(secrets[None], b'/o',
|
||||||
|
digestmod=hashlib.sha256).digest(),
|
||||||
|
'id': {'path': '/o', 'v': '1'},
|
||||||
|
'all_ids': [
|
||||||
|
{'path': '/o', 'v': '1'},
|
||||||
|
{'path': '/o', 'secret_id': '22', 'v': '1'}]}
|
||||||
|
self.assertEqual(expected_keys, keys)
|
||||||
|
self.assertEqual([('/a/c', None), ('/o', None)], calls)
|
||||||
|
del calls[:]
|
||||||
|
|
||||||
|
context = keymaster.KeyMasterContext(self.app, 'a', 'c', '/o')
|
||||||
|
with mock.patch.object(self.app, 'create_key', mock_create_key):
|
||||||
|
keys = context.fetch_crypto_keys(key_id={
|
||||||
|
'v': '2', 'path': '/a/c//o'})
|
||||||
|
expected_keys = {
|
||||||
|
'container': hmac.new(secrets[None], b'/a/c',
|
||||||
|
digestmod=hashlib.sha256).digest(),
|
||||||
|
'object': hmac.new(secrets[None], b'/a/c//o',
|
||||||
|
digestmod=hashlib.sha256).digest(),
|
||||||
|
'id': {'path': '/a/c//o', 'v': '2'},
|
||||||
|
'all_ids': [
|
||||||
|
{'path': '/a/c//o', 'v': '2'},
|
||||||
|
{'path': '/a/c//o', 'secret_id': '22', 'v': '2'}]}
|
||||||
|
self.assertEqual(expected_keys, keys)
|
||||||
|
self.assertEqual([('/a/c', None), ('/a/c//o', None)], calls)
|
||||||
|
|
||||||
@mock.patch('swift.common.middleware.crypto.keymaster.readconf')
|
@mock.patch('swift.common.middleware.crypto.keymaster.readconf')
|
||||||
def test_keymaster_config_path(self, mock_readconf):
|
def test_keymaster_config_path(self, mock_readconf):
|
||||||
for secret in (os.urandom(32), os.urandom(33), os.urandom(50)):
|
for secret in (os.urandom(32), os.urandom(33), os.urandom(50)):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user