Replace M2Crypto RSA with PyCrypto.

M2Crypto has been abandonded upstream, has many outstanding bugs,
and doesn't build unpatched on Fedora. Since the only thing M2Crypto
is being used for in the salt codebase is RSA support, using the PyCrypto
RSA module instead is pretty straight forward. Additionally, the RSA
module uses sign/verify terminology around our use of private encrypting
known tokens for verification purposes rather than private_encrypt and
public_descrypt, which makes reading the intent a little clearer.
This commit is contained in:
Monty Taylor 2012-04-19 10:20:03 -07:00
parent aafd9477df
commit 3867ffb3ef
8 changed files with 91 additions and 99 deletions

View file

@ -36,7 +36,9 @@ MOCK_MODULES = [
'zmq',
'Crypto',
'Crypto.Cipher',
'M2Crypto',
'Crypto.Hash',
'Crypto.PublicKey',
'Crypto.Random',
# modules, renderers, states, returners, et al
'MySQLdb',
'MySQLdb.cursors',

View file

@ -104,8 +104,6 @@ Salt should run on any Unix\-like platform so long as the dependencies are met.
.IP \(bu 2
\fI\%pyzmq\fP >= 2.1.9 \- ZeroMQ Python bindings
.IP \(bu 2
\fI\%M2Crypto\fP \- Python OpenSSL wrapper
.IP \(bu 2
\fI\%PyCrypto\fP \- The Python cryptography toolkit
.IP \(bu 2
\fI\%msgpack-python\fP \- High\-performance message interchange format

View file

@ -13,7 +13,6 @@ Salt should run on any Unix-like platform so long as the dependencies are met.
* `Python 2.6`_
* `ZeroMQ`_ >= 2.1.9
* `pyzmq`_ >= 2.1.9 - ZeroMQ Python bindings
* `M2Crypto`_ - Python OpenSSL wrapper
* `PyCrypto`_ - The Python cryptography toolkit
* `msgpack-python`_ - High-performance message interchange format
* `YAML`_ - Python YAML bindings
@ -27,7 +26,6 @@ Optional Dependencies
.. _`Python 2.6`: http://python.org/download/
.. _`ZeroMQ`: http://www.zeromq.org/
.. _`pyzmq`: https://github.com/zeromq/pyzmq
.. _`M2Crypto`: http://chandlerproject.org/Projects/MeTooCrypto
.. _`msgpack-python`: http://pypi.python.org/pypi/msgpack-python/0.1.12
.. _`YAML`: http://pyyaml.org/
.. _`PyCrypto`: http://www.dlitz.net/software/pycrypto/

View file

@ -48,50 +48,48 @@ Install on Windows XP 32bit
7. Install `pyzmq-2.1.11.win32-py2.7.msi`_
8. Install `M2Crypto-0.21.1.win32-py2.7.msi`_
8. Install `pycrypto-2.3.win32-py2.7.msi`_
9. Install `pycrypto-2.3.win32-py2.7.msi`_
9. Install `PyYAML-3.10.win32-py2.7.msi`_
10. Install `PyYAML-3.10.win32-py2.7.msi`_
10. Install `Cython-0.15.1.win32-py2.79.exe`_
11. Install `Cython-0.15.1.win32-py2.79.exe`_
12. Download and run `distribute_setup.py`_
11. Download and run `distribute_setup.py`_
.. code-block:: bash
python distribute_setup.py
13. Download and run `pip`_
12. Download and run `pip`_
.. code-block:: bash
python get-pip.py
14. Add c:\\python27\\scripts to your path
13. Add c:\\python27\\scripts to your path
15. Close terminal window and open a new terminal window (cmd)
14. Close terminal window and open a new terminal window (cmd)
16. Install jinja2
15. Install jinja2
.. code-block:: bash
pip install jinja2
17. Install Messagepack
16. Install Messagepack
.. code-block:: bash
pip install msgpack-python
18. Install Salt
17. Install Salt
.. code-block:: bash
cd ./salt
python setup.py install
19. Edit c:\\etc\\salt\\minon
18. Edit c:\\etc\\salt\\minon
.. code-block:: bash
@ -104,14 +102,14 @@ Install on Windows XP 32bit
open_mode: False
multiprocessing: False
20. Start the salt-minion
19. Start the salt-minion
.. code-block:: bash
cd c:\python27\scripts
python salt-minion
21. On the salt-master accept the new minion's key
20. On the salt-master accept the new minion's key
.. code-block:: bash
@ -119,7 +117,7 @@ Install on Windows XP 32bit
(This accepts all unaccepted keys. If you're concerned about security just accept the key for this specific minion)
22. Test that your minion is responding
21. Test that your minion is responding
a. On the salt-master run:
@ -138,7 +136,6 @@ Install on Windows XP 32bit
.. _vcredist_x86: http://www.microsoft.com/download/en/details.aspx?id=5582
.. _Win32OpenSSL-1_0_0e.exe: http://www.slproweb.com/products/Win32OpenSSL.html
.. _pyzmq-2.1.11.win32-py2.7.msi: https://github.com/zeromq/pyzmq/downloads
.. _M2Crypto-0.21.1.win32-py2.7.msi: http://chandlerproject.org/Projects/MeTooCrypto#Downloads
.. _pycrypto-2.3.win32-py2.7.msi: http://www.voidspace.org.uk/python/modules.shtml#pycrypto
.. _PyYAML-3.10.win32-py2.7.msi: http://pyyaml.org/wiki/PyYAML
.. _Cython-0.15.1.win32-py2.79.exe: http://www.lfd.uci.edu/~gohlke/pythonlibs/#cython

View file

@ -137,12 +137,6 @@ requirements are met.
Refine Security
---------------
Only One Crypt Backend
``````````````````````
Right now we have pycrypto and m2crypto, we need to pick just one. So far
the plan is to dump m2crypto and use just pycrypto.
Make the iv explicit
````````````````````

View file

@ -1,6 +1,5 @@
# pip requirements file for Salt
Jinja2
M2Crypto
msgpack-python
PyCrypto
PyYAML

View file

@ -13,9 +13,13 @@ import logging
import tempfile
# Import Cryptography libs
from M2Crypto import RSA
from Crypto.Cipher import AES
# RSA Support
from Crypto.Hash import MD5
from Crypto.PublicKey import RSA
from Crypto import Random
# Import zeromq libs
import zmq
@ -27,13 +31,6 @@ from salt.exceptions import AuthenticationError
log = logging.getLogger(__name__)
def foo_pass(self, data=''):
'''
used as a workaround for the no-passphrase issue in M2Crypto.RSA
'''
return 'foo'
def gen_keys(keydir, keyname, keysize):
'''
Generate a keypair for use with salt
@ -41,14 +38,17 @@ def gen_keys(keydir, keyname, keysize):
base = os.path.join(keydir, keyname)
priv = '{0}.pem'.format(base)
pub = '{0}.pub'.format(base)
gen = RSA.gen_key(keysize, 1)
privkey = RSA.generate(keysize, Random.new().read)
pubkey = privkey.publickey()
cumask = os.umask(191)
gen.save_key(priv, callback=foo_pass)
with open(priv, "w") as priv_file:
priv_file.write(privkey.exportKey())
os.umask(cumask)
gen.save_pub_key(pub)
key = RSA.load_key(priv, callback=foo_pass)
with open(pub, "w") as pub_file:
pub_file.write(pubkey.exportKey())
os.chmod(priv, 256)
return key
return (pubkey, privkey)
class MasterKeys(dict):
@ -60,37 +60,39 @@ class MasterKeys(dict):
self.opts = opts
self.pub_path = os.path.join(self.opts['pki_dir'], 'master.pub')
self.rsa_path = os.path.join(self.opts['pki_dir'], 'master.pem')
self.key = self.__get_priv_key()
self.pub_str = self.__get_pub_str()
(self.pub_key, self.key) = self.__get_keys()
self.token = self.__gen_token()
def __get_priv_key(self):
def __get_keys(self):
'''
Returns a private key object for the master
Returns a key objects for the master
'''
key = None
try:
key = RSA.load_key(self.rsa_path, callback=foo_pass)
if os.path.exists(self.rsa_path):
try:
key = RSA.importKey(open(self.rsa_path, 'r').read())
except:
key = RSA.importKey(open(self.rsa_path, 'r').read(),
passphrase='foo')
log.debug('Loaded master key: {0}'.format(self.rsa_path))
except:
log.info('Generating master key: {0}'.format(self.rsa_path))
key = gen_keys(self.opts['pki_dir'], 'master', 4096)
return key
def __get_pub_str(self):
'''
Returns the string contents of the public key
'''
if not os.path.isfile(self.pub_path):
key = self.__get_priv_key()
key.save_pub_key(self.pub_path)
return open(self.pub_path, 'r').read()
pub_key = RSA.importKey(open(self.pub_path, 'r').read())
log.debug('Loaded master public key: {0}'.format(self.pub_path))
else:
log.info('Generating keys: {0}'.format(self.opts['pki_dir']))
(pubkey, key) = gen_keys(self.opts['pki_dir'], 'master', 4096)
return (pubkey, key)
def __gen_token(self):
'''
Generate the authentication token
'''
return self.key.private_encrypt('salty bacon', 5)
return self.key.sign('salty bacon', Random.new().read)
def get_pub_str(self):
'''
Return the string representation of the public key
'''
return self.pub_key.exportKey()
class Auth(object):
@ -101,6 +103,7 @@ class Auth(object):
def __init__(self, opts):
self.opts = opts
self.serial = salt.payload.Serial(self.opts)
self.pub_path = os.path.join(self.opts['pki_dir'], 'minion.pub')
self.rsa_path = os.path.join(self.opts['pki_dir'], 'minion.pem')
if 'syndic_master' in self.opts:
self.mpub = 'syndic_master.pub'
@ -109,18 +112,24 @@ class Auth(object):
else:
self.mpub = 'minion_master.pub'
def get_priv_key(self):
def get_keys(self):
'''
Returns a private key object for the minion
Returns a key objects for the minion
'''
key = None
try:
key = RSA.load_key(self.rsa_path, callback=foo_pass)
if os.path.exists(self.rsa_path):
try:
key = RSA.importKey(open(self.rsa_path, 'r').read())
except:
key = RSA.importKey(open(self.rsa_path, 'r').read(),
passphrase='foo')
log.debug('Loaded minion key: {0}'.format(self.rsa_path))
except:
log.info('Generating minion key: {0}'.format(self.rsa_path))
key = gen_keys(self.opts['pki_dir'], 'minion', 4096)
return key
pub_key = RSA.importKey(open(self.pub_path, 'r').read())
log.debug('Loaded minion public key: {0}'.format(self.pub_path))
else:
log.info('Generating keys: {0}'.format(self.opts['pki_dir']))
(pubkey, key) = gen_keys(self.opts['pki_dir'], 'minion', 4096)
return (pubkey, key)
def minion_sign_in_payload(self):
'''
@ -129,15 +138,12 @@ class Auth(object):
public key to encrypt the AES key sent back form the master.
'''
payload = {}
key = self.get_priv_key()
tmp_pub = tempfile.mktemp()
key.save_pub_key(tmp_pub)
(pub, key) = self.get_keys()
payload['enc'] = 'clear'
payload['load'] = {}
payload['load']['cmd'] = '_auth'
payload['load']['id'] = self.opts['id']
payload['load']['pub'] = open(tmp_pub, 'r').read()
os.remove(tmp_pub)
payload['load']['pub'] = pub.exportKey()
return payload
def decrypt_aes(self, aes):
@ -150,24 +156,20 @@ class Auth(object):
Returns the decrypted aes seed key, a string
'''
log.debug('Decrypting the current master AES key')
key = self.get_priv_key()
return key.private_decrypt(aes, 4)
(pub, key) = self.get_keys()
return key.decrypt(aes)
def verify_master(self, master_pub, token):
'''
Takes the master pubkey and compares it to the saved master pubkey,
the token is encrypted with the master private key and must be
decrypted successfully to verify that the master has been connected
to. The token must decrypt with the public key, and it must say:
'salty bacon'
the token is sign with the master private key and must be
verified successfully to verify that the master has been connected
to. The token must verify as signature of the phrase 'salty bacon'
with the public key.
Returns a bool
'''
tmp_pub = tempfile.mktemp()
open(tmp_pub, 'w+').write(master_pub)
m_pub_fn = os.path.join(self.opts['pki_dir'], self.mpub)
pub = RSA.load_pub_key(tmp_pub)
os.remove(tmp_pub)
if os.path.isfile(m_pub_fn) and not self.opts['open_mode']:
local_master_pub = open(m_pub_fn).read()
if not master_pub == local_master_pub:
@ -178,7 +180,8 @@ class Auth(object):
return False
else:
open(m_pub_fn, 'w+').write(master_pub)
if pub.public_decrypt(token, 5) == 'salty bacon':
pub = RSA.importKey(master_pub)
if pub.verify('salty bacon', token):
return True
log.error('The salt master has failed verification for an unknown '
'reason, verify your salt keys')
@ -333,4 +336,5 @@ class SAuth(Auth):
Encrypt a string with the minion private key to verify identity
with the master.
'''
return self.get_priv_key().private_encrypt(clear_tok, 5)
(pub, key) = self.get_keys()
return key.sign(clear_tok, Random.new().read)

View file

@ -18,11 +18,15 @@ import subprocess
# Import zeromq
import zmq
from M2Crypto import RSA
# Import Third Party Libs
import yaml
# RSA Support
from Crypto.Hash import MD5
from Crypto.PublicKey import RSA
from Crypto import Random
# Import salt modules
import salt.crypt
import salt.utils
@ -405,18 +409,14 @@ class AESFuncs(object):
def __verify_minion(self, id_, token):
'''
Take a minion id and a string encrypted with the minion private key
The string needs to decrypt as 'salt' with the minion public key
Take a minion id and a string signed with the minion private key
The string needs to verify as 'salt' with the minion public key
'''
pub_path = os.path.join(self.opts['pki_dir'], 'minions', id_)
with open(pub_path, 'r') as fp_:
minion_pub = fp_.read()
tmp_pub = tempfile.mktemp()
with open(tmp_pub, 'w+') as fp_:
fp_.write(minion_pub)
pub = RSA.load_pub_key(tmp_pub)
os.remove(tmp_pub)
if pub.public_decrypt(token, 5) == 'salt':
pub = RSA.PublicKey.import_key(minion_pub)
if pub.verify("salt", token):
return True
log.error('Salt minion claiming to be {0} has attempted to'
'communicate with the master and could not be verified'
@ -940,13 +940,13 @@ class ClearFuncs(object):
log.info('Authentication accepted from %(id)s', load)
with open(pubfn, 'w+') as fp_:
fp_.write(load['pub'])
key = RSA.load_pub_key(pubfn)
pub = RSA.importKey(load['pub'])
ret = {'enc': 'pub',
'pub_key': self.master_key.pub_str,
'pub_key': self.master_key.get_pub_str(),
'token': self.master_key.token,
'publish_port': self.opts['publish_port'],
}
ret['aes'] = key.public_encrypt(self.opts['aes'], 4)
ret['aes'] = pub.encrypt(self.opts['aes'], Random.new().read)
if self.opts['cluster_masters']:
self._send_cluster()
return ret