From af83b082163ea340219a11b65068708b58430d3c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 16 Sep 2022 11:37:23 +0100 Subject: [PATCH] Start generating our own key pairs Nova API microversion 2.92 removed the ability to generate a private key. The user or client is now responsible for generating the key pair. Start doing that using cryptography, which is in our requirements (unlike paramiko, which nova uses). included: https://review.opendev.org/c/openstack/ec2-api/+/857880 https://review.opendev.org/c/openstack/ec2-api/+/859192 Change-Id: I0032de8cd779beafbd6848a2aecbcb6455e8eada Signed-off-by: Stephen Finucane --- .zuul.yaml | 1 - ec2api/api/key_pair.py | 28 ++++++++++++++++++++++++++-- ec2api/db/sqlalchemy/api.py | 6 +++++- ec2api/tests/unit/test_key_pair.py | 9 +++++++-- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 98cb9825..92501a7c 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -7,6 +7,5 @@ jobs: - ec2api-tempest-plugin-functional gate: - queue: ec2-api jobs: - ec2api-tempest-plugin-functional diff --git a/ec2api/api/key_pair.py b/ec2api/api/key_pair.py index ade5660c..d95d1559 100644 --- a/ec2api/api/key_pair.py +++ b/ec2api/api/key_pair.py @@ -14,6 +14,9 @@ import base64 +from cryptography.hazmat import backends +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives import serialization as crypt_serialization from novaclient import exceptions as nova_exception from oslo_config import cfg from oslo_log import log as logging @@ -81,17 +84,38 @@ def _validate_name(name): reason='lenght is exceeds maximum of 255') +# We may wish to make the algorithm configurable. This would require API +# changes. +def _generate_key_pair(): + key = rsa.generate_private_key( + backend=backends.default_backend(), + public_exponent=65537, + key_size=2048 + ) + private_key = key.private_bytes( + crypt_serialization.Encoding.PEM, + crypt_serialization.PrivateFormat.TraditionalOpenSSL, + crypt_serialization.NoEncryption(), + ).decode() + public_key = key.public_key().public_bytes( + crypt_serialization.Encoding.OpenSSH, + crypt_serialization.PublicFormat.OpenSSH, + ).decode() + return private_key, public_key + + def create_key_pair(context, key_name): _validate_name(key_name) nova = clients.nova(context) + private_key, public_key = _generate_key_pair() try: - key_pair = nova.keypairs.create(key_name) + key_pair = nova.keypairs.create(key_name, public_key) except nova_exception.OverLimit: raise exception.ResourceLimitExceeded(resource='keypairs') except nova_exception.Conflict: raise exception.InvalidKeyPairDuplicate(key_name=key_name) formatted_key_pair = _format_key_pair(key_pair) - formatted_key_pair['keyMaterial'] = key_pair.private_key + formatted_key_pair['keyMaterial'] = private_key return formatted_key_pair diff --git a/ec2api/db/sqlalchemy/api.py b/ec2api/db/sqlalchemy/api.py index 50618f69..dbdf8ba5 100644 --- a/ec2api/db/sqlalchemy/api.py +++ b/ec2api/db/sqlalchemy/api.py @@ -41,7 +41,11 @@ def _create_facade_lazily(): global _MASTER_FACADE if _MASTER_FACADE is None: - _MASTER_FACADE = db_session.EngineFacade.from_config(CONF) + # FIXME(priteau): Remove autocommit=True (and ideally use of + # LegacyEngineFacade) asap since it's not compatible with SQLAlchemy + # 2.0. + _MASTER_FACADE = db_session.EngineFacade.from_config(CONF, + autocommit=True) return _MASTER_FACADE diff --git a/ec2api/tests/unit/test_key_pair.py b/ec2api/tests/unit/test_key_pair.py index d8ff9e2d..4ca00ea0 100644 --- a/ec2api/tests/unit/test_key_pair.py +++ b/ec2api/tests/unit/test_key_pair.py @@ -13,6 +13,8 @@ # limitations under the License. import base64 +from unittest import mock + from novaclient import exceptions as nova_exception @@ -24,12 +26,15 @@ from ec2api.tests.unit import tools class KeyPairCase(base.ApiTestCase): - def test_create_key_pair(self): + @mock.patch('ec2api.api.key_pair._generate_key_pair') + def test_create_key_pair(self, _generate_key_pair): + _generate_key_pair.return_value = ( + fakes.PRIVATE_KEY_KEY_PAIR, fakes.PUBLIC_KEY_KEY_PAIR) self.nova.keypairs.create.return_value = ( fakes.NovaKeyPair(fakes.OS_KEY_PAIR)) resp = self.execute('CreateKeyPair', {'KeyName': fakes.NAME_KEY_PAIR}) self.assertThat(fakes.EC2_KEY_PAIR, matchers.DictMatches(resp)) - self.nova.keypairs.create.assert_called_once_with(fakes.NAME_KEY_PAIR) + _generate_key_pair.assert_called_once_with() def test_create_key_pair_invalid(self): self.nova.keypairs.create.side_effect = (