]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add key manager implementation with static key
authorJoel Coffman <joel.coffman@jhuapl.edu>
Tue, 24 Sep 2013 23:10:09 +0000 (19:10 -0400)
committerJohn Griffith <john.griffith@solidfire.com>
Thu, 3 Oct 2013 03:48:29 +0000 (21:48 -0600)
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

cinder/exception.py
cinder/keymgr/__init__.py
cinder/keymgr/conf_key_mgr.py [new file with mode: 0644]
cinder/keymgr/key.py
cinder/tests/conf_fixture.py
cinder/tests/keymgr/mock_key_mgr.py
cinder/tests/keymgr/test_conf_key_mgr.py [new file with mode: 0644]
cinder/tests/keymgr/test_key.py
cinder/tests/keymgr/test_key_mgr.py
etc/cinder/cinder.conf.sample

index 7c2bdddc9c2a174a4437d18117fb06124164d968..cf4add63119ba2f696138540809764b2f59ee433 100644 (file)
@@ -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")
index 5540c738cf2a1b8698d6721e11ec6d234b47f06b..f87e9f5b535c804aa406f84ae4befa39e557c5f2 100644 (file)
 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 (file)
index 0000000..7b53e0c
--- /dev/null
@@ -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)
index 3e3f1394122c28a00804562de1c1a6b2236e79ca..644cf34c4d52079e9324a76ed6047108a2c143ad 100644 (file)
@@ -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
index 499f332630b74f8c15637e48733808ca1c59d335..dbf8d08c783b9b7ce2e4733c8f1129b4cf5d8be9 100644 (file)
@@ -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')
index 62b7c4a3d612d31127025feb5ce7e79362d65c0b..9c5fd1d088af5e107d200a9ad787c5e2e969ba0c 100644 (file)
 #    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 (file)
index 0000000..eb846aa
--- /dev/null
@@ -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)
index 63dd4effeebcf9b0ded9163fd605e3aa7dfa9e4b..1355164929a68ba5015117fa3196503cdce3d6f0 100644 (file)
@@ -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)
index 72cf24f11528c3c347e27acfb687e4b13d2f0ce6..7179d408917eac09fc33f25a850a34b2f166430c 100644 (file)
@@ -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()
index 932bfdbafb2220be324c978936d7ffa22f29d893..045feb002d58e7921b3714e0c668c1890503a156 100644 (file)
 
 # 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=<None>
 
 
 #
 #volume_dd_blocksize=1M
 
 
-# Total option count: 381
+# Total option count: 382