From eeb70eafa36383ffa02b36decd2f205395c20ed5 Mon Sep 17 00:00:00 2001 From: Joel Coffman Date: Tue, 30 Jul 2013 10:06:28 -0400 Subject: [PATCH] Create key manager interface 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 | 37 ++++++++ cinder/keymgr/key.py | 81 +++++++++++++++++ cinder/keymgr/key_mgr.py | 85 ++++++++++++++++++ cinder/tests/keymgr/__init__.py | 15 ++++ cinder/tests/keymgr/mock_key_mgr.py | 107 +++++++++++++++++++++++ cinder/tests/keymgr/test_key.py | 57 ++++++++++++ cinder/tests/keymgr/test_key_mgr.py | 33 +++++++ cinder/tests/keymgr/test_mock_key_mgr.py | 90 +++++++++++++++++++ 8 files changed, 505 insertions(+) create mode 100644 cinder/keymgr/__init__.py create mode 100644 cinder/keymgr/key.py create mode 100644 cinder/keymgr/key_mgr.py create mode 100644 cinder/tests/keymgr/__init__.py create mode 100644 cinder/tests/keymgr/mock_key_mgr.py create mode 100644 cinder/tests/keymgr/test_key.py create mode 100644 cinder/tests/keymgr/test_key_mgr.py create mode 100644 cinder/tests/keymgr/test_mock_key_mgr.py diff --git a/cinder/keymgr/__init__.py b/cinder/keymgr/__init__.py new file mode 100644 index 000000000..f8d2f0c29 --- /dev/null +++ b/cinder/keymgr/__init__.py @@ -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 index 000000000..3e3f13941 --- /dev/null +++ b/cinder/keymgr/key.py @@ -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 index 000000000..4d48eee19 --- /dev/null +++ b/cinder/keymgr/key_mgr.py @@ -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 index 000000000..33d5398ac --- /dev/null +++ b/cinder/tests/keymgr/__init__.py @@ -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 index 000000000..d74200911 --- /dev/null +++ b/cinder/tests/keymgr/mock_key_mgr.py @@ -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 index 000000000..1e78c16ef --- /dev/null +++ b/cinder/tests/keymgr/test_key.py @@ -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 index 000000000..72cf24f11 --- /dev/null +++ b/cinder/tests/keymgr/test_key_mgr.py @@ -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 index 000000000..586b159c1 --- /dev/null +++ b/cinder/tests/keymgr/test_mock_key_mgr.py @@ -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) -- 2.45.2