From 452fe92787ff871417846748fc13e2a6a2899325 Mon Sep 17 00:00:00 2001 From: Eric Brown Date: Fri, 3 Jul 2015 00:20:05 -0700 Subject: [PATCH] Replace openssl calls with cryptography lib This patch is a code reduction cleanup fix. Exec calls to openssl can mostly be replaced with a proper python library called cryptography. Changed the following: * Removed convert_from_sshrsa_to_pkcs8 and _to_sequence since cryptography already has a method to do this conversion. * Replaced openssl rsautl call in ssh_encrypt_text and decrypt_text with cryptography's public key encrypt and decrypt methods. * Replaced call to openssl x509 with cryptography's load_pem_x509_certificate which also includes a way to retrieve the fingerprint. * generate_fingerprint's calls to paramiko were replaced with cryptography's serialization module. Cryptography supports all key sizes of elliptic curve keys whereas paramiko did not. * Removed the requirement on pyasn1 Change-Id: I750b49e6a73cd18af8199c1c859c9f6047252387 --- nova/crypto.py | 156 +++++++++------------------------ nova/tests/unit/test_crypto.py | 48 +++------- requirements.txt | 2 +- 3 files changed, 52 insertions(+), 154 deletions(-) diff --git a/nova/crypto.py b/nova/crypto.py index 1b050788f35f..dfaafcfdad16 100644 --- a/nova/crypto.py +++ b/nova/crypto.py @@ -25,9 +25,13 @@ from __future__ import absolute_import import base64 import binascii import os -import re -import struct +from cryptography import exceptions +from cryptography.hazmat import backends +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import serialization +from cryptography import x509 from oslo_concurrency import processutils from oslo_config import cfg from oslo_log import log as logging @@ -35,8 +39,6 @@ from oslo_utils import excutils from oslo_utils import fileutils from oslo_utils import timeutils import paramiko -from pyasn1.codec.der import encoder as der_encoder -from pyasn1.type import univ import six from nova import context @@ -130,24 +132,21 @@ def ensure_ca_filesystem(): def generate_fingerprint(public_key): try: - parts = public_key.split(' ') - ssh_alg = parts[0] - pub_data = base64.b64decode(parts[1]) - 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=_('Unknown ssh key type %s') % ssh_alg) - raw_fp = binascii.hexlify(pkey.get_fingerprint()) + pub_bytes = public_key.encode('utf-8') + # Test that the given public_key string is a proper ssh key. The + # returned object is unused since pyca/cryptography does not have a + # fingerprint method. + serialization.load_ssh_public_key( + pub_bytes, backends.default_backend()) + pub_data = base64.b64decode(public_key.split(' ')[1]) + digest = hashes.Hash(hashes.MD5(), backends.default_backend()) + digest.update(pub_data) + md5hash = digest.finalize() + raw_fp = binascii.hexlify(md5hash) if six.PY3: raw_fp = raw_fp.decode('ascii') return ':'.join(a + b for a, b in zip(raw_fp[::2], raw_fp[1::2])) - except (TypeError, IndexError, UnicodeDecodeError, binascii.Error, - paramiko.ssh_exception.SSHException): + except Exception: raise exception.InvalidKeypair( reason=_('failed to generate fingerprint')) @@ -156,12 +155,13 @@ def generate_x509_fingerprint(pem_key): try: if isinstance(pem_key, six.text_type): pem_key = pem_key.encode('utf-8') - (out, _err) = utils.execute('openssl', 'x509', '-inform', 'PEM', - '-fingerprint', '-noout', - process_input=pem_key) - fingerprint = out.rpartition('=')[2].strip() - return fingerprint.lower() - except processutils.ProcessExecutionError as ex: + cert = x509.load_pem_x509_certificate( + pem_key, backends.default_backend()) + raw_fp = binascii.hexlify(cert.fingerprint(hashes.SHA1())) + if six.PY3: + raw_fp = raw_fp.decode('ascii') + return ':'.join(a + b for a, b in zip(raw_fp[::2], raw_fp[1::2])) + except (ValueError, TypeError, binascii.Error) as ex: raise exception.InvalidKeypair( reason=_('failed to generate X509 fingerprint. ' 'Error message: %s') % ex) @@ -189,81 +189,17 @@ def fetch_crl(project_id): def decrypt_text(project_id, text): - private_key = key_path(project_id) - if not os.path.exists(private_key): + private_key_file = key_path(project_id) + if not os.path.exists(private_key_file): raise exception.ProjectNotFound(project_id=project_id) + with open(private_key_file, 'rb') as f: + data = f.read() try: - dec, _err = utils.execute('openssl', - 'rsautl', - '-decrypt', - '-inkey', '%s' % private_key, - process_input=text, - binary=True) - return dec - except processutils.ProcessExecutionError as exc: - raise exception.DecryptionFailure(reason=exc.stderr) - - -_RSA_OID = univ.ObjectIdentifier('1.2.840.113549.1.1.1') - - -def _to_sequence(*vals): - seq = univ.Sequence() - for i in range(len(vals)): - seq.setComponentByPosition(i, vals[i]) - return seq - - -def convert_from_sshrsa_to_pkcs8(pubkey): - """Convert a ssh public key to openssl format - Equivalent to the ssh-keygen's -m option - """ - # get the second field from the public key file. - try: - keydata = base64.b64decode(pubkey.split(None)[1]) - except IndexError: - msg = _("Unable to find the key") - raise exception.EncryptionFailure(reason=msg) - - # decode the parts of the key - parts = [] - while keydata: - dlen = struct.unpack('>I', keydata[:4])[0] - data = keydata[4:dlen + 4] - keydata = keydata[4 + dlen:] - parts.append(data) - - # Use asn to build the openssl key structure - # - # SEQUENCE(2 elem) - # +- SEQUENCE(2 elem) - # | +- OBJECT IDENTIFIER (1.2.840.113549.1.1.1) - # | +- NULL - # +- BIT STRING(1 elem) - # +- SEQUENCE(2 elem) - # +- INTEGER(2048 bit) - # +- INTEGER 65537 - - # Build the sequence for the bit string - n_val = int(binascii.hexlify(parts[2]), 16) - e_val = int(binascii.hexlify(parts[1]), 16) - pkinfo = _to_sequence(univ.Integer(n_val), univ.Integer(e_val)) - - # Convert the sequence into a bit string - pklong = int(binascii.hexlify(der_encoder.encode(pkinfo)), 16) - pkbitstring = univ.BitString("'00%s'B" % bin(pklong)[2:]) - - # Build the key data structure - oid = _to_sequence(_RSA_OID, univ.Null()) - pkcs1_seq = _to_sequence(oid, pkbitstring) - pkcs8 = base64.b64encode(der_encoder.encode(pkcs1_seq)) - if six.PY3: - pkcs8 = pkcs8.decode('ascii') - - # Remove the embedded new line and format the key, each line - # should be 64 characters long - return ('-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n' % - re.sub("(.{64})", "\\1\n", pkcs8.replace('\n', ''), re.DOTALL)) + priv_key = serialization.load_pem_private_key( + data, None, backends.default_backend()) + return priv_key.decrypt(text, padding.PKCS1v15()) + except (ValueError, TypeError, exceptions.UnsupportedAlgorithm) as exc: + raise exception.DecryptionFailure(reason=six.text_type(exc)) def ssh_encrypt_text(ssh_public_key, text): @@ -273,23 +209,13 @@ def ssh_encrypt_text(ssh_public_key, text): """ if isinstance(text, six.text_type): text = text.encode('utf-8') - with utils.tempdir() as tmpdir: - sslkey = os.path.abspath(os.path.join(tmpdir, 'ssl.key')) - try: - out = convert_from_sshrsa_to_pkcs8(ssh_public_key) - with open(sslkey, 'w') as f: - f.write(out) - enc, _err = utils.execute('openssl', - 'rsautl', - '-encrypt', - '-pubin', - '-inkey', sslkey, - '-keyform', 'PEM', - process_input=text, - binary=True) - return enc - except processutils.ProcessExecutionError as exc: - raise exception.EncryptionFailure(reason=exc.stderr) + try: + pub_bytes = ssh_public_key.encode('utf-8') + pub_key = serialization.load_ssh_public_key( + pub_bytes, backends.default_backend()) + return pub_key.encrypt(text, padding.PKCS1v15()) + except Exception as exc: + raise exception.EncryptionFailure(reason=six.text_type(exc)) def revoke_cert(project_id, file_name): diff --git a/nova/tests/unit/test_crypto.py b/nova/tests/unit/test_crypto.py index a6963e3493a9..0052af3f47ac 100644 --- a/nova/tests/unit/test_crypto.py +++ b/nova/tests/unit/test_crypto.py @@ -16,9 +16,10 @@ Tests for Crypto module. """ -import base64 import os +from cryptography.hazmat import backends +from cryptography.hazmat.primitives import serialization import mock from mox3 import mox from oslo_concurrency import processutils @@ -257,34 +258,6 @@ e6fCXWECgYEAqgpGvva5kJ1ISgNwnJbwiNw0sOT9BMOsdNZBElf0kJIIy6FMPvap crypto.ssh_encrypt_text, '', self.text) -class ConversionTests(test.TestCase): - k1 = ("ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA4CqmrxfU7x4sJrubpMNxeglul+d" - "ByrsicnvQcHDEjPzdvoz+BaoAG9bjCA5mCeTBIISsVTVXz/hxNeiuBV6LH/UR/c" - "27yl53ypN+821ImoexQZcKItdnjJ3gVZlDob1f9+1qDVy63NJ1c+TstkrCTRVeo" - "9VyE7RpdSS4UCiBe8Xwk3RkedioFxePrI0Ktc2uASw2G0G2Rl7RN7KZOJbCivfF" - "LQMAOu6e+7fYvuE1gxGHHj7dxaBY/ioGOm1W4JmQ1V7AKt19zTBlZKduN8FQMSF" - "r35CDlvoWs0+OP8nwlebKNCi/5sdL8qiSLrAcPB4LqdkAf/blNSVA2Yl83/c4lQ" - "== test@test") - - k2 = ("-----BEGIN PUBLIC KEY-----\n" - "MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQEA4CqmrxfU7x4sJrubpMNx\n" - "eglul+dByrsicnvQcHDEjPzdvoz+BaoAG9bjCA5mCeTBIISsVTVXz/hxNeiuBV6L\n" - "H/UR/c27yl53ypN+821ImoexQZcKItdnjJ3gVZlDob1f9+1qDVy63NJ1c+TstkrC\n" - "TRVeo9VyE7RpdSS4UCiBe8Xwk3RkedioFxePrI0Ktc2uASw2G0G2Rl7RN7KZOJbC\n" - "ivfFLQMAOu6e+7fYvuE1gxGHHj7dxaBY/ioGOm1W4JmQ1V7AKt19zTBlZKduN8FQ\n" - "MSFr35CDlvoWs0+OP8nwlebKNCi/5sdL8qiSLrAcPB4LqdkAf/blNSVA2Yl83/c4\n" - "lQIBIw==\n" - "-----END PUBLIC KEY-----\n") - - def test_convert_keys(self): - result = crypto.convert_from_sshrsa_to_pkcs8(self.k1) - self.assertEqual(result, self.k2) - - 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" @@ -363,19 +336,18 @@ class KeyPairTest(test.TestCase): def test_generate_key_pair_2048_bits(self): (private_key, public_key, fingerprint) = crypto.generate_key_pair() - raw_pub = public_key.split(' ')[1] - if six.PY3: - raw_pub = raw_pub.encode('ascii') - raw_pub = base64.b64decode(raw_pub) - pkey = paramiko.rsakey.RSAKey(None, raw_pub) - self.assertEqual(2048, pkey.get_bits()) + pub_bytes = public_key.encode('utf-8') + pkey = serialization.load_ssh_public_key( + pub_bytes, backends.default_backend()) + self.assertEqual(2048, pkey.key_size) def test_generate_key_pair_1024_bits(self): bits = 1024 (private_key, public_key, fingerprint) = crypto.generate_key_pair(bits) - raw_pub = base64.b64decode(public_key.split(' ')[1]) - pkey = paramiko.rsakey.RSAKey(None, raw_pub) - self.assertEqual(bits, pkey.get_bits()) + pub_bytes = public_key.encode('utf-8') + pkey = serialization.load_ssh_public_key( + pub_bytes, backends.default_backend()) + self.assertEqual(bits, pkey.key_size) def test_generate_key_pair_mocked_private_key(self): keyin = six.StringIO() diff --git a/requirements.txt b/requirements.txt index 3c958f605ef6..2ae360197016 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ keystonemiddleware>=2.0.0 lxml>=2.3 Routes!=2.0,!=2.1,>=1.12.3;python_version=='2.7' Routes!=2.0,>=1.12.3;python_version!='2.7' +cryptography>=0.9.1 # Apache-2.0 WebOb>=1.2.3 greenlet>=0.3.2 PasteDeploy>=1.5.0 @@ -21,7 +22,6 @@ sqlalchemy-migrate>=0.9.6 netaddr>=0.7.12 netifaces>=0.10.4 paramiko>=1.13.0 -pyasn1 Babel>=1.3 iso8601>=0.1.9 jsonschema!=2.5.0,<3.0.0,>=2.0.0