Browse Source

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
tags/12.0.0a0
Eric Brown 4 years ago
parent
commit
3f3f9bf22e
2 changed files with 130 additions and 33 deletions
  1. 28
    33
      nova/crypto.py
  2. 102
    0
      nova/tests/unit/test_crypto.py

+ 28
- 33
nova/crypto.py View File

@@ -23,9 +23,11 @@ Includes root and intermediate CAs, SSH key_pairs and x509 certificates.
23 23
 from __future__ import absolute_import
24 24
 
25 25
 import base64
26
+import binascii
26 27
 import os
27 28
 import re
28 29
 import string
30
+import StringIO
29 31
 import struct
30 32
 
31 33
 from oslo_concurrency import processutils
@@ -33,6 +35,7 @@ from oslo_config import cfg
33 35
 from oslo_log import log as logging
34 36
 from oslo_utils import excutils
35 37
 from oslo_utils import timeutils
38
+import paramiko
36 39
 from pyasn1.codec.der import encoder as der_encoder
37 40
 from pyasn1.type import univ
38 41
 
@@ -126,22 +129,26 @@ def ensure_ca_filesystem():
126 129
             os.chdir(start)
127 130
 
128 131
 
129
-def _generate_fingerprint(public_key_file):
130
-    (out, err) = utils.execute('ssh-keygen', '-q', '-l', '-f', public_key_file)
131
-    fingerprint = out.split(' ')[1]
132
-    return fingerprint
133
-
134
-
135 132
 def generate_fingerprint(public_key):
136
-    with utils.tempdir() as tmpdir:
137
-        try:
138
-            pubfile = os.path.join(tmpdir, 'temp.pub')
139
-            with open(pubfile, 'w') as f:
140
-                f.write(public_key)
141
-            return _generate_fingerprint(pubfile)
142
-        except processutils.ProcessExecutionError:
133
+    try:
134
+        parts = public_key.split(' ')
135
+        ssh_alg = parts[0]
136
+        pub_data = parts[1].decode('base64')
137
+        if ssh_alg == 'ssh-rsa':
138
+            pkey = paramiko.RSAKey(data=pub_data)
139
+        elif ssh_alg == 'ssh-dss':
140
+            pkey = paramiko.DSSKey(data=pub_data)
141
+        elif ssh_alg == 'ecdsa-sha2-nistp256':
142
+            pkey = paramiko.ECDSAKey(data=pub_data, validate_point=False)
143
+        else:
143 144
             raise exception.InvalidKeypair(
144
-                reason=_('failed to generate fingerprint'))
145
+                reason=_('Unknown ssh key type %s') % ssh_alg)
146
+        raw_fp = binascii.hexlify(pkey.get_fingerprint())
147
+        return ':'.join(a + b for a, b in zip(raw_fp[::2], raw_fp[1::2]))
148
+    except (IndexError, UnicodeDecodeError, binascii.Error,
149
+            paramiko.ssh_exception.SSHException):
150
+        raise exception.InvalidKeypair(
151
+            reason=_('failed to generate fingerprint'))
145 152
 
146 153
 
147 154
 def generate_x509_fingerprint(pem_key):
@@ -157,25 +164,13 @@ def generate_x509_fingerprint(pem_key):
157 164
                      'Error message: %s') % ex)
158 165
 
159 166
 
160
-def generate_key_pair(bits=None):
161
-    with utils.tempdir() as tmpdir:
162
-        keyfile = os.path.join(tmpdir, 'temp')
163
-        args = ['ssh-keygen', '-q', '-N', '', '-t', 'rsa',
164
-                '-f', keyfile, '-C', 'Generated-by-Nova']
165
-        if bits is not None:
166
-            args.extend(['-b', bits])
167
-        utils.execute(*args)
168
-        fingerprint = _generate_fingerprint('%s.pub' % (keyfile))
169
-        if not os.path.exists(keyfile):
170
-            raise exception.FileNotFound(keyfile)
171
-        with open(keyfile) as f:
172
-            private_key = f.read()
173
-        public_key_path = keyfile + '.pub'
174
-        if not os.path.exists(public_key_path):
175
-            raise exception.FileNotFound(public_key_path)
176
-        with open(public_key_path) as f:
177
-            public_key = f.read()
178
-
167
+def generate_key_pair(bits=2048):
168
+    key = paramiko.RSAKey.generate(bits)
169
+    keyout = StringIO.StringIO()
170
+    key.write_private_key(keyout)
171
+    private_key = keyout.getvalue()
172
+    public_key = '%s %s Generated-by-Nova' % (key.get_name(), key.get_base64())
173
+    fingerprint = generate_fingerprint(public_key)
179 174
     return (private_key, public_key, fingerprint)
180 175
 
181 176
 

+ 102
- 0
nova/tests/unit/test_crypto.py View File

