diff --git a/cinder/exception.py b/cinder/exception.py index 7c2bdddc9c2..cf4add63119 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 5540c738cf2..f87e9f5b535 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 00000000000..7b53e0cb49c --- /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 3e3f1394122..644cf34c4d5 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 499f332630b..dbf8d08c783 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 62b7c4a3d61..9c5fd1d088a 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 00000000000..eb846aafa7e --- /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 63dd4effeeb..1355164929a 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 72cf24f1152..7179d408917 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 5b2dcae6a56..ff2bba96325 100644 --- a/etc/cinder/cinder.conf.sample +++ b/etc/cinder/cinder.conf.sample @@ -509,7 +509,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= # @@ -1769,4 +1778,4 @@ #volume_dd_blocksize=1M -# Total option count: 381 +# Total option count: 382