summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2012-01-25 00:00:35 +0000
committerGerrit Code Review <review@openstack.org>2012-01-25 00:00:35 +0000
commita99f429591b5efcbcc21a618190e4bef7d9fba38 (patch)
tree44af4b0a6db6b6b335ca974f00ec67e2bc624b47
parent5156c0e757859b0823ebd35e77e660e9f5726811 (diff)
parent0c5273c85ea72d60e5907acb22398584ded0a077 (diff)
Merge "Create nova cert worker for x509 support"
-rwxr-xr-xbin/nova-all2
-rwxr-xr-xbin/nova-cert47
-rw-r--r--nova/api/ec2/cloud.py25
-rw-r--r--nova/api/openstack/compute/contrib/cloudpipe.py23
-rw-r--r--nova/cert/__init__.py15
-rw-r--r--nova/cert/manager.py67
-rw-r--r--nova/crypto.py44
-rw-r--r--nova/exception.py4
-rw-r--r--nova/flags.py3
-rw-r--r--nova/image/s3.py67
-rw-r--r--nova/tests/test_auth.py23
-rw-r--r--nova/tests/test_crypto.py63
-rw-r--r--setup.py1
13 files changed, 271 insertions, 113 deletions
diff --git a/bin/nova-all b/bin/nova-all
index 9c9e2bbaa..806c2d002 100755
--- a/bin/nova-all
+++ b/bin/nova-all
@@ -65,7 +65,7 @@ if __name__ == '__main__':
except (Exception, SystemExit):
logging.exception(_('Failed to load %s') % 'objectstore-wsgi')
for binary in ['nova-xvpvncproxy', 'nova-compute', 'nova-volume',
- 'nova-network', 'nova-scheduler', 'nova-vsa']:
+ 'nova-network', 'nova-scheduler', 'nova-vsa', 'nova-cert']:
try:
servers.append(service.Service.create(binary=binary))
except (Exception, SystemExit):
diff --git a/bin/nova-cert b/bin/nova-cert
new file mode 100755
index 000000000..725bf4aee
--- /dev/null
+++ b/bin/nova-cert
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC.
+#
+# 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.
+
+"""Starter script for Nova Cert."""
+
+import eventlet
+eventlet.monkey_patch()
+
+import os
+import sys
+
+# If ../nova/__init__.py exists, add ../ to Python search path, so that
+# it will override what happens to be installed in /usr/(local/)lib/python...
+POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
+ os.pardir,
+ os.pardir))
+if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'nova', '__init__.py')):
+ sys.path.insert(0, POSSIBLE_TOPDIR)
+
+
+from nova import flags
+from nova import log as logging
+from nova import service
+from nova import utils
+
+if __name__ == '__main__':
+ utils.default_flagfile()
+ flags.FLAGS(sys.argv)
+ logging.setup()
+ utils.monkey_patch()
+ server = service.Service.create(binary='nova-cert')
+ service.serve(server)
+ service.wait()
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index eb115d8dc..5a3b952a9 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -205,35 +205,10 @@ class CloudController(object):
self.volume_api = volume.API()
self.compute_api = compute.API(network_api=self.network_api,
volume_api=self.volume_api)
- self.setup()
def __str__(self):
return 'CloudController'
- def setup(self):
- """ Ensure the keychains and folders exist. """
- # FIXME(ja): this should be moved to a nova-manage command,
- # if not setup throw exceptions instead of running
- # Create keys folder, if it doesn't exist
- if not os.path.exists(FLAGS.keys_path):
- os.makedirs(FLAGS.keys_path)
- # Gen root CA, if we don't have one
- root_ca_path = os.path.join(FLAGS.ca_path, FLAGS.ca_file)
- if not os.path.exists(root_ca_path):
- genrootca_sh_path = os.path.join(os.path.dirname(__file__),
- os.path.pardir,
- os.path.pardir,
- 'CA',
- 'genrootca.sh')
-
- start = os.getcwd()
- if not os.path.exists(FLAGS.ca_path):
- os.makedirs(FLAGS.ca_path)
- os.chdir(FLAGS.ca_path)
- # TODO(vish): Do this with M2Crypto instead
- utils.runthis(_("Generating root CA: %s"), "sh", genrootca_sh_path)
- os.chdir(start)
-
def _get_image_state(self, image):
# NOTE(vish): fallback status if image_state isn't set
state = image.get('status')
diff --git a/nova/api/openstack/compute/contrib/cloudpipe.py b/nova/api/openstack/compute/contrib/cloudpipe.py
index 1cf47a2a9..9d944366b 100644
--- a/nova/api/openstack/compute/contrib/cloudpipe.py
+++ b/nova/api/openstack/compute/contrib/cloudpipe.py
@@ -60,28 +60,11 @@ class CloudpipeController(object):
def setup(self):
"""Ensure the keychains and folders exist."""
- # TODO(todd): this was copyed from api.ec2.cloud
- # FIXME(ja): this should be moved to a nova-manage command,
- # if not setup throw exceptions instead of running
- # Create keys folder, if it doesn't exist
+ # NOTE(vish): One of the drawbacks of doing this in the api is
+ # the keys will only be on the api node that launched
+ # the cloudpipe.
if not os.path.exists(FLAGS.keys_path):
os.makedirs(FLAGS.keys_path)
- # Gen root CA, if we don't have one
- root_ca_path = os.path.join(FLAGS.ca_path, FLAGS.ca_file)
- if not os.path.exists(root_ca_path):
- genrootca_sh_path = os.path.join(os.path.dirname(__file__),
- os.path.pardir,
- os.path.pardir,
- 'CA',
- 'genrootca.sh')
-
- start = os.getcwd()
- if not os.path.exists(FLAGS.ca_path):
- os.makedirs(FLAGS.ca_path)
- os.chdir(FLAGS.ca_path)
- # TODO(vish): Do this with M2Crypto instead
- utils.runthis(_("Generating root CA: %s"), "sh", genrootca_sh_path)
- os.chdir(start)
def _get_cloudpipe_for_project(self, context, project_id):
"""Get the cloudpipe instance for a project ID."""
diff --git a/nova/cert/__init__.py b/nova/cert/__init__.py
new file mode 100644
index 000000000..74cec6938
--- /dev/null
+++ b/nova/cert/__init__.py
@@ -0,0 +1,15 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC.
+#
+# 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/cert/manager.py b/nova/cert/manager.py
new file mode 100644
index 000000000..ee16dc8a9
--- /dev/null
+++ b/nova/cert/manager.py
@@ -0,0 +1,67 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC.
+#
+# 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.
+"""
+Cert manager manages x509 certificates.
+
+**Related Flags**
+:cert_topic: What :mod:`rpc` topic to listen to (default: `cert`).
+:cert_manager: The module name of a class derived from
+ :class:`manager.Manager` (default:
+ :class:`nova.cert.manager.Manager`).
+"""
+
+import base64
+
+from nova import crypto
+from nova import flags
+from nova import log as logging
+from nova import manager
+
+LOG = logging.getLogger('nova.cert.manager')
+FLAGS = flags.FLAGS
+
+
+class CertManager(manager.Manager):
+ def init_host(self):
+ crypto.ensure_ca_filesystem()
+
+ def revoke_certs_by_user(self, context, user_id):
+ """Revoke all user certs."""
+ return crypto.revoke_certs_by_user(user_id)
+
+ def revoke_certs_by_project(self, context, project_id):
+ """Revoke all project certs."""
+ return crypto.revoke_certs_by_project(project_id)
+
+ def revoke_certs_by_user_and_project(self, context, user_id, project_id):
+ """Revoke certs for user in project."""
+ return crypto.revoke_certs_by_user_and_project(project_id)
+
+ def generate_x509_cert(self, context, user_id, project_id):
+ """Generate and sign a cert for user in project"""
+ return crypto.generate_x509_cert(user_id, project_id)
+
+ def fetch_ca(self, context, project_id):
+ """Get root ca for a project"""
+ return crypto.fetch_ca(project_id)
+
+ def fetch_crl(self, context, project_id):
+ """Get crl for a project"""
+ return crypto.fetch_ca(project_id)
+
+ def decrypt_text(self, context, project_id, text):
+ """Decrypt base64 encoded text using the projects private key."""
+ return crypto.decrypt_text(project_id, base64.b64decode(text))
diff --git a/nova/crypto.py b/nova/crypto.py
index 0ddc9e8e7..c0bb750f5 100644
--- a/nova/crypto.py
+++ b/nova/crypto.py
@@ -39,6 +39,7 @@ gettext.install('nova', unicode=1)
from nova import context
from nova import db
+from nova import exception
from nova import flags
from nova import log as logging
@@ -85,6 +86,10 @@ def key_path(project_id=None):
return os.path.join(ca_folder(project_id), FLAGS.key_file)
+def crl_path(project_id=None):
+ return os.path.join(ca_folder(project_id), FLAGS.crl_file)
+
+
def fetch_ca(project_id=None):
if not FLAGS.use_project_ca:
project_id = None
@@ -92,6 +97,22 @@ def fetch_ca(project_id=None):
return cafile.read()
+def ensure_ca_filesystem():
+ """Ensure the CA filesystem exists."""
+ ca_dir = ca_folder()
+ if not os.path.exists(ca_path()):
+ genrootca_sh_path = os.path.join(os.path.dirname(__file__),
+ 'CA',
+ 'genrootca.sh')
+
+ start = os.getcwd()
+ if not os.path.exists(ca_dir):
+ os.makedirs(ca_dir)
+ os.chdir(ca_dir)
+ utils.runthis(_("Generating root CA: %s"), "sh", genrootca_sh_path)
+ os.chdir(start)
+
+
def _generate_fingerprint(public_key_file):
(out, err) = utils.execute('ssh-keygen', '-q', '-l', '-f', public_key_file)
fingerprint = out.split(' ')[1]
@@ -148,6 +169,29 @@ def ssl_pub_to_ssh_pub(ssl_public_key, name='root', suffix='nova'):
return '%s %s %s@%s\n' % (key_type, b64_blob, name, suffix)
+def fetch_crl(project_id):
+ """Get crl file for project."""
+ if not FLAGS.use_project_ca:
+ project_id = None
+ with open(crl_path(project_id), 'r') as crlfile:
+ return crlfile.read()
+
+
+def decrypt_text(project_id, text):
+ private_key = key_path(project_id)
+ if not os.path.exists(private_key):
+ raise exception.ProjectNotFound(project_id=project_id)
+ try:
+ dec, _err = utils.execute('openssl',
+ 'rsautl',
+ '-decrypt',
+ '-inkey', '%s' % private_key,
+ process_input=text)
+ return dec
+ except exception.ProcessExecutionError:
+ raise exception.DecryptionFailure()
+
+
def revoke_cert(project_id, file_name):
"""Revoke a cert by file name."""
start = os.getcwd()
diff --git a/nova/exception.py b/nova/exception.py
index 8eda6bfc6..147834066 100644
--- a/nova/exception.py
+++ b/nova/exception.py
@@ -179,6 +179,10 @@ class NovaException(Exception):
super(NovaException, self).__init__(message)
+class DecryptionFailure(NovaException):
+ message = _("Failed to decrypt text")
+
+
class ImagePaginationFailed(NovaException):
message = _("Failed to paginate through images from image service")
diff --git a/nova/flags.py b/nova/flags.py
index 855ef8ed0..d535f783f 100644
--- a/nova/flags.py
+++ b/nova/flags.py
@@ -275,6 +275,7 @@ DEFINE_integer('glance_num_retries', 0,
DEFINE_integer('s3_port', 3333, 's3 port')
DEFINE_string('s3_host', '$my_ip', 's3 host (for infrastructure)')
DEFINE_string('s3_dmz', '$my_ip', 's3 dmz ip (for instances)')
+DEFINE_string('cert_topic', 'cert', 'the topic cert nodes listen on')
DEFINE_string('compute_topic', 'compute', 'the topic compute nodes listen on')
DEFINE_string('console_topic', 'console',
'the topic console proxy nodes listen on')
@@ -367,6 +368,8 @@ DEFINE_string('compute_manager', 'nova.compute.manager.ComputeManager',
'Manager for compute')
DEFINE_string('console_manager', 'nova.console.manager.ConsoleProxyManager',
'Manager for console proxy')
+DEFINE_string('cert_manager', 'nova.cert.manager.CertManager',
+ 'Manager for cert')
DEFINE_string('instance_dns_manager',
'nova.network.dns_driver.DNSDriver',
'DNS Manager for instance IPs')
diff --git a/nova/image/s3.py b/nova/image/s3.py
index fe320a406..e73364a42 100644
--- a/nova/image/s3.py
+++ b/nova/image/s3.py
@@ -18,6 +18,7 @@
"""Proxy AMI-related calls from cloud controller to objectstore service."""
+import base64
import binascii
import os
import shutil
@@ -28,7 +29,7 @@ from xml.etree import ElementTree
import boto.s3.connection
import eventlet
-from nova import crypto
+from nova import rpc
import nova.db.api
from nova import exception
from nova import flags
@@ -302,14 +303,9 @@ class S3ImageService(object):
hex_iv = manifest.find('image/ec2_encrypted_iv').text
encrypted_iv = binascii.a2b_hex(hex_iv)
- # FIXME(vish): grab key from common service so this can run on
- # any host.
- cloud_pk = crypto.key_path(context.project_id)
-
dec_filename = os.path.join(image_path, 'image.tar.gz')
- self._decrypt_image(enc_filename, encrypted_key,
- encrypted_iv, cloud_pk,
- dec_filename)
+ self._decrypt_image(context, enc_filename, encrypted_key,
+ encrypted_iv, dec_filename)
except Exception:
LOG.exception(_("Failed to decrypt %(image_location)s "
"to %(image_path)s"), log_vars)
@@ -353,39 +349,38 @@ class S3ImageService(object):
return image
@staticmethod
- def _decrypt_image(encrypted_filename, encrypted_key, encrypted_iv,
- cloud_private_key, decrypted_filename):
- key, err = utils.execute('openssl',
- 'rsautl',
- '-decrypt',
- '-inkey', '%s' % cloud_private_key,
- process_input=encrypted_key,
- check_exit_code=False)
- if err:
+ def _decrypt_image(context, encrypted_filename, encrypted_key,
+ encrypted_iv, decrypted_filename):
+ elevated = context.elevated()
+ try:
+ key = rpc.call(elevated, FLAGS.cert_topic,
+ {"method": "decrypt_text",
+ "args": {"project_id": context.project_id,
+ "text": base64.b64encode(encrypted_key)}})
+ except Exception, exc:
raise exception.Error(_('Failed to decrypt private key: %s')
- % err)
- iv, err = utils.execute('openssl',
- 'rsautl',
- '-decrypt',
- '-inkey', '%s' % cloud_private_key,
- process_input=encrypted_iv,
- check_exit_code=False)
- if err:
+ % exc)
+ try:
+ iv = rpc.call(elevated, FLAGS.cert_topic,
+ {"method": "decrypt_text",
+ "args": {"project_id": context.project_id,
+ "text": base64.b64encode(encrypted_iv)}})
+ except Exception, exc:
raise exception.Error(_('Failed to decrypt initialization '
- 'vector: %s') % err)
-
- _out, err = utils.execute('openssl', 'enc',
- '-d', '-aes-128-cbc',
- '-in', '%s' % (encrypted_filename,),
- '-K', '%s' % (key,),
- '-iv', '%s' % (iv,),
- '-out', '%s' % (decrypted_filename,),
- check_exit_code=False)
- if err:
+ 'vector: %s') % exc)
+
+ try:
+ utils.execute('openssl', 'enc',
+ '-d', '-aes-128-cbc',
+ '-in', '%s' % (encrypted_filename,),
+ '-K', '%s' % (key,),
+ '-iv', '%s' % (iv,),
+ '-out', '%s' % (decrypted_filename,))
+ except exception.ProcessExecutionError, exc:
raise exception.Error(_('Failed to decrypt image file '
'%(image_file)s: %(err)s') %
{'image_file': encrypted_filename,
- 'err': err})
+ 'err': exc.stdout})
@staticmethod
def _test_for_malicious_tarball(path, filename):
diff --git a/nova/tests/test_auth.py b/nova/tests/test_auth.py
index b1feb0856..bdc7f3142 100644
--- a/nova/tests/test_auth.py
+++ b/nova/tests/test_auth.py
@@ -16,7 +16,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-from M2Crypto import X509
import unittest
from nova import crypto
@@ -245,28 +244,6 @@ class _AuthManagerBaseTestCase(test.TestCase):
project))
self.assertFalse(self.manager.is_project_member(user, project))
- def test_can_generate_x509(self):
- # NOTE(todd): this doesn't assert against the auth manager
- # so it probably belongs in crypto_unittest
- # but I'm leaving it where I found it.
- with user_and_project_generator(self.manager) as (user, project):
- # NOTE(vish): Setup runs genroot.sh if it hasn't been run
- cloud.CloudController().setup()
- _key, cert_str = crypto.generate_x509_cert(user.id, project.id)
- LOG.debug(cert_str)
-
- int_cert = crypto.fetch_ca(project_id=project.id)
- cloud_cert = crypto.fetch_ca()
- signed_cert = X509.load_cert_string(cert_str)
- int_cert = X509.load_cert_string(int_cert)
- cloud_cert = X509.load_cert_string(cloud_cert)
- self.assertTrue(signed_cert.verify(int_cert.get_pubkey()))
-
- if not FLAGS.use_project_ca:
- self.assertTrue(signed_cert.verify(cloud_cert.get_pubkey()))
- else:
- self.assertFalse(signed_cert.verify(cloud_cert.get_pubkey()))
-
def test_adding_role_to_project_is_ignored_unless_added_to_user(self):
with user_and_project_generator(self.manager) as (user, project):
self.assertFalse(self.manager.has_role(user, 'sysadmin', project))
diff --git a/nova/tests/test_crypto.py b/nova/tests/test_crypto.py
index 6c25b396e..b5f70ea72 100644
--- a/nova/tests/test_crypto.py
+++ b/nova/tests/test_crypto.py
@@ -16,12 +16,20 @@
Tests for Crypto module.
"""
+import os
+import shutil
+import tempfile
+
import mox
-import stubout
+from M2Crypto import X509
from nova import crypto
from nova import db
+from nova import flags
from nova import test
+from nova import utils
+
+FLAGS = flags.FLAGS
class SymmetricKeyTestCase(test.TestCase):
@@ -52,15 +60,54 @@ class SymmetricKeyTestCase(test.TestCase):
self.assertEquals(plain_text, plain)
-class RevokeCertsTest(test.TestCase):
+class X509Test(test.TestCase):
+ def test_can_generate_x509(self):
+ tmpdir = tempfile.mkdtemp()
+ self.flags(ca_path=tmpdir)
+ try:
+ crypto.ensure_ca_filesystem()
+ _key, cert_str = crypto.generate_x509_cert('fake', 'fake')
+
+ project_cert = crypto.fetch_ca(project_id='fake')
+ cloud_cert = crypto.fetch_ca()
+ # TODO(vish): This will need to be replaced with something else
+ # when we remove M2Crypto
+ signed_cert = X509.load_cert_string(cert_str)
+ project_cert = X509.load_cert_string(project_cert)
+ cloud_cert = X509.load_cert_string(cloud_cert)
+ self.assertTrue(signed_cert.verify(project_cert.get_pubkey()))
+
+ if not FLAGS.use_project_ca:
+ self.assertTrue(signed_cert.verify(cloud_cert.get_pubkey()))
+ else:
+ self.assertFalse(signed_cert.verify(cloud_cert.get_pubkey()))
+ finally:
+ shutil.rmtree(tmpdir)
+
+ def test_encrypt_decrypt_x509(self):
+ tmpdir = tempfile.mkdtemp()
+ self.flags(ca_path=tmpdir)
+ project_id = "fake"
+ try:
+ crypto.ensure_ca_filesystem()
+ cert = crypto.fetch_ca(project_id)
+ public_key = os.path.join(tmpdir, "public.pem")
+ with open(public_key, 'w') as keyfile:
+ keyfile.write(cert)
+ text = "some @#!%^* test text"
+ enc, _err = utils.execute('openssl',
+ 'rsautl',
+ '-certin',
+ '-encrypt',
+ '-inkey', '%s' % public_key,
+ process_input=text)
+ dec = crypto.decrypt_text(project_id, enc)
+ self.assertEqual(text, dec)
+ finally:
+ shutil.rmtree(tmpdir)
- def setUp(self):
- super(RevokeCertsTest, self).setUp()
- self.stubs = stubout.StubOutForTesting()
- def tearDown(self):
- self.stubs.UnsetAll()
- super(RevokeCertsTest, self).tearDown()
+class RevokeCertsTest(test.TestCase):
def test_revoke_certs_by_user_and_project(self):
user_id = 'test_user'
diff --git a/setup.py b/setup.py
index b63bfde39..d98918968 100644
--- a/setup.py
+++ b/setup.py
@@ -89,6 +89,7 @@ setup(name='nova',
'bin/nova-api-metadata',
'bin/nova-api-os-compute',
'bin/nova-api-os-volume',
+ 'bin/nova-cert',
'bin/nova-compute',
'bin/nova-console',
'bin/nova-consoleauth',