@@ -17,10 +17,12 @@ Tests for Crypto module.
17 17
 """
18 18
 
19 19
 import os
20
+import StringIO
20 21
 
21 22
 import mock
22 23
 from mox3 import mox
23 24
 from oslo_concurrency import processutils
25
+import paramiko
24 26
 
25 27
 from nova import crypto
26 28
 from nova import db
@@ -264,3 +266,103 @@ class ConversionTests(test.TestCase):
264 266
     def test_convert_failure(self):
265 267
         self.assertRaises(exception.EncryptionFailure,
266 268
                           crypto.convert_from_sshrsa_to_pkcs8, '')
269
+
270
+
271
+class KeyPairTest(test.TestCase):
272
+    rsa_prv = (
273
+        "-----BEGIN RSA PRIVATE KEY-----\n"
274
+        "MIIEowIBAAKCAQEA5G44D6lEgMj6cRwCPydsMl1VRN2B9DVyV5lmwssGeJClywZM\n"
275
+        "WcKlSZBaWPbwbt20/r74eMGZPlqtEi9Ro+EHj4/n5+3A2Mh11h0PGSt53PSPfWwo\n"
276
+        "ZhEg9hQ1w1ZxfBMCx7eG2YdGFQocMgR0zQasJGjjt8hruCnWRB3pNH9DhEwKhgET\n"
277
+        "H0/CFzxSh0eZWs/O4GSf4upwmRG/1Yu90vnVZq3AanwvvW5UBk6g4uWb6FTES867\n"
278
+        "kAy4b5EcH6WR3lLE09omuG/NqtH+qkgIdQconDkmkuK3xf5go6GSwEod0erM1G1v\n"
279
+        "e+C4w/MD98KZ4Zlon9hy7oE2rcqHXf58gZtOTQIDAQABAoIBAQCnkeM2Oemyv7xY\n"
280
+        "dT+ArJ7GY4lFt2i5iOuUL0ge5Wid0R6OTNR9lDhEOszMLno6GhHIPrdvfjW4dDQ5\n"
281
+        "/tRY757oRZzNmq+5V3R52V9WC3qeCBmq3EjWdwJDAphd72/YoOmNMKiPsphKntwI\n"
282
+        "JRS5wodNPlSuYSwEMUypM3f7ttAEn5CASgYgribBDapm7EqkVa2AqSvpFzNvN3/e\n"
283
+        "Sc36/XlxJin7AkKVOnRksuVOOj504VUQfXgVWZkfTeZqAROgA1FSnjUAffcubJmq\n"
284
+        "pDL/JSgOqN4S+sJkkTrb19MuM9M/IdXteloynF+GUKZx6FdVQQc8xCiXgeupeeSD\n"
285
+        "fNMAP7DRAoGBAP0JRFm3fCAavBREKVOyZm20DpeR6zMrVP7ht0SykkT/bw/kiRG+\n"
286
+        "FH1tNioj9uyixt5SiKhH3ZVAunjsKvrwET8i3uz1M2Gk+ovWdLXurBogYNNWafjQ\n"
287
+        "hRhFHpyExoZYRsn58bvYvjFXTO6JxuNS2b59DGBRkQ5mpsOhxarfbZnXAoGBAOcb\n"
288
+        "K+qoPDeDicnQZ8+ygYYHxY3fy1nvm1F19jBiWd26bAUOHeZNPPKGvTSlrGWJgEyA\n"
289
+        "FjZIlHJOY2s0dhukiytOiXzdA5iqK1NvlF+QTUI4tCeNMVejWC+n6sKR9ADZkX8D\n"
290
+        "NOHaLkDzc/ukus59aKyjxP53I6SV6y6m5NeyvDx7AoGAaUji1MXA8wbMvU4DOB0h\n"
291
+        "+4GRFMYVbEwaaJd4jzASJn12M9GuquBBXFMF15DxXFL6lmUXEZYdf83YCRqTY6hi\n"
292
+        "NLgIs+XuxDFGQssv8sdletWAFE9/dpUk3A1eiFfC1wGCKuZCDBxKPvOJQjO3uryt\n"
293
+        "d1JGxQkLZ0eVGg+E1O10iC8CgYB4w2QRfNPqllu8D6EPkVHJfeonltgmKOTajm+V\n"
294
+        "HO+kw7OKeLP7EkVU3j+kcSZC8LUQRKZWu1qG2Jtu+7zz+OmYObPygXNNpS56rQW1\n"
295
+        "Yixc/FB3knpEN2DvlilAfxAoGYjD/CL4GhCtdAoZZx0Opc262OEpr4v6hzSb7i4K\n"
296
+        "4KUoXQKBgHfbiaSilxx9guUqvSaexpHmtiUwx05a05fD6tu8Cofl6AM9wGpw3xOT\n"
297
+        "tfo4ehvS13tTz2RDE2xKuetMmkya7UgifcxUmBzqkOlgr0oOi2rp+eDKXnzUUqsH\n"
298
+        "V7E96Dj36K8q2+gZIXcNqjN7PzfkF8pA0G+E1veTi8j5dnvIsy1x\n"
299
+        "-----END RSA PRIVATE KEY-----\n"
300
+    )
301
+
302
+    rsa_pub = (
303
+        "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDkbjgPqUSAyPpxHAI/J2wyXVVE"
304
+        "3YH0NXJXmWbCywZ4kKXLBkxZwqVJkFpY9vBu3bT+vvh4wZk+Wq0SL1Gj4QePj+fn"
305
+        "7cDYyHXWHQ8ZK3nc9I99bChmESD2FDXDVnF8EwLHt4bZh0YVChwyBHTNBqwkaOO3"
306
+        "yGu4KdZEHek0f0OETAqGARMfT8IXPFKHR5laz87gZJ/i6nCZEb/Vi73S+dVmrcBq"
307
+        "fC+9blQGTqDi5ZvoVMRLzruQDLhvkRwfpZHeUsTT2ia4b82q0f6qSAh1ByicOSaS"
308
+        "4rfF/mCjoZLASh3R6szUbW974LjD8wP3wpnhmWif2HLugTatyodd/nyBm05N Gen"
309
+        "erated-by-Nova"
310
+    )
311
+
312
+    rsa_fp = "e7:66:a1:2c:4f:90:6e:11:19:da:ac:c2:69:e1:ad:89"
313
+
314
+    dss_pub = (
315
+        "ssh-dss AAAAB3NzaC1kc3MAAACBAKWFW2++pDxJWObkADbSXw8KfZ4VupkRKEXF"
316
+        "SPN2kV0v+FgdnBEcrEJPExaOTMhmxIuc82ktTv76wHSEpbbsLuI7IDbB6KJJwHs2"
317
+        "y356yB28Q9rin7X0VMYKkPxvAcbIUSrEbQtyPMihlOaaQ2dGSsEQGQSpjm3f3RU6"
318
+        "OWux0w/NAAAAFQCgzWF2zxQmi/Obd11z9Im6gY02gwAAAIAHCDLjipVwMLXIqNKO"
319
+        "MktiPex+ewRQxBi80dzZ3mJzARqzLPYI9hJFUU0LiMtLuypV/djpUWN0cQpmgTQf"
320
+        "TfuZx9ipC6Mtiz66NQqjkQuoihzdk+9KlOTo03UsX5uBGwuZ09Dnf1VTF8ZsW5Hg"
321
+        "HyOk6qD71QBajkcFJAKOT3rFfgAAAIAy8trIzqEps9/n37Nli1TvNPLbFQAXl1LN"
322
+        "wUFmFDwBCGTLl8puVZv7VSu1FG8ko+mzqNebqcN4RMC26NxJqe+RRubn5KtmLoIa"
323
+        "7tRe74hvQ1HTLLuGxugwa4CewNbwzzEDEs8U79WDhGKzDkJR4nLPVimj5WLAWV70"
324
+        "RNnRX7zj5w== Generated-by-Nova"
325
+    )
326
+
327
+    dss_fp = "b9:dc:ac:57:df:2a:2b:cf:65:a8:c3:4e:9d:4a:82:3c"
328
+
329
+    ecdsa_pub = (
330
+        "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy"
331
+        "NTYAAABBBG1r4wzPTIjSo78POCq+u/czb8gYK0KvqlmCvcRPrnDWxgLw7y6BX51t"
332
+        "uYREz7iLRCP7BwUt8R+ZWzFZDeOLIWU= Generated-by-Nova"
333
+    )
334
+
335
+    ecdsa_fp = "16:6a:c9:ec:80:4d:17:3e:d5:3b:6f:c0:d7:15:04:40"
336
+
337
+    def test_generate_fingerprint(self):
338
+        fingerprint = crypto.generate_fingerprint(self.rsa_pub)
339
+        self.assertEqual(self.rsa_fp, fingerprint)
340
+
341
+        fingerprint = crypto.generate_fingerprint(self.dss_pub)
342
+        self.assertEqual(self.dss_fp, fingerprint)
343
+
344
+        fingerprint = crypto.generate_fingerprint(self.ecdsa_pub)
345
+        self.assertEqual(self.ecdsa_fp, fingerprint)
346
+
347
+    def test_generate_key_pair(self):
348
+        (private_key, public_key, fingerprint) = crypto.generate_key_pair()
349
+        raw_pub = public_key.split(' ')[1].decode('base64')
350
+        pkey = paramiko.rsakey.RSAKey(None, raw_pub)
351
+        self.assertEqual(2048, pkey.get_bits())
352
+
353
+        bits = 4096
354
+        (private_key, public_key, fingerprint) = crypto.generate_key_pair(bits)
355
+        raw_pub = public_key.split(' ')[1].decode('base64')
356
+        pkey = paramiko.rsakey.RSAKey(None, raw_pub)
357
+        self.assertEqual(bits, pkey.get_bits())
358
+
359
+        keyin = StringIO.StringIO()
360
+        keyin.write(self.rsa_prv)
361
+        keyin.seek(0)
362
+        key = paramiko.RSAKey.from_private_key(keyin)
363
+
364
+        with mock.patch.object(paramiko.RSAKey, 'generate') as mock_generate:
365
+            mock_generate.return_value = key
366
+            (private_key, public_key, fingerprint) = crypto.generate_key_pair()
367
+            self.assertEqual(self.rsa_pub, public_key)
368
+            self.assertEqual(self.rsa_fp, fingerprint)

Loading…
Cancel
Save