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
This commit is contained in:
Eric Brown 2015-07-03 00:20:05 -07:00
parent 3ee98a619f
commit 452fe92787
3 changed files with 52 additions and 154 deletions

View File

@ -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):

View File

@ -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()

View File

@ -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