Skip to content

Commit ea5e8da

Browse files
committed
crypto: replaced openssl with cryptography module
Use cryptography instead of the flaky openssl libraries loading. If the libssl DLLs were not present or were present in another order then the required one, there could be errors when securing the password before sending it to the metadata service. Fixes: #34 Change-Id: I1a2245e199f65f4665071ada9576dcae77a3a432
1 parent 576db31 commit ea5e8da

File tree

5 files changed

+39
-240
lines changed

5 files changed

+39
-240
lines changed

cloudbaseinit/plugins/common/setuserpassword.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
1414

15-
import base64
16-
1715
from oslo_log import log as oslo_logging
1816

1917
from cloudbaseinit import conf as cloudbaseinit_conf
@@ -23,7 +21,6 @@
2321
from cloudbaseinit.plugins.common import constants as plugin_constant
2422
from cloudbaseinit.utils import crypt
2523

26-
2724
CONF = cloudbaseinit_conf.CONF
2825
LOG = oslo_logging.getLogger(__name__)
2926

@@ -32,9 +29,7 @@ class SetUserPasswordPlugin(base.BasePlugin):
3229

3330
def _encrypt_password(self, ssh_pub_key, password):
3431
cm = crypt.CryptManager()
35-
with cm.load_ssh_rsa_public_key(ssh_pub_key) as rsa:
36-
enc_password = rsa.public_encrypt(password.encode())
37-
return base64.b64encode(enc_password)
32+
return cm.public_encrypt(ssh_pub_key, password)
3833

3934
def _get_password(self, service, shared_data):
4035
injected = False

cloudbaseinit/tests/plugins/common/test_setuserpassword.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,25 +37,17 @@ def setUp(self):
3737
self.fake_data = fake_json_response.get_fake_metadata_json(
3838
'2013-04-04')
3939

40-
@mock.patch('base64.b64encode')
4140
@mock.patch('cloudbaseinit.utils.crypt.CryptManager'
42-
'.load_ssh_rsa_public_key')
43-
def test_encrypt_password(self, mock_load_ssh_key, mock_b64encode):
44-
mock_rsa = mock.MagicMock()
45-
fake_ssh_pub_key = 'fake key'
41+
'.public_encrypt')
42+
def test_encrypt_password(self, mock_public_encrypt):
43+
fake_ssh_pub_key = 'ssh-rsa key'
4644
fake_password = 'fake password'
47-
mock_load_ssh_key.return_value = mock_rsa
48-
mock_rsa.__enter__().public_encrypt.return_value = 'public encrypted'
49-
mock_b64encode.return_value = 'encrypted password'
50-
45+
fake_encrypt_pwd = 'encrypted password'
46+
mock_public_encrypt.return_value = fake_encrypt_pwd
5147
response = self._setpassword_plugin._encrypt_password(
5248
fake_ssh_pub_key, fake_password)
5349

54-
mock_load_ssh_key.assert_called_with(fake_ssh_pub_key)
55-
mock_rsa.__enter__().public_encrypt.assert_called_with(
56-
b'fake password')
57-
mock_b64encode.assert_called_with('public encrypted')
58-
self.assertEqual('encrypted password', response)
50+
self.assertEqual(fake_encrypt_pwd, response)
5951

6052
def _test_get_password(self, inject_password):
6153
shared_data = {}

cloudbaseinit/tests/utils/test_crypt.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,13 @@
1616

1717
from cloudbaseinit.utils import crypt
1818

19-
20-
class TestOpenSSLException(unittest.TestCase):
21-
22-
def setUp(self):
23-
self._openssl = crypt.OpenSSLException()
24-
25-
def test_get_openssl_error_msg(self):
26-
expected_err_msg = u'error:00000000:lib(0):func(0):reason(0)'
27-
expected_err_msg_py10 = u'error:00000000:lib(0)::reason(0)'
28-
err_msg = self._openssl._get_openssl_error_msg()
29-
self.assertIn(err_msg, [expected_err_msg, expected_err_msg_py10])
19+
PUB_KEY = '''
20+
AAAAB3NzaC1yc2EAAAADAQABAAABAQDP1e9IAYXwwUKuFtoReGXidwnM1RuXWB53IO0Hg
21+
mbZArXvEIOfgm/l6IsOJwF7znOBn0hClW7ZONPweX1Al9Hy/LInX1x96Aamq4yyKQCmHDiuZc7Qwu
22+
xr82Ph8XfWic/wo4es/ODSYeFT5NoFDhsYII8O9EGoubpQdakxt9skX0X+zg8TYPuIOANGhlaN8nn
23+
U7gYbO7Gt9vZDmYeRACthNzCIg+w38oxmcgmQqQHxPEp4tUtuFfpjptyVvHz273QvisbdymD3RO0L
24+
9oGMdKzjGgcdE1VuhXuucnUWlZuKe7BirxF8glF5NHKzWto67lDRzVI/F1snkTAorm5EWkA9 test
25+
'''
3026

3127

3228
class TestCryptManager(unittest.TestCase):
@@ -36,6 +32,16 @@ def setUp(self):
3632

3733
def test_load_ssh_rsa_public_key_invalid(self):
3834
ssh_pub_key = "ssh"
39-
exc = Exception
40-
self.assertRaises(exc, self._crypt_manager.load_ssh_rsa_public_key,
41-
ssh_pub_key)
35+
exc = crypt.CryptException
36+
self.assertRaises(exc, self._crypt_manager.public_encrypt,
37+
ssh_pub_key, '')
38+
39+
def test_encrypt_password(self):
40+
ssh_pub_key = "ssh-rsa " + PUB_KEY.replace('\n', "")
41+
password = 'testpassword'
42+
43+
response = self._crypt_manager.public_encrypt(
44+
ssh_pub_key, password)
45+
46+
self.assertTrue(len(response) > 0)
47+
self.assertTrue(isinstance(response, bytes))

cloudbaseinit/utils/crypt.py

Lines changed: 11 additions & 206 deletions
Original file line numberDiff line numberDiff line change
@@ -13,223 +13,28 @@
1313
# under the License.
1414

1515
import base64
16-
import ctypes
17-
import ctypes.util
18-
import struct
19-
import sys
2016

21-
clib_path = ctypes.util.find_library("c")
22-
23-
if sys.platform == "win32":
24-
if clib_path:
25-
clib = ctypes.CDLL(clib_path)
26-
else:
27-
clib = ctypes.cdll.ucrtbase
28-
29-
# for backwards compatibility, try the older names
30-
# libcrypto-1_1 comes bundled with PY 3.7 to 3.12
31-
ssl_lib_names = [
32-
"libcrypto-1_1",
33-
"libcrypto",
34-
"libeay32"
35-
]
36-
37-
for ssl_lib_name in ssl_lib_names:
38-
try:
39-
openssl = ctypes.CDLL(ssl_lib_name)
40-
break
41-
except Exception:
42-
pass
43-
else:
44-
clib = ctypes.CDLL(clib_path)
45-
openssl_lib_path = ctypes.util.find_library("ssl")
46-
openssl = ctypes.CDLL(openssl_lib_path)
47-
48-
49-
class RSA(ctypes.Structure):
50-
_fields_ = [
51-
("pad", ctypes.c_int),
52-
("version", ctypes.c_long),
53-
("meth", ctypes.c_void_p),
54-
("engine", ctypes.c_void_p),
55-
("n", ctypes.c_void_p),
56-
("e", ctypes.c_void_p),
57-
("d", ctypes.c_void_p),
58-
("p", ctypes.c_void_p),
59-
("q", ctypes.c_void_p),
60-
("dmp1", ctypes.c_void_p),
61-
("dmq1", ctypes.c_void_p),
62-
("iqmp", ctypes.c_void_p),
63-
("sk", ctypes.c_void_p),
64-
("dummy", ctypes.c_int),
65-
("references", ctypes.c_int),
66-
("flags", ctypes.c_int),
67-
("_method_mod_n", ctypes.c_void_p),
68-
("_method_mod_p", ctypes.c_void_p),
69-
("_method_mod_q", ctypes.c_void_p),
70-
("bignum_data", ctypes.c_char_p),
71-
("blinding", ctypes.c_void_p),
72-
("mt_blinding", ctypes.c_void_p)
73-
]
74-
75-
76-
openssl.RSA_PKCS1_PADDING = 1
77-
78-
openssl.RSA_new.restype = ctypes.POINTER(RSA)
79-
80-
openssl.BN_bin2bn.restype = ctypes.c_void_p
81-
openssl.BN_bin2bn.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.c_void_p]
82-
83-
openssl.BN_new.restype = ctypes.c_void_p
84-
85-
openssl.RSA_size.restype = ctypes.c_int
86-
openssl.RSA_size.argtypes = [ctypes.POINTER(RSA)]
87-
88-
openssl.RSA_public_encrypt.argtypes = [ctypes.c_int,
89-
ctypes.c_char_p,
90-
ctypes.c_char_p,
91-
ctypes.POINTER(RSA),
92-
ctypes.c_int]
93-
openssl.RSA_public_encrypt.restype = ctypes.c_int
94-
95-
openssl.RSA_free.argtypes = [ctypes.POINTER(RSA)]
96-
97-
openssl.PEM_write_RSAPublicKey.restype = ctypes.c_int
98-
openssl.PEM_write_RSAPublicKey.argtypes = [ctypes.c_void_p,
99-
ctypes.POINTER(RSA)]
100-
101-
openssl.ERR_get_error.restype = ctypes.c_long
102-
openssl.ERR_get_error.argtypes = []
103-
104-
openssl.ERR_error_string_n.restype = ctypes.c_void_p
105-
openssl.ERR_error_string_n.argtypes = [ctypes.c_long,
106-
ctypes.c_char_p,
107-
ctypes.c_int]
108-
109-
try:
110-
openssl.ERR_load_crypto_strings.restype = ctypes.c_int
111-
openssl.ERR_load_crypto_strings.argtypes = []
112-
except AttributeError:
113-
# NOTE(avladu): This function is deprecated and no longer needed
114-
# since OpenSSL 1.1
115-
pass
116-
117-
clib.fopen.restype = ctypes.c_void_p
118-
clib.fopen.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
119-
120-
clib.fclose.restype = ctypes.c_int
121-
clib.fclose.argtypes = [ctypes.c_void_p]
17+
from cryptography.hazmat import backends
18+
from cryptography.hazmat.primitives.asymmetric import padding
19+
from cryptography.hazmat.primitives import serialization
12220

