mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
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:
parent
aafd9477df
commit
3867ffb3ef
8 changed files with 91 additions and 99 deletions
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
````````````````````
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# pip requirements file for Salt
|
||||
Jinja2
|
||||
M2Crypto
|
||||
msgpack-python
|
||||
PyCrypto
|
||||
PyYAML
|
||||
|
|
120
salt/crypt.py
120
salt/crypt.py
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue