Merge "Adds x509 certificate keypair support"

This commit is contained in:
Jenkins 2015-03-06 01:46:47 +00:00 committed by Gerrit Code Review
commit b41b4e08ac
8 changed files with 157 additions and 11 deletions

View File

@ -35,6 +35,7 @@ from nova import context
from nova import network
from nova import objects
from nova.objects import base as obj_base
from nova.objects import keypair as keypair_obj
from nova import utils
from nova.virt import netutils
@ -309,6 +310,16 @@ class InstanceMetadata(object):
metadata['public_keys'] = {
self.instance.key_name: self.instance.key_data
}
keypair = keypair_obj.KeyPair.get_by_name(
context.get_admin_context(), self.instance.user_id,
self.instance.key_name)
metadata['keys'] = [
{'name': keypair.name,
'type': keypair.type,
'data': keypair.public_key}
]
metadata['hostname'] = self._get_hostname()
metadata['name'] = self.instance.display_name
metadata['launch_index'] = self.instance.launch_index

View File

@ -3731,7 +3731,8 @@ class KeypairAPI(base.Base):
notify.info(context, 'keypair.%s' % event_suffix, payload)
def _validate_new_key_pair(self, context, user_id, key_name, key_type):
if key_type is not keypair_obj.KEYPAIR_TYPE_SSH:
if key_type not in [keypair_obj.KEYPAIR_TYPE_SSH,
keypair_obj.KEYPAIR_TYPE_X509]:
raise exception.InvalidKeypair(
reason=_('Specified Keypair type "%s" is invalid') % key_type)
@ -3763,7 +3764,7 @@ class KeypairAPI(base.Base):
self._notify(context, 'import.start', key_name)
fingerprint = crypto.generate_fingerprint(public_key)
fingerprint = self._generate_fingerprint(public_key, key_type)
keypair = objects.KeyPair(context)
keypair.user_id = user_id
@ -3785,7 +3786,8 @@ class KeypairAPI(base.Base):
self._notify(context, 'create.start', key_name)
private_key, public_key, fingerprint = crypto.generate_key_pair()
private_key, public_key, fingerprint = self._generate_key_pair(
context, user_id, key_type)
keypair = objects.KeyPair(context)
keypair.user_id = user_id
@ -3799,6 +3801,18 @@ class KeypairAPI(base.Base):
return keypair, private_key
def _generate_fingerprint(self, public_key, key_type):
if key_type == keypair_obj.KEYPAIR_TYPE_SSH:
return crypto.generate_fingerprint(public_key)
elif key_type == keypair_obj.KEYPAIR_TYPE_X509:
return crypto.generate_x509_fingerprint(public_key)
def _generate_key_pair(self, context, user_id, key_type):
if key_type == keypair_obj.KEYPAIR_TYPE_SSH:
return crypto.generate_key_pair()
elif key_type == keypair_obj.KEYPAIR_TYPE_X509:
return crypto.generate_winrm_x509_cert(user_id, context.project_id)
@wrap_exception()
def delete_key_pair(self, context, user_id, key_name):
"""Delete a keypair by name."""

View File