12321

12422
class CryptException(Exception):
12523
pass
12624

12725

128-
class OpenSSLException(CryptException):
129-
130-
def __init__(self):
131-
message = self._get_openssl_error_msg()
132-
super(OpenSSLException, self).__init__(message)
133-
134-
def _get_openssl_error_msg(self):
135-
try:
136-
openssl.ERR_load_crypto_strings()
137-
except AttributeError:
138-
pass
139-
140-
errno = openssl.ERR_get_error()
141-
errbuf = ctypes.create_string_buffer(1024)
142-
openssl.ERR_error_string_n(errno, errbuf, 1024)
143-
return errbuf.value.decode("ascii")
144-
145-
146-
class RSAWrapper(object):
147-
148-
def __init__(self, rsa_p):
149-
self._rsa_p = rsa_p
150-
151-
def __enter__(self):
152-
return self
153-
154-
def __exit__(self, tp, value, tb):
155-
self.free()
156-
157-
def free(self):
158-
openssl.RSA_free(self._rsa_p)
159-
160-
def public_encrypt(self, clear_text):
161-
flen = len(clear_text)
162-
rsa_size = openssl.RSA_size(self._rsa_p)
163-
enc_text = ctypes.create_string_buffer(rsa_size)
164-
165-
enc_text_len = openssl.RSA_public_encrypt(flen,
166-
clear_text,
167-
enc_text,
168-
self._rsa_p,
169-
openssl.RSA_PKCS1_PADDING)
170-
if enc_text_len == -1:
171-
raise OpenSSLException()
172-
173-
return enc_text[:enc_text_len]
174-
175-
17626
class CryptManager(object):
17727

178-
def load_ssh_rsa_public_key(self, ssh_pub_key):
28+
def public_encrypt(self, ssh_pub_key, password):
17929
ssh_rsa_prefix = "ssh-rsa "
18030

18131
if not ssh_pub_key.startswith(ssh_rsa_prefix):
18232
raise CryptException('Invalid SSH key')
18333

184-
s = ssh_pub_key[len(ssh_rsa_prefix):]
185-
idx = s.find(' ')
186-
if idx >= 0:
187-
b64_pub_key = s[:idx]
188-
else:
189-
b64_pub_key = s
190-
191-
pub_key = base64.b64decode(b64_pub_key)
192-
193-
offset = 0
194-
195-
key_type_len = struct.unpack('>I', pub_key[offset:offset + 4])[0]
196-
offset += 4
197-
198-
key_type = pub_key[offset:offset + key_type_len].decode('utf-8')
199-
offset += key_type_len
200-
201-
if key_type not in ['ssh-rsa', 'rsa', 'rsa1']:
202-
raise CryptException('Unsupported SSH key type "%s". '
203-
'Only RSA keys are currently supported'
204-
% key_type)
205-
206-
rsa_p = openssl.RSA_new()
207-
try:
208-
rsa_p.contents.e = openssl.BN_new()
209-
rsa_p.contents.n = openssl.BN_new()
210-
211-
e_len = struct.unpack('>I', pub_key[offset:offset + 4])[0]
212-
offset += 4
213-
214-
e_key_bin = pub_key[offset:offset + e_len]
215-
offset += e_len
216-
217-
if not openssl.BN_bin2bn(e_key_bin, e_len, rsa_p.contents.e):
218-
raise OpenSSLException()
219-
220-
n_len = struct.unpack('>I', pub_key[offset:offset + 4])[0]
221-
offset += 4
222-
223-
n_key_bin = pub_key[offset:offset + n_len]
224-
offset += n_len
225-
226-
if offset != len(pub_key):
227-
raise CryptException('Invalid SSH key')
228-
229-
if not openssl.BN_bin2bn(n_key_bin, n_len, rsa_p.contents.n):
230-
raise OpenSSLException()
231-
232-
return RSAWrapper(rsa_p)
233-
except Exception:
234-
openssl.RSA_free(rsa_p)
235-
raise
34+
rsa_public_key = serialization.load_ssh_public_key(
35+
ssh_pub_key.encode(), backends.default_backend())
36+
enc_password = rsa_public_key.encrypt(
37+
password.encode(),
38+
padding.PKCS1v15()
39+
)
40+
return base64.b64encode(enc_password)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ PyYAML
1212
requests
1313
untangle==1.2.1
1414
jinja2
15+
cryptography
1516
pywin32;sys_platform=="win32"
1617
comtypes;sys_platform=="win32"
1718
pymi;sys_platform=="win32"

0 commit comments

Comments
 (0)