diff --git a/nova/keymgr/__init__.py b/nova/keymgr/__init__.py new file mode 100644 index 000000000000..c6e1b5011403 --- /dev/null +++ b/nova/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 nova.openstack.common import importutils +from nova.openstack.common import log as logging + +keymgr_opts = [ + cfg.StrOpt('keymgr_api_class', + default='nova.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/nova/keymgr/key.py b/nova/keymgr/key.py new file mode 100644 index 000000000000..fc7255e252cc --- /dev/null +++ b/nova/keymgr/key.py @@ -0,0 +1,76 @@ +# 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 this key's algorithm. For example, "DSA" would indicate + that this key is a DSA key. + """ + pass + + @abc.abstractmethod + def get_format(self): + """Returns the encoding format of this key 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. This specifies 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 returns 'RAW'.""" + return "RAW" + + def get_encoded(self): + """Returns the key in its encoded format.""" + return self.key diff --git a/nova/keymgr/key_mgr.py b/nova/keymgr/key_mgr.py new file mode 100644 index 000000000000..4d48eee196e5 --- /dev/null +++ b/nova/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/nova/tests/keymgr/__init__.py b/nova/tests/keymgr/__init__.py new file mode 100644 index 000000000000..33d5398ac89b --- /dev/null +++ b/nova/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/nova/tests/keymgr/mock_key_mgr.py b/nova/tests/keymgr/mock_key_mgr.py new file mode 100644 index 000000000000..04eb3e9fdc51 --- /dev/null +++ b/nova/tests/keymgr/mock_key_mgr.py @@ -0,0 +1,111 @@ +# 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 +import uuid + +from nova import exception +from nova.keymgr import key +from nova.keymgr import key_mgr +from nova.openstack.common import log as logging +from nova 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 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. + + This class should NOT be used for anything but integration testing because + the same key is created for all invocations of create_key and 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. + + This implementation does nothing -- i.e., the specified key is + discarded. + """ + if ctxt is None: + raise exception.NotAuthorized() + + # generate UUID and ensure that it isn't in use + key_id = uuid.uuid4() + while key_id in self.keys: + key_id = uuid.uuid4() + + 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 a fixed key that is associated with the + UUID returned by the create_key method. 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. + + This implementation intentionally does nothing except raise a + NotAuthorized exception is the context is None or a KeyError if the + UUID is invalid. + """ + if ctxt is None: + raise exception.NotAuthorized() + + del self.keys[key_id] diff --git a/nova/tests/keymgr/test_key.py b/nova/tests/keymgr/test_key.py new file mode 100644 index 000000000000..d0d6eea5a61f --- /dev/null +++ b/nova/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 nova.keymgr import key +from nova 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/nova/tests/keymgr/test_key_mgr.py b/nova/tests/keymgr/test_key_mgr.py new file mode 100644 index 000000000000..0bbf862cad2d --- /dev/null +++ b/nova/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 nova 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/nova/tests/keymgr/test_mock_key_mgr.py b/nova/tests/keymgr/test_mock_key_mgr.py new file mode 100644 index 000000000000..de965644c380 --- /dev/null +++ b/nova/tests/keymgr/test_mock_key_mgr.py @@ -0,0 +1,89 @@ +# 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 nova import context +from nova import exception +from nova.keymgr import key +from nova.tests.keymgr import mock_key_mgr +from nova.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)