diff --git a/nova/crypto.py b/nova/crypto.py index 5c48c60b688b..96e54589374a 100644 --- a/nova/crypto.py +++ b/nova/crypto.py @@ -24,9 +24,15 @@ Includes root and intermediate CAs, SSH key_pairs and x509 certificates. from __future__ import absolute_import +import base64 import hashlib import os +import re import string +import struct + +from pyasn1.codec.der import encoder as der_encoder +from pyasn1.type import univ from nova import context from nova import db @@ -181,23 +187,75 @@ def decrypt_text(project_id, text): 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 = eval( + '0x' + ''.join(['%02X' % struct.unpack('B', x)[0] for x in parts[2]])) + e_val = eval( + '0x' + ''.join(['%02X' % struct.unpack('B', x)[0] for x in parts[1]])) + pkinfo = _to_sequence(univ.Integer(n_val), univ.Integer(e_val)) + + # Convert the sequence into a bit string + pklong = long(der_encoder.encode(pkinfo).encode('hex'), 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.encodestring(der_encoder.encode(pkcs1_seq)) + + # 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)) + + def ssh_encrypt_text(ssh_public_key, text): """Encrypt text with an ssh public key. - - Requires recent ssh-keygen binary in addition to openssl binary. """ with utils.tempdir() as tmpdir: - sshkey = os.path.abspath(os.path.join(tmpdir, 'ssh.key')) - with open(sshkey, 'w') as f: - f.write(ssh_public_key) sslkey = os.path.abspath(os.path.join(tmpdir, 'ssl.key')) try: - # NOTE(vish): -P is to skip prompt on bad keys - out, _err = utils.execute('ssh-keygen', - '-P', '', - '-e', - '-f', sshkey, - '-m', 'PKCS8') + out = convert_from_sshrsa_to_pkcs8(ssh_public_key) with open(sslkey, 'w') as f: f.write(out) enc, _err = utils.execute('openssl', diff --git a/nova/tests/test_crypto.py b/nova/tests/test_crypto.py index 25df336fb10a..cec3ca9c1f9b 100644 --- a/nova/tests/test_crypto.py +++ b/nova/tests/test_crypto.py @@ -212,3 +212,31 @@ e6fCXWECgYEAqgpGvva5kJ1ISgNwnJbwiNw0sOT9BMOsdNZBElf0kJIIy6FMPvap def test_ssh_encrypt_failure(self): self.assertRaises(exception.EncryptionFailure, 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, '') diff --git a/tools/pip-requires b/tools/pip-requires index 126f0125c84f..d7e48ff87bb3 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -16,6 +16,7 @@ sqlalchemy-migrate>=0.7.2 netaddr suds==0.4 paramiko +pyasn1 Babel>=0.9.6 iso8601>=0.1.4 httplib2