Replace ssh exec calls with paramiko lib

Nova already has a dependency on paramiko and therefore should
take advantage of it for generating key pairs.  This will reduce
the code complexity and remove calls to exec.

Change-Id: Ibb01f5227ded9b79816c064a06a1f6724f765e78
This commit is contained in:
Eric Brown 2015-02-20 12:44:45 -08:00
parent 45c4fcb095
commit 3f3f9bf22e
2 changed files with 130 additions and 33 deletions

View File

@ -23,9 +23,11 @@ Includes root and intermediate CAs, SSH key_pairs and x509 certificates.
from __future__ import absolute_import
import base64
import binascii
import os
import re
import string
import StringIO
import struct
from oslo_concurrency import processutils
@ -33,6 +35,7 @@ from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import timeutils
import paramiko
from pyasn1.codec.der import encoder as der_encoder
from pyasn1.type import univ
@ -126,22 +129,26 @@ def ensure_ca_filesystem():
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]
return fingerprint
def generate_fingerprint(public_key):
with utils.tempdir() as tmpdir:
try:
pubfile = os.path.join(tmpdir, 'temp.pub')
with open(pubfile, 'w') as f:
f.write(public_key)
return _generate_fingerprint(pubfile)
except processutils.ProcessExecutionError:
try:
parts = public_key.split(' ')
ssh_alg = parts[0]
pub_data = parts[1].decode('base64')
if ssh_alg == 'ssh-rsa':
pkey = paramiko.RSAKey(data=pub_data)
elif ssh_alg == 'ssh-dss':
pkey = paramiko.DSSKey(data=pub_data)
elif ssh_alg == 'ecdsa-sha2-nistp256':
pkey = paramiko.ECDSAKey(data=pub_data, validate_point=False)
else:
raise exception.InvalidKeypair(
reason=_('failed to generate fingerprint'))
reason=_('Unknown ssh key type %s') % ssh_alg)
raw_fp = binascii.hexlify(pkey.get_fingerprint())
return ':'.join(a + b for a, b in zip(raw_fp[::2], raw_fp[1::2]))
except (IndexError, UnicodeDecodeError, binascii.Error,
paramiko.ssh_exception.SSHException):
raise exception.InvalidKeypair(
reason=_('failed to generate fingerprint'))
def generate_x509_fingerprint(pem_key):
@ -157,25 +164,13 @@ def generate_x509_fingerprint(pem_key):
'Error message: %s') % ex)
def generate_key_pair(bits=None):
with utils.tempdir() as tmpdir:
keyfile = os.path.join(tmpdir, 'temp')
args = ['ssh-keygen', '-q', '-N', '', '-t', 'rsa',
'-f', keyfile, '-C', 'Generated-by-Nova']
if bits is not None:
args.extend(['-b', bits])
utils.execute(*args)
fingerprint = _generate_fingerprint('%s.pub' % (keyfile))
if not os.path.exists(keyfile):
raise exception.FileNotFound(keyfile)
with open(keyfile) as f:
private_key = f.read()
public_key_path = keyfile + '.pub'
if not os.path.exists(public_key_path):
raise exception.FileNotFound(public_key_path)
with open(public_key_path) as f:
public_key = f.read()
def generate_key_pair(bits=2048):
key = paramiko.RSAKey.generate(bits)
keyout = StringIO.StringIO()
key.write_private_key(keyout)
private_key = keyout.getvalue()
public_key = '%s %s Generated-by-Nova' % (key.get_name(), key.get_base64())
fingerprint = generate_fingerprint(public_key)
return (private_key, public_key, fingerprint)

View File

