]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Create key manager interface
authorJoel Coffman <joel.coffman@jhuapl.edu>
Tue, 30 Jul 2013 14:06:28 +0000 (10:06 -0400)
committerJoel Coffman <joel.coffman@jhuapl.edu>
Tue, 6 Aug 2013 13:34:10 +0000 (09:34 -0400)
This interface provides a thin wrapper around an underlying key
management implementation such as Barbican or a KMIP server. The key
manager interface is used by the volume encryption code to retrieve
keys for volumes.

This change is a copy-paste of the key manager interface accepted by
Nova. The major modifications are to the module imports (i.e.,
nova -> cinder).

Change-Id: I8f79165d66d67ad8faaca60946959c5a61811c59
Implements: blueprint encrypt-cinder-volumes

cinder/keymgr/__init__.py [new file with mode: 0644]
cinder/keymgr/key.py [new file with mode: 0644]
cinder/keymgr/key_mgr.py [new file with mode: 0644]
cinder/tests/keymgr/__init__.py [new file with mode: 0644]
cinder/tests/keymgr/mock_key_mgr.py [new file with mode: 0644]
cinder/tests/keymgr/test_key.py [new file with mode: 0644]
cinder/tests/keymgr/test_key_mgr.py [new file with mode: 0644]
cinder/tests/keymgr/test_mock_key_mgr.py [new file with mode: 0644]

