summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStephen Finucane <sfinucan@redhat.com>2023-04-28 11:28:56 +0100
committerStephen Finucane <sfinucan@redhat.com>2023-05-02 12:18:52 +0100
commit2454636386d443473dedff1f07f8623108e87298 (patch)
tree204f54a811b80170e87941c68b3f942b39c8b2f7
parenta2f877f70c460769337fab5fd2d65cca0ba9091c (diff)
downloadpython-openstackclient-2454636386d443473dedff1f07f8623108e87298.tar.gz
compute: Generate SSH keypairs ourselves
Starting with the 2.92 microversion, nova will no longer generate SSH keys. Avoid breaking users by generating keypairs ourselves using the cryptography library, which was already an indirect dependency through openstacksdk. Change-Id: I3ad2732f70854ab72da0947f00847351dda23944 Implements: blueprint keypair-generation-removal
-rw-r--r--openstackclient/compute/v2/keypair.py101
-rw-r--r--openstackclient/tests/functional/compute/v2/test_keypair.py14
-rw-r--r--openstackclient/tests/unit/compute/v2/fakes.py4
-rw-r--r--openstackclient/tests/unit/compute/v2/test_keypair.py49
-rw-r--r--releasenotes/notes/keypair-create-client-side-generation-73d8dd36192f70c9.yaml11
-rw-r--r--requirements.txt1
6 files changed, 117 insertions, 63 deletions
diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py
index 7dabf78d..3a5513ef 100644
--- a/openstackclient/compute/v2/keypair.py
+++ b/openstackclient/compute/v2/keypair.py
@@ -15,11 +15,13 @@
"""Keypair action implementations"""
+import collections
import io
import logging
import os
-import sys
+from cryptography.hazmat.primitives.asymmetric import ed25519
+from cryptography.hazmat.primitives import serialization
from openstack import utils as sdk_utils
from osc_lib.command import command
from osc_lib import exceptions
@@ -30,6 +32,27 @@ from openstackclient.identity import common as identity_common
LOG = logging.getLogger(__name__)
+Keypair = collections.namedtuple('Keypair', 'private_key public_key')
+
+
+def _generate_keypair():
+ """Generate a Ed25519 keypair in OpenSSH format.
+
+ :returns: A `Keypair` named tuple with the generated private and public
+ keys.
+ """
+ key = ed25519.Ed25519PrivateKey.generate()
+ private_key = key.private_bytes(
+ serialization.Encoding.PEM,
+ serialization.PrivateFormat.OpenSSH,
+ serialization.NoEncryption()
+ ).decode()
+ public_key = key.public_key().public_bytes(
+ serialization.Encoding.OpenSSH,
+ serialization.PublicFormat.OpenSSH
+ ).decode()
+
+ return Keypair(private_key, public_key)
def _get_keypair_columns(item, hide_pub_key=False, hide_priv_key=False):
@@ -59,30 +82,37 @@ class CreateKeypair(command.ShowOne):
key_group.add_argument(
'--public-key',
metavar='<file>',
- help=_("Filename for public key to add. If not used, "
- "creates a private key.")
+ help=_(
+ "Filename for public key to add. "
+ "If not used, generates a private key in ssh-ed25519 format. "
+ "To generate keys in other formats, including the legacy "
+ "ssh-rsa format, you must use an external tool such as "
+ "ssh-keygen and specify this argument."
+ ),
)
key_group.add_argument(
'--private-key',
metavar='<file>',
- help=_("Filename for private key to save. If not used, "
- "print private key in console.")
+ help=_(
+ "Filename for private key to save. "
+ "If not used, print private key in console."
+ )
)
parser.add_argument(
'--type',
metavar='<type>',
choices=['ssh', 'x509'],
help=_(
- "Keypair type. Can be ssh or x509. "
- "(Supported by API versions '2.2' - '2.latest')"
+ 'Keypair type '
+ '(supported by --os-compute-api-version 2.2 or above)'
),
)
parser.add_argument(
'--user',
metavar='<user>',
help=_(
- 'The owner of the keypair. (admin only) (name or ID). '
- 'Requires ``--os-compute-api-version`` 2.10 or greater.'
+ 'The owner of the keypair (admin only) (name or ID) '
+ '(supported by --os-compute-api-version 2.10 or above)'
),
)
identity_common.add_user_domain_option_to_parser(parser)
@@ -96,19 +126,43 @@ class CreateKeypair(command.ShowOne):
'name': parsed_args.name
}
- public_key = parsed_args.public_key
- if public_key:
+ if parsed_args.public_key:
+ generated_keypair = None
try:
with io.open(os.path.expanduser(parsed_args.public_key)) as p:
public_key = p.read()
except IOError as e:
msg = _("Key file %(public_key)s not found: %(exception)s")
raise exceptions.CommandError(
- msg % {"public_key": parsed_args.public_key,
- "exception": e}
+ msg % {
+ "public_key": parsed_args.public_key,
+ "exception": e,
+ }
)
kwargs['public_key'] = public_key
+ else:
+ generated_keypair = _generate_keypair()
+ kwargs['public_key'] = generated_keypair.public_key
+
+ # If user have us a file, save private key into specified file
+ if parsed_args.private_key:
+ try:
+ with io.open(
+ os.path.expanduser(parsed_args.private_key), 'w+'
+ ) as p:
+ p.write(generated_keypair.private_key)
+ except IOError as e:
+ msg = _(
+ "Key file %(private_key)s can not be saved: "
+ "%(exception)s"
+ )
+ raise exceptions.CommandError(
+ msg % {
+ "private_key": parsed_args.private_key,
+ "exception": e,
+ }
+ )
if parsed_args.type:
if not sdk_utils.supports_microversion(compute_client, '2.2'):
@@ -136,32 +190,17 @@ class CreateKeypair(command.ShowOne):
keypair = compute_client.create_keypair(**kwargs)
- private_key = parsed_args.private_key
- # Save private key into specified file
- if private_key:
- try:
- with io.open(
- os.path.expanduser(parsed_args.private_key), 'w+'
- ) as p:
- p.write(keypair.private_key)
- except IOError as e:
- msg = _("Key file %(private_key)s can not be saved: "
- "%(exception)s")
- raise exceptions.CommandError(
- msg % {"private_key": parsed_args.private_key,
- "exception": e}
- )
# NOTE(dtroyer): how do we want to handle the display of the private
# key when it needs to be communicated back to the user
# For now, duplicate nova keypair-add command output
- if public_key or private_key:
+ if parsed_args.public_key or parsed_args.private_key:
display_columns, columns = _get_keypair_columns(
keypair, hide_pub_key=True, hide_priv_key=True)
data = utils.get_item_properties(keypair, columns)
return (display_columns, data)
else:
- sys.stdout.write(keypair.private_key)
+ self.app.stdout.write(generated_keypair.private_key)
return ({}, {})
@@ -405,5 +444,5 @@ class ShowKeypair(command.ShowOne):
data = utils.get_item_properties(keypair, columns)
return (display_columns, data)
else:
- sys.stdout.write(keypair.public_key)
+ self.app.stdout.write(keypair.public_key)
return ({}, {})
diff --git a/openstackclient/tests/functional/compute/v2/test_keypair.py b/openstackclient/tests/functional/compute/v2/test_keypair.py
index 828d5dad..e1d12977 100644
--- a/openstackclient/tests/functional/compute/v2/test_keypair.py
+++ b/openstackclient/tests/functional/compute/v2/test_keypair.py
@@ -117,24 +117,28 @@ class KeypairTests(KeypairBase):
self.assertIsNotNone(cmd_output.get('user_id'))
self.assertIsNotNone(cmd_output.get('fingerprint'))
pk_content = f.read()
- self.assertInOutput('-----BEGIN RSA PRIVATE KEY-----', pk_content)
+ self.assertInOutput(
+ '-----BEGIN OPENSSH PRIVATE KEY-----', pk_content,
+ )
self.assertRegex(pk_content, "[0-9A-Za-z+/]+[=]{0,3}\n")
- self.assertInOutput('-----END RSA PRIVATE KEY-----', pk_content)
+ self.assertInOutput(
+ '-----END OPENSSH PRIVATE KEY-----', pk_content,
+ )
def test_keypair_create(self):
"""Test keypair create command.
Test steps:
1) Create keypair in setUp
- 2) Check RSA private key in output
+ 2) Check Ed25519 private key in output
3) Check for new keypair in keypairs list
"""
NewName = data_utils.rand_name('TestKeyPairCreated')
raw_output = self.openstack('keypair create ' + NewName)
self.addCleanup(self.openstack, 'keypair delete ' + NewName)
- self.assertInOutput('-----BEGIN RSA PRIVATE KEY-----', raw_output)
+ self.assertInOutput('-----BEGIN OPENSSH PRIVATE KEY-----', raw_output)
self.assertRegex(raw_output, "[0-9A-Za-z+/]+[=]{0,3}\n")
- self.assertInOutput('-----END RSA PRIVATE KEY-----', raw_output)
+ self.assertInOutput('-----END OPENSSH PRIVATE KEY-----', raw_output)
self.assertIn(NewName, self.keypair_list())
def test_keypair_delete_not_existing(self):
diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py
index 08d4a574..356cc29c 100644
--- a/openstackclient/tests/unit/compute/v2/fakes.py
+++ b/openstackclient/tests/unit/compute/v2/fakes.py
@@ -793,7 +793,7 @@ class FakeKeypair(object):
"""Fake one or more keypairs."""
@staticmethod
- def create_one_keypair(attrs=None, no_pri=False):
+ def create_one_keypair(attrs=None):
"""Create a fake keypair
:param dict attrs:
@@ -811,8 +811,6 @@ class FakeKeypair(object):
'public_key': 'dummy',
'user_id': 'user'
}
- if not no_pri:
- keypair_info['private_key'] = 'private_key'
# Overwrite default attributes.
keypair_info.update(attrs)
diff --git a/openstackclient/tests/unit/compute/v2/test_keypair.py b/openstackclient/tests/unit/compute/v2/test_keypair.py
index 65d9396a..1c2923b2 100644
--- a/openstackclient/tests/unit/compute/v2/test_keypair.py
+++ b/openstackclient/tests/unit/compute/v2/test_keypair.py
@@ -54,10 +54,10 @@ class TestKeypair(compute_fakes.TestComputev2):
class TestKeypairCreate(TestKeypair):
- keypair = compute_fakes.FakeKeypair.create_one_keypair()
-
def setUp(self):
- super(TestKeypairCreate, self).setUp()
+ super().setUp()
+
+ self.keypair = compute_fakes.FakeKeypair.create_one_keypair()
self.columns = (
'fingerprint',
@@ -77,8 +77,11 @@ class TestKeypairCreate(TestKeypair):
self.sdk_client.create_keypair.return_value = self.keypair
- def test_key_pair_create_no_options(self):
-
+ @mock.patch.object(
+ keypair, '_generate_keypair',
+ return_value=keypair.Keypair('private', 'public'),
+ )
+ def test_key_pair_create_no_options(self, mock_generate):
arglist = [
self.keypair.name,
]
@@ -90,18 +93,14 @@ class TestKeypairCreate(TestKeypair):
columns, data = self.cmd.take_action(parsed_args)
self.sdk_client.create_keypair.assert_called_with(
- name=self.keypair.name
+ name=self.keypair.name,
+ public_key=mock_generate.return_value.public_key,
)
self.assertEqual({}, columns)
self.assertEqual({}, data)
def test_keypair_create_public_key(self):
- # overwrite the setup one because we want to omit private_key
- self.keypair = compute_fakes.FakeKeypair.create_one_keypair(
- no_pri=True)
- self.sdk_client.create_keypair.return_value = self.keypair
-
self.data = (
self.keypair.fingerprint,
self.keypair.name,
@@ -135,7 +134,11 @@ class TestKeypairCreate(TestKeypair):
self.assertEqual(self.columns, columns)
self.assertEqual(self.data, data)
- def test_keypair_create_private_key(self):
+ @mock.patch.object(
+ keypair, '_generate_keypair',
+ return_value=keypair.Keypair('private', 'public'),
+ )
+ def test_keypair_create_private_key(self, mock_generate):
tmp_pk_file = '/tmp/kp-file-' + uuid.uuid4().hex
arglist = [
'--private-key', tmp_pk_file,
@@ -156,10 +159,13 @@ class TestKeypairCreate(TestKeypair):
self.sdk_client.create_keypair.assert_called_with(
name=self.keypair.name,
+ public_key=mock_generate.return_value.public_key,
)
mock_open.assert_called_once_with(tmp_pk_file, 'w+')
- m_file.write.assert_called_once_with(self.keypair.private_key)
+ m_file.write.assert_called_once_with(
+ mock_generate.return_value.private_key,
+ )
self.assertEqual(self.columns, columns)
self.assertEqual(self.data, data)
@@ -167,8 +173,6 @@ class TestKeypairCreate(TestKeypair):
@mock.patch.object(sdk_utils, 'supports_microversion', return_value=True)
def test_keypair_create_with_key_type(self, sm_mock):
for key_type in ['x509', 'ssh']:
- self.keypair = compute_fakes.FakeKeypair.create_one_keypair(
- no_pri=True)
self.sdk_client.create_keypair.return_value = self.keypair
self.data = (
@@ -233,8 +237,12 @@ class TestKeypairCreate(TestKeypair):
'--os-compute-api-version 2.2 or greater is required',
str(ex))
+ @mock.patch.object(
+ keypair, '_generate_keypair',
+ return_value=keypair.Keypair('private', 'public'),
+ )
@mock.patch.object(sdk_utils, 'supports_microversion', return_value=True)
- def test_key_pair_create_with_user(self, sm_mock):
+ def test_key_pair_create_with_user(self, sm_mock, mock_generate):
arglist = [
'--user', identity_fakes.user_name,
self.keypair.name,
@@ -250,6 +258,7 @@ class TestKeypairCreate(TestKeypair):
self.sdk_client.create_keypair.assert_called_with(
name=self.keypair.name,
user_id=identity_fakes.user_id,
+ public_key=mock_generate.return_value.public_key,
)
self.assertEqual({}, columns)
@@ -673,9 +682,6 @@ class TestKeypairShow(TestKeypair):
self.cmd, arglist, verifylist)
def test_keypair_show(self):
- # overwrite the setup one because we want to omit private_key
- self.keypair = compute_fakes.FakeKeypair.create_one_keypair(
- no_pri=True)
self.sdk_client.find_keypair.return_value = self.keypair
self.data = (
@@ -704,7 +710,6 @@ class TestKeypairShow(TestKeypair):
self.assertEqual(self.data, data)
def test_keypair_show_public(self):
-
arglist = [
'--public-key',
self.keypair.name
@@ -723,10 +728,6 @@ class TestKeypairShow(TestKeypair):
@mock.patch.object(sdk_utils, 'supports_microversion', return_value=True)
def test_keypair_show_with_user(self, sm_mock):
-
- # overwrite the setup one because we want to omit private_key
- self.keypair = compute_fakes.FakeKeypair.create_one_keypair(
- no_pri=True)
self.sdk_client.find_keypair.return_value = self.keypair
self.data = (
diff --git a/releasenotes/notes/keypair-create-client-side-generation-73d8dd36192f70c9.yaml b/releasenotes/notes/keypair-create-client-side-generation-73d8dd36192f70c9.yaml
new file mode 100644
index 00000000..bf5fd5b7
--- /dev/null
+++ b/releasenotes/notes/keypair-create-client-side-generation-73d8dd36192f70c9.yaml
@@ -0,0 +1,11 @@
+---
+features:
+ - |
+ The ``openstack keypair create`` command will now generate keypairs on the
+ client side in ssh-ed25519 format. The Compute service no longer supports
+ server-side key generation starting with ``--os-compute-api-version 2.92``
+ while the use of ssh-ed25519 is necessary as support for ssh-rsa has been
+ disabled by default starting in OpenSSH 8.8, which prevents its use in
+ guests using this version of OpenSSH in the default configuration.
+ ssh-ed25519 support is widespread and is supported by OpenSSH 6.5 or later
+ and Dropbear 2020.79 or later.
diff --git a/requirements.txt b/requirements.txt
index 1ae8cec4..458fb411 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,6 +4,7 @@
pbr!=2.1.0,>=2.0.0 # Apache-2.0
+cryptography>=2.7 # BSD/Apache-2.0
cliff>=3.5.0 # Apache-2.0
iso8601>=0.1.11 # MIT
openstacksdk>=0.103.0 # Apache-2.0