@ -17,10 +17,12 @@ Tests for Crypto module.
"""
import os
import StringIO
import mock
from mox3 import mox
from oslo_concurrency import processutils
import paramiko
from nova import crypto
from nova import db
@ -264,3 +266,103 @@ class ConversionTests(test.TestCase):
def test_convert_failure(self):
self.assertRaises(exception.EncryptionFailure,
crypto.convert_from_sshrsa_to_pkcs8, '')
class KeyPairTest(test.TestCase):
rsa_prv = (
"-----BEGIN RSA PRIVATE KEY-----\n"
"MIIEowIBAAKCAQEA5G44D6lEgMj6cRwCPydsMl1VRN2B9DVyV5lmwssGeJClywZM\n"
"WcKlSZBaWPbwbt20/r74eMGZPlqtEi9Ro+EHj4/n5+3A2Mh11h0PGSt53PSPfWwo\n"
"ZhEg9hQ1w1ZxfBMCx7eG2YdGFQocMgR0zQasJGjjt8hruCnWRB3pNH9DhEwKhgET\n"
"H0/CFzxSh0eZWs/O4GSf4upwmRG/1Yu90vnVZq3AanwvvW5UBk6g4uWb6FTES867\n"
"kAy4b5EcH6WR3lLE09omuG/NqtH+qkgIdQconDkmkuK3xf5go6GSwEod0erM1G1v\n"
"e+C4w/MD98KZ4Zlon9hy7oE2rcqHXf58gZtOTQIDAQABAoIBAQCnkeM2Oemyv7xY\n"
"dT+ArJ7GY4lFt2i5iOuUL0ge5Wid0R6OTNR9lDhEOszMLno6GhHIPrdvfjW4dDQ5\n"
"/tRY757oRZzNmq+5V3R52V9WC3qeCBmq3EjWdwJDAphd72/YoOmNMKiPsphKntwI\n"
"JRS5wodNPlSuYSwEMUypM3f7ttAEn5CASgYgribBDapm7EqkVa2AqSvpFzNvN3/e\n"
"Sc36/XlxJin7AkKVOnRksuVOOj504VUQfXgVWZkfTeZqAROgA1FSnjUAffcubJmq\n"
"pDL/JSgOqN4S+sJkkTrb19MuM9M/IdXteloynF+GUKZx6FdVQQc8xCiXgeupeeSD\n"
"fNMAP7DRAoGBAP0JRFm3fCAavBREKVOyZm20DpeR6zMrVP7ht0SykkT/bw/kiRG+\n"
"FH1tNioj9uyixt5SiKhH3ZVAunjsKvrwET8i3uz1M2Gk+ovWdLXurBogYNNWafjQ\n"
"hRhFHpyExoZYRsn58bvYvjFXTO6JxuNS2b59DGBRkQ5mpsOhxarfbZnXAoGBAOcb\n"
"K+qoPDeDicnQZ8+ygYYHxY3fy1nvm1F19jBiWd26bAUOHeZNPPKGvTSlrGWJgEyA\n"
"FjZIlHJOY2s0dhukiytOiXzdA5iqK1NvlF+QTUI4tCeNMVejWC+n6sKR9ADZkX8D\n"
"NOHaLkDzc/ukus59aKyjxP53I6SV6y6m5NeyvDx7AoGAaUji1MXA8wbMvU4DOB0h\n"
"+4GRFMYVbEwaaJd4jzASJn12M9GuquBBXFMF15DxXFL6lmUXEZYdf83YCRqTY6hi\n"
"NLgIs+XuxDFGQssv8sdletWAFE9/dpUk3A1eiFfC1wGCKuZCDBxKPvOJQjO3uryt\n"
"d1JGxQkLZ0eVGg+E1O10iC8CgYB4w2QRfNPqllu8D6EPkVHJfeonltgmKOTajm+V\n"
"HO+kw7OKeLP7EkVU3j+kcSZC8LUQRKZWu1qG2Jtu+7zz+OmYObPygXNNpS56rQW1\n"
"Yixc/FB3knpEN2DvlilAfxAoGYjD/CL4GhCtdAoZZx0Opc262OEpr4v6hzSb7i4K\n"
"4KUoXQKBgHfbiaSilxx9guUqvSaexpHmtiUwx05a05fD6tu8Cofl6AM9wGpw3xOT\n"
"tfo4ehvS13tTz2RDE2xKuetMmkya7UgifcxUmBzqkOlgr0oOi2rp+eDKXnzUUqsH\n"
"V7E96Dj36K8q2+gZIXcNqjN7PzfkF8pA0G+E1veTi8j5dnvIsy1x\n"
"-----END RSA PRIVATE KEY-----\n"
)
rsa_pub = (
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDkbjgPqUSAyPpxHAI/J2wyXVVE"
"3YH0NXJXmWbCywZ4kKXLBkxZwqVJkFpY9vBu3bT+vvh4wZk+Wq0SL1Gj4QePj+fn"
"7cDYyHXWHQ8ZK3nc9I99bChmESD2FDXDVnF8EwLHt4bZh0YVChwyBHTNBqwkaOO3"
"yGu4KdZEHek0f0OETAqGARMfT8IXPFKHR5laz87gZJ/i6nCZEb/Vi73S+dVmrcBq"
"fC+9blQGTqDi5ZvoVMRLzruQDLhvkRwfpZHeUsTT2ia4b82q0f6qSAh1ByicOSaS"
"4rfF/mCjoZLASh3R6szUbW974LjD8wP3wpnhmWif2HLugTatyodd/nyBm05N Gen"
"erated-by-Nova"
)
rsa_fp = "e7:66:a1:2c:4f:90:6e:11:19:da:ac:c2:69:e1:ad:89"
dss_pub = (
"ssh-dss AAAAB3NzaC1kc3MAAACBAKWFW2++pDxJWObkADbSXw8KfZ4VupkRKEXF"
"SPN2kV0v+FgdnBEcrEJPExaOTMhmxIuc82ktTv76wHSEpbbsLuI7IDbB6KJJwHs2"
"y356yB28Q9rin7X0VMYKkPxvAcbIUSrEbQtyPMihlOaaQ2dGSsEQGQSpjm3f3RU6"
"OWux0w/NAAAAFQCgzWF2zxQmi/Obd11z9Im6gY02gwAAAIAHCDLjipVwMLXIqNKO"
"MktiPex+ewRQxBi80dzZ3mJzARqzLPYI9hJFUU0LiMtLuypV/djpUWN0cQpmgTQf"
"TfuZx9ipC6Mtiz66NQqjkQuoihzdk+9KlOTo03UsX5uBGwuZ09Dnf1VTF8ZsW5Hg"
"HyOk6qD71QBajkcFJAKOT3rFfgAAAIAy8trIzqEps9/n37Nli1TvNPLbFQAXl1LN"
"wUFmFDwBCGTLl8puVZv7VSu1FG8ko+mzqNebqcN4RMC26NxJqe+RRubn5KtmLoIa"
"7tRe74hvQ1HTLLuGxugwa4CewNbwzzEDEs8U79WDhGKzDkJR4nLPVimj5WLAWV70"
"RNnRX7zj5w== Generated-by-Nova"
)
dss_fp = "b9:dc:ac:57:df:2a:2b:cf:65:a8:c3:4e:9d:4a:82:3c"
ecdsa_pub = (
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy"
"NTYAAABBBG1r4wzPTIjSo78POCq+u/czb8gYK0KvqlmCvcRPrnDWxgLw7y6BX51t"
"uYREz7iLRCP7BwUt8R+ZWzFZDeOLIWU= Generated-by-Nova"
)
ecdsa_fp = "16:6a:c9:ec:80:4d:17:3e:d5:3b:6f:c0:d7:15:04:40"
def test_generate_fingerprint(self):
fingerprint = crypto.generate_fingerprint(self.rsa_pub)
self.assertEqual(self.rsa_fp, fingerprint)
fingerprint = crypto.generate_fingerprint(self.dss_pub)
self.assertEqual(self.dss_fp, fingerprint)
fingerprint = crypto.generate_fingerprint(self.ecdsa_pub)
self.assertEqual(self.ecdsa_fp, fingerprint)
def test_generate_key_pair(self):
(private_key, public_key, fingerprint) = crypto.generate_key_pair()
raw_pub = public_key.split(' ')[1].decode('base64')
pkey = paramiko.rsakey.RSAKey(None, raw_pub)
self.assertEqual(2048, pkey.get_bits())
bits = 4096
(private_key, public_key, fingerprint) = crypto.generate_key_pair(bits)
raw_pub = public_key.split(' ')[1].decode('base64')
pkey = paramiko.rsakey.RSAKey(None, raw_pub)
self.assertEqual(bits, pkey.get_bits())
keyin = StringIO.StringIO()
keyin.write(self.rsa_prv)
keyin.seek(0)
key = paramiko.RSAKey.from_private_key(keyin)
with mock.patch.object(paramiko.RSAKey, 'generate') as mock_generate:
mock_generate.return_value = key
(private_key, public_key, fingerprint) = crypto.generate_key_pair()
self.assertEqual(self.rsa_pub, public_key)
self.assertEqual(self.rsa_fp, fingerprint)