diff --git a/cinder/keymgr/__init__.py b/cinder/keymgr/__init__.py
new file mode 100644 (file)
index 0000000..f8d2f0c
--- /dev/null
@@ -0,0 +1,37 @@
+# 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.
+
+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.key_mgr.KeyManager',
+               help='The full class name of the key manager API class'),
+]
+
+CONF = cfg.CONF
+CONF.register_opts(keymgr_opts)
+
+LOG = logging.getLogger(__name__)
+
+
+def API():
+    keymgr_api_class = CONF.keymgr_api_class
+    cls = importutils.import_class(keymgr_api_class)
+    return cls()
diff --git a/cinder/keymgr/key.py b/cinder/keymgr/key.py
new file mode 100644 (file)
index 0000000..3e3f139
--- /dev/null
@@ -0,0 +1,81 @@
+# 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.
+
+"""
+Base Key and SymmetricKey Classes
+
+This module defines the Key and SymmetricKey classes. The Key class is the base
+class to represent all encryption keys. The basis for this class was copied
+from Java.
+"""
+
+import abc
+
+
+class Key(object):
+    """Base class to represent all keys."""
+
+    __metaclass__ = abc.ABCMeta
+
+    @abc.abstractmethod
+    def get_algorithm(self):
+        """Returns the key's algorithm.
+
+        Returns the key's algorithm. For example, "DSA" indicates that this key
+        is a DSA key and "AES" indicates that this key is an AES key.
+        """
+        pass
+
+    @abc.abstractmethod
+    def get_format(self):
+        """Returns the encoding format.
+
+        Returns the key's encoding format or None if this key is not encoded.
+        """
+        pass
+
+    @abc.abstractmethod
+    def get_encoded(self):
+        """Returns the key in the format specified by its encoding."""
+        pass
+
+
+class SymmetricKey(Key):
+    """
+    This class represents symmetric keys
+    """
+
+    def __init__(self, alg, key):
+        """Create a new SymmetricKey object.
+
+        The arguments specify the algorithm for the symmetric encryption and
+        the bytes for the key.
+        """
+        self.alg = alg
+        self.key = key
+
+    def get_algorithm(self):
+        """Returns the algorithm for symmetric encryption."""
+        return self.alg
+
+    def get_format(self):
+        """This method returns 'RAW'."""
+        return "RAW"
+
+    def get_encoded(self):
+        """Returns the key in its encoded format."""
+        return self.key
diff --git a/cinder/keymgr/key_mgr.py b/cinder/keymgr/key_mgr.py
new file mode 100644 (file)
index 0000000..4d48eee
--- /dev/null
@@ -0,0 +1,85 @@
+# 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.
+
+"""
+Key manager API
+"""
+
+import abc
+
+
+class KeyManager(object):
+    """Base Key Manager Interface
+
+    A Key Manager is responsible for managing encryption keys for volumes. A
+    Key Manager is responsible for creating, reading, and deleting keys.
+    """
+
+    __metaclass__ = abc.ABCMeta
+
+    @abc.abstractmethod
+    def create_key(self, ctxt, algorithm='AES', length=256, expiration=None,
+                   **kwargs):
+        """Creates a key.
+
+        This method creates a key and returns the key's UUID. If the specified
+        context does not permit the creation of keys, then a NotAuthorized
+        exception should be raised.
+        """
+        pass
+
+    @abc.abstractmethod
+    def store_key(self, ctxt, key, expiration=None, **kwargs):
+        """Stores (i.e., registers) a key with the key manager.
+
+        This method stores the specified key and returns its UUID that
+        identifies it within the key manager. If the specified context does
+        not permit the creation of keys, then a NotAuthorized exception should
+        be raised.
+        """
+        pass
+
+    @abc.abstractmethod
+    def get_key(self, ctxt, key_id, **kwargs):
+        """Retrieves the specified key.
+
+        Implementations should verify that the caller has permissions to
+        retrieve the key by checking the context object passed in as ctxt. If
+        the user lacks permission then a NotAuthorized exception is raised.
+
+        If the specified key does not exist, then a KeyError should be raised.
+        Implementations should preclude users from discerning the UUIDs of
+        keys that belong to other users by repeatedly calling this method.
+        That is, keys that belong to other users should be considered "non-
+        existent" and completely invisible.
+        """
+        pass
+
+    @abc.abstractmethod
+    def delete_key(self, ctxt, key_id, **kwargs):
+        """Deletes the specified key.
+
+        Implementations should verify that the caller has permission to delete
+        the key by checking the context object (ctxt). A NotAuthorized
+        exception should be raised if the caller lacks permission.
+
+        If the specified key does not exist, then a KeyError should be raised.
+        Implementations should preclude users from discerning the UUIDs of
+        keys that belong to other users by repeatedly calling this method.
+        That is, keys that belong to other users should be considered "non-
+        existent" and completely invisible.
+        """
+        pass
diff --git a/cinder/tests/keymgr/__init__.py b/cinder/tests/keymgr/__init__.py
new file mode 100644 (file)
index 0000000..33d5398
--- /dev/null
@@ -0,0 +1,15 @@
+# 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.
diff --git a/cinder/tests/keymgr/mock_key_mgr.py b/cinder/tests/keymgr/mock_key_mgr.py
new file mode 100644 (file)
index 0000000..d742009
--- /dev/null
@@ -0,0 +1,107 @@
+# 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.
+
+"""
+A mock implementation of a key manager. This module should NOT be used for
+anything but integration testing.
+"""
+
+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
+    dictionary, and as a result, it is not acceptable for use across different
+    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.
+    """
+
+    def __init__(self):
+        self.keys = {}
+
+    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()
+
+        # 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)
+
+    def store_key(self, ctxt, key, **kwargs):
+        """Stores (i.e., registers) a key with the key manager.
+        """
+        if ctxt is None:
+            raise exception.NotAuthorized()
+
+        # generate UUID and ensure that it isn't in use
+        key_id = uuidutils.generate_uuid()
+        while key_id in self.keys:
+            key_id = uuidutils.generate_uuid()
+
+        self.keys[key_id] = key
+
+        return 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()
+
+        return self.keys[key_id]
+
+    def delete_key(self, ctxt, key_id, **kwargs):
+        """Deletes the key identified by the specified id.
+
+        A NotAuthorized exception is raised if the context is None and a
+        KeyError is raised if the UUID is invalid.
+        """
+        if ctxt is None:
+            raise exception.NotAuthorized()
+
+        del self.keys[key_id]
diff --git a/cinder/tests/keymgr/test_key.py b/cinder/tests/keymgr/test_key.py
new file mode 100644 (file)
index 0000000..1e78c16
--- /dev/null
@@ -0,0 +1,57 @@
+# 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 key classes.
+"""
+
+import array
+
+from cinder.keymgr import key
+from cinder import test
+
+
+class KeyTestCase(test.TestCase):
+
+    def _create_key(self):
+        raise NotImplementedError()
+
+    def setUp(self):
+        super(KeyTestCase, self).setUp()
+
+        self.key = self._create_key()
+
+
+class SymmetricKeyTestCase(KeyTestCase):
+
+    def _create_key(self):
+        return key.SymmetricKey(self.algorithm, self.encoded)
+
+    def setUp(self):
+        self.algorithm = 'AES'
+        self.encoded = array.array('B', ('0' * 64).decode('hex')).tolist()
+
+        super(SymmetricKeyTestCase, self).setUp()
+
+    def test_get_algorithm(self):
+        self.assertEquals(self.key.get_algorithm(), self.algorithm)
+
+    def test_get_format(self):
+        self.assertEquals(self.key.get_format(), 'RAW')
+
+    def test_get_encoded(self):
+        self.assertEquals(self.key.get_encoded(), self.encoded)
diff --git a/cinder/tests/keymgr/test_key_mgr.py b/cinder/tests/keymgr/test_key_mgr.py
new file mode 100644 (file)
index 0000000..72cf24f
--- /dev/null
@@ -0,0 +1,33 @@
+# 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 key manager.
+"""
+
+from cinder import test
+
+
+class KeyManagerTestCase(test.TestCase):
+
+    def _create_key_manager(self):
+        raise NotImplementedError()
+
+    def setUp(self):
+        super(KeyManagerTestCase, self).setUp()
+
+        self.key_mgr = self._create_key_manager()
diff --git a/cinder/tests/keymgr/test_mock_key_mgr.py b/cinder/tests/keymgr/test_mock_key_mgr.py
new file mode 100644 (file)
index 0000000..586b159
--- /dev/null
@@ -0,0 +1,90 @@
+# 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 mock key manager.
+"""
+
+import array
+
+from cinder import context
+from cinder import exception
+from cinder.keymgr import key
+from cinder.tests.keymgr import mock_key_mgr
+from cinder.tests.keymgr import test_key_mgr
+
+
+class MockKeyManagerTestCase(test_key_mgr.KeyManagerTestCase):
+
+    def _create_key_manager(self):
+        return mock_key_mgr.MockKeyManager()
+
+    def setUp(self):
+        super(MockKeyManagerTestCase, self).setUp()
+
+        self.ctxt = context.RequestContext('fake', 'fake')
+
+    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 unique
+        self.assertNotEqual(key_id_1, key_id_2)
+
+    def test_create_key_with_length(self):
+        for length in [64, 128, 256]:
+            key_id = self.key_mgr.create_key(self.ctxt, key_length=length)
+            key = self.key_mgr.get_key(self.ctxt, key_id)
+            self.assertEqual(length / 8, len(key.get_encoded()))
+
+    def test_create_null_context(self):
+        self.assertRaises(exception.NotAuthorized,
+                          self.key_mgr.create_key, None)
+
+    def test_store_key(self):
+        _key = key.SymmetricKey('AES',
+                                array.array('B',
+                                            ('0' * 64).decode('hex')).tolist())
+        key_id = self.key_mgr.store_key(self.ctxt, _key)
+
+        actual_key = self.key_mgr.get_key(self.ctxt, key_id)
+        self.assertEqual(_key, actual_key)
+
+    def test_store_null_context(self):
+        self.assertRaises(exception.NotAuthorized,
+                          self.key_mgr.store_key, None, None)
+
+    def test_get_key(self):
+        pass
+
+    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)
+
+    def test_delete_key(self):
+        key_id = self.key_mgr.create_key(self.ctxt)
+        self.key_mgr.delete_key(self.ctxt, key_id)
+
+        self.assertRaises(KeyError, self.key_mgr.get_key, self.ctxt, 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(KeyError, self.key_mgr.delete_key, self.ctxt, None)