@ -144,6 +144,19 @@ def generate_fingerprint(public_key):
reason=_('failed to generate fingerprint'))
def generate_x509_fingerprint(pem_key):
try:
(out, _err) = utils.execute('openssl', 'x509', '-inform', 'PEM',
'-fingerprint', '-noout',
process_input=pem_key)
fingerprint = string.strip(out.rpartition('=')[2])
return fingerprint
except processutils.ProcessExecutionError as ex:
raise exception.InvalidKeypair(
reason=_('failed to generate X509 fingerprint. '
'Error message: %s') % ex)
def generate_key_pair(bits=None):
with utils.tempdir() as tmpdir:
keyfile = os.path.join(tmpdir, 'temp')
@ -352,6 +365,44 @@ def generate_x509_cert(user_id, project_id, bits=2048):
return (private_key, signed_csr)
def generate_winrm_x509_cert(user_id, project_id, bits=2048):
"""Generate a cert for passwordless auth for user in project."""
subject = '/CN=%s-%s' % (project_id, user_id)
upn = '%s@localhost' % user_id
with utils.tempdir() as tmpdir:
keyfile = os.path.abspath(os.path.join(tmpdir, 'temp.key'))
conffile = os.path.abspath(os.path.join(tmpdir, 'temp.conf'))
_create_x509_openssl_config(conffile, upn)
(certificate, _err) = utils.execute(
'openssl', 'req', '-x509', '-nodes', '-days', '3650',
'-config', conffile, '-newkey', 'rsa:%s' % bits,
'-outform', 'PEM', '-keyout', keyfile, '-subj', subject,
'-extensions', 'v3_req_client')
(out, _err) = utils.execute('openssl', 'pkcs12', '-export',
'-inkey', keyfile, '-password', 'pass:',
process_input=certificate)
private_key = out.encode('base64')
fingerprint = generate_x509_fingerprint(certificate)
return (private_key, certificate, fingerprint)
def _create_x509_openssl_config(conffile, upn):
content = ("distinguished_name = req_distinguished_name\n"
"[req_distinguished_name]\n"
"[v3_req_client]\n"
"extendedKeyUsage = clientAuth\n"
"subjectAltName = otherName:""1.3.6.1.4.1.311.20.2.3;UTF8:%s\n")
with open(conffile, 'w') as file:
file.write(content % upn)
def _ensure_project_folder(project_id):
if not os.path.exists(ca_path(project_id)):
geninter_sh_path = os.path.abspath(

View File

@ -20,6 +20,7 @@ from nova.objects import fields
from nova import utils
KEYPAIR_TYPE_SSH = 'ssh'
KEYPAIR_TYPE_X509 = 'x509'
# TODO(berrange): Remove NovaObjectDictCompat

View File

@ -123,15 +123,15 @@ def wsgi_app_v21(inner_app_v21=None, fake_auth_context=None,
return mapper
def stub_out_key_pair_funcs(stubs, have_key_pair=True):
def stub_out_key_pair_funcs(stubs, have_key_pair=True, **kwargs):
def key_pair(context, user_id):
return [dict(test_keypair.fake_keypair,
name='key', public_key='public_key')]
name='key', public_key='public_key', **kwargs)]
def one_key_pair(context, user_id, name):
if name == 'key':
return dict(test_keypair.fake_keypair,
name='key', public_key='public_key')
name='key', public_key='public_key', **kwargs)
else:
raise exc.KeypairNotFound(user_id=user_id, name=name)

View File

