From: Joel Coffman Date: Tue, 24 Sep 2013 23:10:09 +0000 (-0400) Subject: Add key manager implementation with static key X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=ae6b7642e8d32ef5fa75cdcfe55be23c052fd547;p=openstack-build%2Fcinder-build.git Add key manager implementation with static key Per feedback received on other patch sets, an example key manager driver is required to support ephemeral storage encryption and Cinder volume encryption -- see * https://blueprints.launchpad.net/nova/+spec/encrypt-cinder-volumes * https://blueprints.launchpad.net/nova/+spec/encrypt-ephemeral-storage The ConfKeyManager class reads its key from the project's configuration file and provides this key for *all* requests. As such, this key manager is insecure but allows the aforementioned encryption features to be used without further integration effort. To clarify the above statements, the configuration-based key manager uses a single, fixed key. When used to encrypt data (e.g., by the Cinder volume encryption feature), the encryption provides limited protection for the confidentiality of data. For example, data cannot be read from a lost or stolen disk, and a volume's contents cannot be reconstructed if an attacker intercepts the iSCSI traffic between the compute and storage host. If the key is ever compromised, then any data encrypted with the key can be decrypted. This commit copies the ConfKeyManager class from Nova as well as synchronizing changes with the key manager interface in Nova. Implements blueprint encrypt-cinder-volumes DocImpact SecurityImpact Change-Id: I5cb06386410f46cabc490fa6af23272d1d2cb979 --- diff --git a/cinder/exception.py b/cinder/exception.py index 7c2bdddc9..cf4add631 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -637,3 +637,7 @@ class InvalidQoSSpecs(Invalid): class QoSSpecsInUse(CinderException): message = _("QoS Specs %(specs_id)s is still associated with entities.") + + +class KeyManagerError(CinderException): + msg_fmt = _("key manager error: %(reason)s") diff --git a/cinder/keymgr/__init__.py b/cinder/keymgr/__init__.py index 5540c738c..f87e9f5b5 100644 --- a/cinder/keymgr/__init__.py +++ b/cinder/keymgr/__init__.py @@ -17,22 +17,17 @@ from oslo.config import cfg from cinder.openstack.common import importutils -from cinder.openstack.common import log as logging keymgr_opts = [ - cfg.StrOpt('keymgr_api_class', - default='cinder.keymgr.' - 'not_implemented_key_mgr.NotImplementedKeyManager', + cfg.StrOpt('api_class', + default='cinder.keymgr.conf_key_mgr.ConfKeyManager', help='The full class name of the key manager API class'), ] CONF = cfg.CONF -CONF.register_opts(keymgr_opts) - -LOG = logging.getLogger(__name__) +CONF.register_opts(keymgr_opts, group='keymgr') def API(): - keymgr_api_class = CONF.keymgr_api_class - cls = importutils.import_class(keymgr_api_class) + cls = importutils.import_class(CONF.keymgr.api_class) return cls() diff --git a/cinder/keymgr/conf_key_mgr.py b/cinder/keymgr/conf_key_mgr.py new file mode 100644 index 000000000..7b53e0cb4 --- /dev/null +++ b/cinder/keymgr/conf_key_mgr.py @@ -0,0 +1,137 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +An implementation of a key manager that reads its key from the project's +configuration options. + +This key manager implementation provides limited security, assuming that the +key remains secret. Using the volume encryption feature as an example, +encryption provides protection against a lost or stolen disk, assuming that +the configuration file that contains the key is not stored on the disk. +Encryption also protects the confidentiality of data as it is transmitted via +iSCSI from the compute host to the storage host (again assuming that an +attacker who intercepts the data does not know the secret key). + +Because this implementation uses a single, fixed key, it proffers no +protection once that key is compromised. In particular, different volumes +encrypted with a key provided by this key manager actually share the same +encryption key so *any* volume can be decrypted once the fixed key is known. +""" + +import array + +from oslo.config import cfg + +from cinder import exception +from cinder.keymgr import key +from cinder.keymgr import key_mgr +from cinder.openstack.common.gettextutils import _ +from cinder.openstack.common import log as logging + + +key_mgr_opts = [ + cfg.StrOpt('fixed_key', + help='Fixed key returned by key manager, specified in hex'), +] + +CONF = cfg.CONF +CONF.register_opts(key_mgr_opts, group='keymgr') + + +LOG = logging.getLogger(__name__) + + +class ConfKeyManager(key_mgr.KeyManager): + """ + This key manager implementation supports all the methods specified by the + key manager interface. This implementation creates a single key in response + to all invocations of create_key. Side effects (e.g., raising exceptions) + for each method are handled as specified by the key manager interface. + """ + + def __init__(self): + LOG.warn(_('This key manager is insecure and is not recommended for ' + 'production deployments')) + super(ConfKeyManager, self).__init__() + + self.key_id = '00000000-0000-0000-0000-000000000000' + if CONF.keymgr.fixed_key is None: + LOG.warn(_('config option keymgr.fixed_key has not been defined: ' + 'some operations may fail unexpectedly')) + + def _generate_key(self, **kwargs): + _hex = self._generate_hex_key(**kwargs) + return key.SymmetricKey('AES', + array.array('B', _hex.decode('hex')).tolist()) + + def _generate_hex_key(self, **kwargs): + if CONF.keymgr.fixed_key is None: + raise ValueError(_('keymgr.fixed_key not defined')) + return CONF.keymgr.fixed_key + + def create_key(self, ctxt, **kwargs): + """Creates a key. + + This implementation returns a UUID for the created key. A + NotAuthorized exception is raised if the specified context is None. + """ + if ctxt is None: + raise exception.NotAuthorized() + + return self.key_id + + def store_key(self, ctxt, key, **kwargs): + """Stores (i.e., registers) a key with the key manager.""" + if ctxt is None: + raise exception.NotAuthorized() + + if key != self._generate_key(): + raise exception.KeyManagerError( + reason="cannot store arbitrary keys") + + return self.key_id + + def copy_key(self, ctxt, key_id, **kwargs): + if ctxt is None: + raise exception.NotAuthorized() + + return self.key_id + + def get_key(self, ctxt, key_id, **kwargs): + """Retrieves the key identified by the specified id. + + This implementation returns the key that is associated with the + specified UUID. A NotAuthorized exception is raised if the specified + context is None; a KeyError is raised if the UUID is invalid. + """ + if ctxt is None: + raise exception.NotAuthorized() + + if key_id != self.key_id: + raise KeyError(key_id) + + return self._generate_key() + + def delete_key(self, ctxt, key_id, **kwargs): + if ctxt is None: + raise exception.NotAuthorized() + + if key_id != self.key_id: + raise exception.KeyManagerError( + reason="cannot delete non-existent key") + + LOG.warn(_("Not deleting key %s"), key_id) diff --git a/cinder/keymgr/key.py b/cinder/keymgr/key.py index 3e3f13941..644cf34c4 100644 --- a/cinder/keymgr/key.py +++ b/cinder/keymgr/key.py @@ -79,3 +79,15 @@ class SymmetricKey(Key): def get_encoded(self): """Returns the key in its encoded format.""" return self.key + + def __eq__(self, other): + if isinstance(other, SymmetricKey): + return (self.alg == other.alg and + self.key == other.key) + return NotImplemented + + def __ne__(self, other): + result = self.__eq__(other) + if result is NotImplemented: + return result + return not result diff --git a/cinder/tests/conf_fixture.py b/cinder/tests/conf_fixture.py index 499f33263..dbf8d08c7 100644 --- a/cinder/tests/conf_fixture.py +++ b/cinder/tests/conf_fixture.py @@ -48,3 +48,6 @@ def set_defaults(conf): 'xiv_ds8k_proxy', 'cinder.tests.test_xiv_ds8k.XIVDS8KFakeProxyDriver') conf.set_default('backup_driver', 'cinder.tests.backup.fake_service') + # NOTE(joel-coffman): This option for the ConfKeyManager must be set or + # else the ConfKeyManager cannot be instantiated. + conf.set_default('fixed_key', default='0' * 64, group='keymgr') diff --git a/cinder/tests/keymgr/mock_key_mgr.py b/cinder/tests/keymgr/mock_key_mgr.py index 62b7c4a3d..9c5fd1d08 100644 --- a/cinder/tests/keymgr/mock_key_mgr.py +++ b/cinder/tests/keymgr/mock_key_mgr.py @@ -15,8 +15,16 @@ # under the License. """ -A mock implementation of a key manager. This module should NOT be used for -anything but integration testing. +A mock implementation of a key manager that stores keys in a dictionary. + +This key manager implementation is primarily intended for testing. In +particular, it does not store keys persistently. Lack of a centralized key +store also makes this implementation unsuitable for use among different +services. + +Note: Instantiating this class multiple times will create separate key stores. +Keys created in one instance will not be accessible from other instances of +this class. """ import array @@ -24,16 +32,11 @@ import array from cinder import exception from cinder.keymgr import key from cinder.keymgr import key_mgr -from cinder.openstack.common import log as logging from cinder.openstack.common import uuidutils from cinder import utils -LOG = logging.getLogger(__name__) - - class MockKeyManager(key_mgr.KeyManager): - """ This mock key manager implementation supports all the methods specified by the key manager interface. This implementation stores keys within a @@ -41,13 +44,24 @@ class MockKeyManager(key_mgr.KeyManager): services. Side effects (e.g., raising exceptions) for each method are handled as specified by the key manager interface. - This class should NOT be used for anything but integration testing because - keys are not stored persistently. + This key manager is not suitable for use in production deployments. """ def __init__(self): self.keys = {} + def _generate_hex_key(self, **kwargs): + key_length = kwargs.get('key_length', 256) + # hex digit => 4 bits + hex_encoded = utils.generate_password(length=key_length / 4, + symbolgroups='0123456789ABCDEF') + return hex_encoded + + def _generate_key(self, **kwargs): + _hex = self._generate_hex_key(**kwargs) + return key.SymmetricKey('AES', + array.array('B', _hex.decode('hex')).tolist()) + def create_key(self, ctxt, **kwargs): """Creates a key. @@ -57,16 +71,8 @@ class MockKeyManager(key_mgr.KeyManager): if ctxt is None: raise exception.NotAuthorized() - # generate the key - key_length = kwargs.get('key_length', 256) - # hex digit => 4 bits - hex_string = utils.generate_password(length=key_length / 4, - symbolgroups='0123456789ABCDEF') - - _bytes = array.array('B', hex_string.decode('hex')).tolist() - _key = key.SymmetricKey('AES', _bytes) - - return self.store_key(ctxt, _key) + key = self._generate_key(**kwargs) + return self.store_key(ctxt, key) def _generate_key_id(self): key_id = uuidutils.generate_uuid() @@ -76,8 +82,7 @@ class MockKeyManager(key_mgr.KeyManager): return key_id def store_key(self, ctxt, key, **kwargs): - """Stores (i.e., registers) a key with the key manager. - """ + """Stores (i.e., registers) a key with the key manager.""" if ctxt is None: raise exception.NotAuthorized() diff --git a/cinder/tests/keymgr/test_conf_key_mgr.py b/cinder/tests/keymgr/test_conf_key_mgr.py new file mode 100644 index 000000000..eb846aafa --- /dev/null +++ b/cinder/tests/keymgr/test_conf_key_mgr.py @@ -0,0 +1,124 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Test cases for the conf key manager. +""" + +import array + +from oslo.config import cfg + +from cinder import context +from cinder import exception +from cinder.keymgr import conf_key_mgr +from cinder.keymgr import key +from cinder.tests.keymgr import test_key_mgr + + +CONF = cfg.CONF +CONF.import_opt('fixed_key', 'cinder.keymgr.conf_key_mgr', group='keymgr') + + +class ConfKeyManagerTestCase(test_key_mgr.KeyManagerTestCase): + def __init__(self, *args, **kwargs): + super(ConfKeyManagerTestCase, self).__init__(*args, **kwargs) + + self._hex_key = '1' * 64 + + def _create_key_manager(self): + CONF.set_default('fixed_key', default=self._hex_key, group='keymgr') + return conf_key_mgr.ConfKeyManager() + + def setUp(self): + super(ConfKeyManagerTestCase, self).setUp() + + self.ctxt = context.RequestContext('fake', 'fake') + + self.key_id = '00000000-0000-0000-0000-000000000000' + encoded = array.array('B', self._hex_key.decode('hex')).tolist() + self.key = key.SymmetricKey('AES', encoded) + + def test___init__(self): + self.assertEqual(self.key_id, self.key_mgr.key_id) + + def test_create_key(self): + key_id_1 = self.key_mgr.create_key(self.ctxt) + key_id_2 = self.key_mgr.create_key(self.ctxt) + # ensure that the UUIDs are the same + self.assertEqual(key_id_1, key_id_2) + + def test_create_null_context(self): + self.assertRaises(exception.NotAuthorized, + self.key_mgr.create_key, None) + + def test_store_key(self): + key_id = self.key_mgr.store_key(self.ctxt, self.key) + + actual_key = self.key_mgr.get_key(self.ctxt, key_id) + self.assertEqual(self.key, actual_key) + + def test_store_null_context(self): + self.assertRaises(exception.NotAuthorized, + self.key_mgr.store_key, None, self.key) + + def test_store_key_invalid(self): + encoded = self.key.get_encoded() + inverse_key = key.SymmetricKey('AES', [~b for b in encoded]) + + self.assertRaises(exception.KeyManagerError, + self.key_mgr.store_key, self.ctxt, inverse_key) + + def test_copy_key(self): + key_id = self.key_mgr.create_key(self.ctxt) + key = self.key_mgr.get_key(self.ctxt, key_id) + + copied_key_id = self.key_mgr.copy_key(self.ctxt, key_id) + copied_key = self.key_mgr.get_key(self.ctxt, copied_key_id) + + self.assertEqual(key_id, copied_key_id) + self.assertEqual(key, copied_key) + + def test_copy_null_context(self): + self.assertRaises(exception.NotAuthorized, + self.key_mgr.copy_key, None, None) + + def test_delete_key(self): + key_id = self.key_mgr.create_key(self.ctxt) + self.key_mgr.delete_key(self.ctxt, key_id) + + # cannot delete key -- might have lingering references + self.assertEqual(self.key, + self.key_mgr.get_key(self.ctxt, self.key_id)) + + def test_delete_null_context(self): + self.assertRaises(exception.NotAuthorized, + self.key_mgr.delete_key, None, None) + + def test_delete_unknown_key(self): + self.assertRaises(exception.KeyManagerError, + self.key_mgr.delete_key, self.ctxt, None) + + def test_get_key(self): + self.assertEqual(self.key, + self.key_mgr.get_key(self.ctxt, self.key_id)) + + def test_get_null_context(self): + self.assertRaises(exception.NotAuthorized, + self.key_mgr.get_key, None, None) + + def test_get_unknown_key(self): + self.assertRaises(KeyError, self.key_mgr.get_key, self.ctxt, None) diff --git a/cinder/tests/keymgr/test_key.py b/cinder/tests/keymgr/test_key.py index 63dd4effe..135516492 100644 --- a/cinder/tests/keymgr/test_key.py +++ b/cinder/tests/keymgr/test_key.py @@ -55,3 +55,15 @@ class SymmetricKeyTestCase(KeyTestCase): def test_get_encoded(self): self.assertEqual(self.key.get_encoded(), self.encoded) + + def test___eq__(self): + self.assertTrue(self.key == self.key) + + self.assertFalse(self.key == None) + self.assertFalse(None == self.key) + + def test___ne__(self): + self.assertFalse(self.key != self.key) + + self.assertTrue(self.key != None) + self.assertTrue(None != self.key) diff --git a/cinder/tests/keymgr/test_key_mgr.py b/cinder/tests/keymgr/test_key_mgr.py index 72cf24f11..7179d4089 100644 --- a/cinder/tests/keymgr/test_key_mgr.py +++ b/cinder/tests/keymgr/test_key_mgr.py @@ -23,6 +23,8 @@ from cinder import test class KeyManagerTestCase(test.TestCase): + def __init__(self, *args, **kwargs): + super(KeyManagerTestCase, self).__init__(*args, **kwargs) def _create_key_manager(self): raise NotImplementedError() diff --git a/etc/cinder/cinder.conf.sample b/etc/cinder/cinder.conf.sample index 932bfdbaf..045feb002 100644 --- a/etc/cinder/cinder.conf.sample +++ b/etc/cinder/cinder.conf.sample @@ -532,7 +532,16 @@ # The full class name of the key manager API class (string # value) -#keymgr_api_class=cinder.keymgr.not_implemented_key_mgr.NotImplementedKeyManager +#api_class=cinder.keymgr.conf_key_mgr.ConfKeyManager + + +# +# Options defined in cinder.keymgr.conf_key_mgr +# + +# Fixed key returned by key manager, specified in hex (string +# value) +#fixed_key= # @@ -1774,4 +1783,4 @@ #volume_dd_blocksize=1M -# Total option count: 381 +# Total option count: 382