@ -24,6 +24,7 @@ from nova import exception
from nova.objects import keypair as keypair_obj
from nova import quota
from nova.tests.unit.compute import test_compute
from nova.tests.unit import fake_crypto
from nova.tests.unit import fake_notifier
from nova.tests.unit.objects import test_keypair
@ -169,29 +170,48 @@ class CreateImportSharedTestMixIn(object):
class CreateKeypairTestCase(KeypairAPITestCase, CreateImportSharedTestMixIn):
func_name = 'create_key_pair'
def test_success(self):
def _check_success(self):
keypair, private_key = self.keypair_api.create_key_pair(
self.ctxt, self.ctxt.user_id, 'foo')
self.ctxt, self.ctxt.user_id, 'foo', key_type=self.keypair_type)
self.assertEqual('foo', keypair['name'])
self.assertEqual(self.keypair_type, keypair['type'])
self._check_notifications()
def test_success_ssh(self):
self._check_success()
def test_success_x509(self):
self.keypair_type = keypair_obj.KEYPAIR_TYPE_X509
self._check_success()
class ImportKeypairTestCase(KeypairAPITestCase, CreateImportSharedTestMixIn):
func_name = 'import_key_pair'
def test_success(self):
def _check_success(self):
keypair = self.keypair_api.import_key_pair(self.ctxt,
self.ctxt.user_id,
'foo',
self.pub_key)
self.pub_key,
self.keypair_type)
self.assertEqual('foo', keypair['name'])
self.assertEqual(self.keypair_type, keypair['type'])
self.assertEqual(self.fingerprint, keypair['fingerprint'])
self.assertEqual(self.pub_key, keypair['public_key'])
self.assertEqual(self.keypair_type, keypair['type'])
self._check_notifications(action='import')
def test_success_ssh(self):
self._check_success()
def test_success_x509(self):
self.keypair_type = keypair_obj.KEYPAIR_TYPE_X509
certif, fingerprint = fake_crypto.get_x509_cert_and_fingerprint()
self.pub_key = certif
self.fingerprint = fingerprint
self._check_success()
def test_bad_key_data(self):
exc = self.assertRaises(exception.InvalidKeypair,
self.keypair_api.import_key_pair,

View File

@ -107,3 +107,28 @@ YZhQPOYoNPEOYru116HdHzjGDVifgWf/nDL8Un5tjJFDSf7jSLtA
-----END CERTIFICATE-----
"""
return pk, csr
def get_x509_cert_and_fingerprint():
fingerprint = "A1:6F:6D:EA:A6:36:D0:3A:C6:EB:B6:EE:07:94:3E:2A:90:98:2B:C9"
certif = (
"-----BEGIN CERTIFICATE-----\n"
"MIIDIjCCAgqgAwIBAgIJAIE8EtWfZhhFMA0GCSqGSIb3DQEBCwUAMCQxIjAgBgNV\n"
"BAMTGWNsb3VkYmFzZS1pbml0LXVzZXItMTM1NTkwHhcNMTUwMTI5MTgyMzE4WhcN\n"
"MjUwMTI2MTgyMzE4WjAkMSIwIAYDVQQDExljbG91ZGJhc2UtaW5pdC11c2VyLTEz\n"
"NTU5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv4lv95ofkXLIbALU\n"
"UEb1f949TYNMUvMGNnLyLgGOY+D61TNG7RZn85cRg9GVJ7KDjSLN3e3LwH5rgv5q\n"
"pU+nM/idSMhG0CQ1lZeExTsMEJVT3bG7LoU5uJ2fJSf5+hA0oih2M7/Kap5ggHgF\n"
"h+h8MWvDC9Ih8x1aadkk/OEmJsTrziYm0C/V/FXPHEuXfZn8uDNKZ/tbyfI6hwEj\n"
"nLz5Zjgg29n6tIPYMrnLNDHScCwtNZOcnixmWzsxCt1bxsAEA/y9gXUT7xWUf52t\n"
"2+DGQbLYxo0PHjnPf3YnFXNavfTt+4c7ZdHhOQ6ZA8FGQ2LJHDHM1r2/8lK4ld2V\n"
"qgNTcQIDAQABo1cwVTATBgNVHSUEDDAKBggrBgEFBQcDAjA+BgNVHREENzA1oDMG\n"
"CisGAQQBgjcUAgOgJQwjY2xvdWRiYXNlLWluaXQtdXNlci0xMzU1OUBsb2NhbGhv\n"
"c3QwDQYJKoZIhvcNAQELBQADggEBAHHX/ZUOMR0ZggQnfXuXLIHWlffVxxLOV/bE\n"
"7JC/dtedHqi9iw6sRT5R6G1pJo0xKWr2yJVDH6nC7pfxCFkby0WgVuTjiu6iNRg2\n"
"4zNJd8TGrTU+Mst+PPJFgsxrAY6vjwiaUtvZ/k8PsphHXu4ON+oLurtVDVgog7Vm\n"
"fQCShx434OeJj1u8pb7o2WyYS5nDVrHBhlCAqVf2JPKu9zY+i9gOG2kimJwH7fJD\n"
"xXpMIwAQ+flwlHR7OrE0L8TNcWwKPRAY4EPcXrT+cWo1k6aTqZDSK54ygW2iWtni\n"
"ZBcstxwcB4GIwnp1DrPW9L2gw5eLe1Sl6wdz443TW8K/KPV9rWQ=\n"
"-----END CERTIFICATE-----\n")
return certif, fingerprint

View File

@ -45,6 +45,7 @@ from nova.network import api as network_api
from nova.network import model as network_model
from nova import objects
from nova import test
from nova.tests.unit.api.openstack import fakes
from nova.tests.unit import fake_block_device
from nova.tests.unit import fake_network
from nova.tests.unit.objects import test_security_group
@ -60,9 +61,10 @@ def fake_inst_obj(context):
inst = objects.Instance(
context=context,
id=1,
user_id='fake_user',
uuid='b65cee2f-8c69-4aeb-be2f-f79742548fc2',
project_id='test',
key_name="mykey",
key_name="key",
key_data="ssh-rsa AAAAB3Nzai....N3NtHw== someuser@somehost",
host='test',
launch_index=1,
@ -300,6 +302,7 @@ class MetadataTestCase(test.TestCase):
network_info=network_info)
def test_InstanceMetadata_invoke_metadata_for_config_drive(self):
fakes.stub_out_key_pair_funcs(self.stubs)
inst = self.instance.obj_clone()
inst_md = base.InstanceMetadata(inst)
for (path, value) in inst_md.metadata_for_config_drive():
@ -396,6 +399,7 @@ class OpenStackMetadataTestCase(test.TestCase):
grizzly_supported_apis)
def test_metadata_json(self):
fakes.stub_out_key_pair_funcs(self.stubs)
inst = self.instance.obj_clone()
content = [
('/etc/my.conf', "content of my.conf"),
@ -429,8 +433,25 @@ class OpenStackMetadataTestCase(test.TestCase):
found = mdinst.lookup("/openstack%s" % fent['content_path'])
self.assertEqual(found, content)
def test_x509_keypair(self):
# check if the x509 content is set, if the keypair type is x509.
fakes.stub_out_key_pair_funcs(self.stubs, type='x509')
inst = self.instance.obj_clone()
mdinst = fake_InstanceMetadata(self.stubs, inst)
mdjson = mdinst.lookup("/openstack/2012-08-10/meta_data.json")
mddict = jsonutils.loads(mdjson)
# keypair is stubbed-out, so it's public_key is 'public_key'.
expected = {'name': self.instance['key_name'],
'type': 'x509',
'data': 'public_key'}
self.assertEqual([expected], mddict['keys'])
def test_extra_md(self):
# make sure extra_md makes it through to metadata
fakes.stub_out_key_pair_funcs(self.stubs)
inst = self.instance.obj_clone()
extra = {'foo': 'bar', 'mylist': [1, 2, 3],
'mydict': {"one": 1, "two": 2}}
@ -470,6 +491,7 @@ class OpenStackMetadataTestCase(test.TestCase):
mdinst.lookup, "/openstack/2012-08-10/user_data")
def test_random_seed(self):
fakes.stub_out_key_pair_funcs(self.stubs)
inst = self.instance.obj_clone()
mdinst = fake_InstanceMetadata(self.stubs, inst)
@ -486,6 +508,7 @@ class OpenStackMetadataTestCase(test.TestCase):
def test_no_dashes_in_metadata(self):
# top level entries in meta_data should not contain '-' in their name
fakes.stub_out_key_pair_funcs(self.stubs)
inst = self.instance.obj_clone()
mdinst = fake_InstanceMetadata(self.stubs, inst)
mdjson = jsonutils.loads(
@ -592,6 +615,7 @@ class MetadataHandlerTestCase(test.TestCase):
self.assertEqual(response.status_int, 404)
def test_json_data(self):
fakes.stub_out_key_pair_funcs(self.stubs)
response = fake_request(self.stubs, self.mdinst,
"/openstack/latest/meta_data.json")
response_ctype = response.headers['Content-Type']