From: Ondrej Lichtner olichtne@redhat.com
First of all, this isn't a patchset to be pulled into the repository yet, just a request for comments to see if anything can be improved, while I work on finishing some minor problem areas: * this breaks the lnst-pool-wizard because it doesn't use SecureSockets yet * this breaks the lnst-ctl deconfigure command for the same reason
The following patch set implements secure communication between the Controller and the Slave. It implements 4 different authentication mechanisms: none - no authentication, just DH secret negotiation and encryption password - password authenticated secret negotiation using SRP-6a protocol and encryption pubkey - DH secret negotiation, authentication by using a private-public key pairs ssh - the same as pubkey, but uses ssh keys already present on the system
By default, both the Controller and Slave assume the "none" mechanism so everything should work just as before without any additional work needed. To try the other mechanisms you'll need to configure both the Controller and the Slave as follows: password: on Slave edit the lnst-slave.conf like this: [security] auth_types = password auth_password = your_chosen_password
on Controller edit the SlaveMachineXML file of the slave and add this: <security> <auth_type>password</auth_type> <auth_password>your_chosen_password</auth_password> </security> under the main <slavemachine> element
pubkey: generate a private-public key pair on both the Slave and the Controller and exchange the public keys between the machines.
on Slave edit the lnst-slave.conf file like this: [security] auth_types = pubkey privkey = path/to/slave_private_key ctl_pubkeys = path/to/pubkeys_dir/
on Controller edit the SlaveMachineXML and add this: <security> <auth_type>pubkey</auth_type> <pubkey_path>path/to/slave_pubkey</pubkey_path> </security> under the main <slavemachine> element and edit the lnst-ctl.conf like this: [security] identity = ctl_name privkey = path/to/ctl_privatekey
On the Slave the Controller public key needs to be placed into the ctl_pubkeys directory and the filename must match the value of the identity option in the lnst-ctl.conf
ssh: have ssh presetup between the Controller and Slave machines - the slave needs its sshd private keys to be located in /etc/ssh (ssh_host.*key files) and the controllers public key must be in the ~/.ssh/authorized_keys file On the Controller you need to have the ~/.ssh/id_rsa file to contain a private RSA key (the public part is in the slaves authorized_keys file) and you also need to have at least one of the Slaves public keys located in the ~/.ssh/known_hosts file
you can easily achieve this by just starting the sshd daemon on the Slave machine and on the Controller generate the key with ssh-keygen -t rsa -C "your_email@example.com" and run ssh-copy-id username@slavehostname
then configure the LNST Slave - lnst-slave.conf: [security] auth_types = ssh
and on the Controller edit the Slave Machine XML file like this: <security> <auth_type>pubkey</auth_type> <pubkey_path>path/to/slave_pubkey</pubkey_path> </security>
Ondrej Lichtner (7): Config: add get_section_values method Config: add security section and options to config Add SecureSocket classes SlaveMachineXML: add security information Machine: add security parameters of the slave machine Use SecureSockets for Ctl<->Slave communication Machine: rename method configure to init_connection
lnst/Common/Config.py | 44 ++++ lnst/Common/ConnectionHandler.py | 39 +--- lnst/Common/SecureSocket.py | 377 ++++++++++++++++++++++++++++++++++ lnst/Controller/CtlSecSocket.py | 318 ++++++++++++++++++++++++++++ lnst/Controller/Machine.py | 17 +- lnst/Controller/NetTestController.py | 2 +- lnst/Controller/SlaveMachineParser.py | 31 +++ lnst/Controller/SlavePool.py | 9 +- lnst/Slave/NetTestSlave.py | 13 +- lnst/Slave/SlaveSecSocket.py | 323 +++++++++++++++++++++++++++++ schema-sm.rng | 29 +++ 11 files changed, 1163 insertions(+), 39 deletions(-) create mode 100644 lnst/Common/SecureSocket.py create mode 100644 lnst/Controller/CtlSecSocket.py create mode 100644 lnst/Slave/SlaveSecSocket.py
From: Ondrej Lichtner olichtne@redhat.com
This method returns all the option values in a specific section. Previously we just had the get_section method which returns the internal dict structure of the Config class which is not very usable in the application.
Signed-off-by: Ondrej Lichtner olichtne@redhat.com --- lnst/Common/Config.py | 10 ++++++++++ 1 file changed, 10 insertions(+)
diff --git a/lnst/Common/Config.py b/lnst/Common/Config.py index 97a82e4..de7ba4a 100644 --- a/lnst/Common/Config.py +++ b/lnst/Common/Config.py @@ -155,6 +155,16 @@ class Config(): raise ConfigError(msg) return self._options[section]
+ def get_section_values(self, section): + if section not in self._options: + msg = 'Unknow section: %s' % section + raise ConfigError(msg) + + res = {} + for opt_name, opt in self._options[section].items(): + res[opt_name] = opt["value"] + return res + def get_option(self, section, option): sect = self.get_section(section) if option not in sect:
From: Ondrej Lichtner olichtne@redhat.com
This adds some basic security parameters that will be used by the lnst-slave and lnst-ctl applications.
On the Controller it's just 2 options: identity - a name that the controller uses to identify to the Slave, Slave stores public keys of controllers with their identification privkey - the path to the private key of the Controller
Both of options are only used in case we're using the pubkey authetication of the Slave.
On the Slave it's these options: auth_types - the accepted authentication types, at this moment it only takes a single value but in the future it will be a list of authentication types provided to Controllers. Possible values are: none, password, pubkey, ssh auth_password - if auth_types == password then this password will be used for authentication privkey - path to the Slaves private key that will be used when auth_types == pubkey ctl_pubkeys - path to directory where the Slave should look for public keys of Controllers when auth_types == pubkey
Signed-off-by: Ondrej Lichtner olichtne@redhat.com --- lnst/Common/Config.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+)
diff --git a/lnst/Common/Config.py b/lnst/Common/Config.py index de7ba4a..31481b2 100644 --- a/lnst/Common/Config.py +++ b/lnst/Common/Config.py @@ -97,6 +97,18 @@ class Config():
self._options['pools'] = dict()
+ self._options['security'] = dict() + self._options['security']['identity'] = {\ + "value" : "", + "additive" : False, + "action" : self.optionPlain, + "name" : "identity"} + self._options['security']['privkey'] = {\ + "value" : "", + "additive" : False, + "action" : self.optionPath, + "name" : "privkey"} + self.colours_scheme()
def slave_init(self): @@ -132,6 +144,28 @@ class Config(): "action" : self.optionTimeval, "name" : "expiration_period"}
+ self._options['security'] = dict() + self._options['security']['auth_types'] = {\ + "value" : "none", + "additive" : False, + "action" : self.optionPlain, #TODO list?? + "name" : "auth_types"} + self._options['security']['auth_password'] = {\ + "value" : "", + "additive" : False, + "action" : self.optionPlain, + "name" : "auth_password"} + self._options['security']['privkey'] = {\ + "value" : "", + "additive" : False, + "action" : self.optionPath, + "name" : "privkey"} + self._options['security']['ctl_pubkeys'] = {\ + "value" : "", + "additive" : False, + "action" : self.optionPath, + "name" : "ctl_pubkeys"} + self.colours_scheme()
def colours_scheme(self):
From: Ondrej Lichtner olichtne@redhat.com
Added 3 new classes: SecureSocket, CtlSecSocket and SlaveSecSocket. The SecureSocket class encapsulates a normal socket object and adds what is basically a TLS Record Layer - adds a signature, padding and encrypts the entire message.
The CtlSecSocket and SlaveSecSocket classes are child classes inheriting from the SecureSocket class and implement the handshake protocols used from the Controller or from the Slave side. The important method is handshake() that takes a sec_params parameter which is a dictionary describing how to do the handshake.
Signed-off-by: Ondrej Lichtner olichtne@redhat.com --- lnst/Common/SecureSocket.py | 377 ++++++++++++++++++++++++++++++++++++++++ lnst/Controller/CtlSecSocket.py | 318 +++++++++++++++++++++++++++++++++ lnst/Slave/SlaveSecSocket.py | 323 ++++++++++++++++++++++++++++++++++ 3 files changed, 1018 insertions(+) create mode 100644 lnst/Common/SecureSocket.py create mode 100644 lnst/Controller/CtlSecSocket.py create mode 100644 lnst/Slave/SlaveSecSocket.py
diff --git a/lnst/Common/SecureSocket.py b/lnst/Common/SecureSocket.py new file mode 100644 index 0000000..d319c54 --- /dev/null +++ b/lnst/Common/SecureSocket.py @@ -0,0 +1,377 @@ +""" +This module defines a SecureSocket class that wraps the normal socket by adding +TLS-like functionality of providing data integrity, confidentiality and +authenticity. The reason why we're not using TLS is because the Python +implementation enforces the use of certificates and we want to also allow +password based authentication. This implements the common class, and the Slave +and Controller implement their sides of the handshake algorithms. + +Copyright 2016 Red Hat, Inc. +Licensed under the GNU General Public License, version 2 as +published by the Free Software Foundation; see COPYING for details. +""" + +__author__ = """ +olichtne@redhat.com (Ondrej Lichtner) +""" + +import os +import cPickle +import hashlib +import hmac +import cryptography.exceptions +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives.asymmetric import padding, ec +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey +from cryptography.hazmat.primitives.asymmetric.dsa import DSAPrivateKey +from cryptography.hazmat.primitives.asymmetric.dsa import DSAPublicKey +from cryptography.hazmat.backends import default_backend + +DH_GROUP = {"p": int("0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1"\ + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD"\ + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245"\ + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED"\ + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D"\ + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F"\ + "83655D23DCA3AD961C62F356208552BB9ED529077096966D"\ + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B"\ + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9"\ + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510"\ + "15728E5A8AACAA68FFFFFFFFFFFFFFFF", 16), + "g": 2} + +DH_GROUP["q"] = (DH_GROUP["p"]-1)/2 +DH_GROUP["q_size"] = DH_GROUP["q"].bit_length()/8 +if DH_GROUP["q"].bit_length()%8: + DH_GROUP["q_size"] += 1 +DH_GROUP["p_size"] = DH_GROUP["p"].bit_length()/8 +if DH_GROUP["p"].bit_length()%8: + DH_GROUP["p_size"] += 1 + +SRP_GROUP = {"p": int("0xAC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC" + "3192943DB56050A37329CBB4A099ED8193E0757767A13DD52312" + "AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF6095179A" + "163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975E" + "EAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773BCA" + "97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32" + "E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF5" + "2FB3786160279004E57AE6AF874E7303CE53299CCC041C7BC308" + "D82A5698F3A8D0C38271AE35F8E9DBFBB694B5C803D89F7AE435" + "DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73", 16), + "g": 2} + +SRP_GROUP["q"] = (SRP_GROUP["p"]-1)/2 +SRP_GROUP["q_size"] = SRP_GROUP["q"].bit_length()/8 +if SRP_GROUP["q"].bit_length()%8: + SRP_GROUP["q_size"] += 1 +SRP_GROUP["p_size"] = SRP_GROUP["p"].bit_length()/8 +if SRP_GROUP["p"].bit_length()%8: + SRP_GROUP["p_size"] += 1 + +class SecSocketException(Exception): + pass + +class SecureSocket(object): + def __init__(self, soc): + self._role = None + self._socket = soc + + self._master_secret = "" + + self._ctl_random = None + self._slave_random = None + + self._current_write_spec = {"enc_key": None, + "mac_key": None, + "seq_num": 0} + self._current_read_spec = {"enc_key": None, + "mac_key": None, + "seq_num": 0} + self._next_write_spec = {"enc_key": None, + "mac_key": None, + "seq_num": 0} + self._next_read_spec = {"enc_key": None, + "mac_key": None, + "seq_num": 0} + + def send_msg(self, msg): + pickled_msg = cPickle.dumps(msg) + return self.send(pickled_msg) + + def recv_msg(self): + pickled_msg = self.recv() + if pickled_msg == "": + raise SecSocketException("Disconnected") + msg = cPickle.loads(pickled_msg) + return msg + + def _add_mac_sign(self, data): + if not self._current_write_spec["mac_key"]: + return data + + msg = str(self._current_write_spec["seq_num"]) + str(len(data)) + data + signature = hmac.new(self._current_write_spec["mac_key"], + msg, + hashlib.sha256) + signed_msg = {"data": data, + "signature": signature.digest()} + return cPickle.dumps(signed_msg) + + def _del_mac_sign(self, signed_data): + if not self._current_read_spec["mac_key"]: + return signed_data + + signed_msg = cPickle.loads(signed_data) + data = signed_msg["data"] + msg = str(self._current_read_spec["seq_num"]) + str(len(data)) + data + + signature = hmac.new(self._current_read_spec["mac_key"], + msg, + hashlib.sha256) + + if signature.digest() != signed_msg["signature"]: + return None + return data + + def _add_padding(self, data): + if not self._current_write_spec["enc_key"]: + return data + + block_size = algorithms.AES.block_size/8 + pad_length = block_size - (len(data) % block_size) + pad_char = ("%02x" % pad_length).decode("hex") + padding = pad_length * pad_char + + padded_data = data+padding + return padded_data + + def _del_padding(self, data): + if not self._current_read_spec["enc_key"]: + return data + + pad_length = int(data[-1].encode("hex"), 16) + for char in data[-pad_length]: + if int(char.encode("hex"), 16) != pad_length: + return None + + return data[:-pad_length] + + def _add_encrypt(self, data): + if not self._current_write_spec["enc_key"]: + return data + + iv = os.urandom(algorithms.AES.block_size/8) + mode = modes.CBC(iv) + key = self._current_write_spec["enc_key"] + cipher = Cipher(algorithms.AES(key), mode, default_backend()) + encryptor = cipher.encryptor() + + encrypted_data = encryptor.update(data) + encryptor.finalize() + + encrypted_msg = {"iv": iv, + "enc_data": encrypted_data} + + return cPickle.dumps(encrypted_msg) + + def _del_encrypt(self, data): + if not self._current_read_spec["enc_key"]: + return data + + encrypted_msg = cPickle.loads(data) + encrypted_data = encrypted_msg["enc_data"] + + iv = encrypted_msg["iv"] + mode = modes.CBC(iv) + key = self._current_read_spec["enc_key"] + cipher = Cipher(algorithms.AES(key), mode, default_backend()) + decryptor = cipher.decryptor() + + decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize() + + return decrypted_data + + def _protect_data(self, data): + signed = self._add_mac_sign(data) + padded = self._add_padding(signed) + encrypted = self._add_encrypt(padded) + + self._current_write_spec["seq_num"] += 1 + return encrypted + + def _uprotect_data(self, encrypted): + padded = self._del_encrypt(encrypted) + signed = self._del_padding(padded) + + if signed is None: + #preventing timing attacks + self._del_mac_sign(padded) + return None + + data = self._del_mac_sign(signed) + + self._current_read_spec["seq_num"] += 1 + return data + + def send(self, data): + protected_data = self._protect_data(data) + + transmit_data = str(len(protected_data)) + " " + protected_data + + return self._socket.sendall(transmit_data) + + def recv(self): + length = "" + while True: + c = self._socket.recv(1) + if c == ' ': + length = int(length) + break + elif c == "": + return "" + else: + length += c + data = "" + + while len(data) < length: + c = self._socket.recv(length - len(data)) + if c == "": + return "" + else: + data += c + + msg = self._uprotect_data(data) + if msg is None: + return self.recv() + return self._handle_internal(msg) + + def _handle_internal(self, orig_msg): + try: + msg = cPickle.loads(orig_msg) + except: + return orig_msg + if "type" in msg and msg["type"] == "change_cipher_spec": + self._change_read_cipher_spec() + return self.recv() + else: + return orig_msg + + def _send_change_cipher_spec(self): + change_cipher_spec_msg = {"type": "change_cipher_spec"} + self.send_msg(change_cipher_spec_msg) + self._change_write_cipher_spec() + return + + def fileno(self): + """needed to work with select()""" + return self._socket.fileno() + + def close(self): + return self._socket.close() + + def shutdown(self, how): + return self._socket.shutdown(how) + + def _change_read_cipher_spec(self): + self._current_read_spec = self._next_read_spec + self._next_read_spec = {"enc_key": None, + "mac_key": None, + "seq_num": 0} + return + + def _change_write_cipher_spec(self): + self._current_write_spec = self._next_write_spec + self._next_write_spec = {"enc_key": None, + "mac_key": None, + "seq_num": 0} + return + + def p_SHA256(self, secret, seed, length): + prev_a = seed + result = "" + while len(result) < length: + a = hmac.new(secret, msg=prev_a, digestmod=hashlib.sha256) + prev_a = a.digest() + hmac_hash = hmac.new(secret, + msg=a.digest()+seed, + digestmod=hashlib.sha256) + result += hmac_hash.digest() + return result[:length] + + def PRF(self, secret, label, seed, length): + return self.p_SHA256(secret, label+seed, length) + + def _init_cipher_spec(self): + if self._role == "server": + client_spec = self._next_read_spec + server_spec = self._next_write_spec + elif self._role == "client": + client_spec = self._next_write_spec + server_spec = self._next_read_spec + else: + raise SecSocketException("Socket without a role!") + + aes_keysize = max(algorithms.AES.key_sizes)/8 + mac_keysize = hashlib.sha256().block_size + + prf_seq = self.PRF(self._master_secret, + "key expansion", + self._slave_random + self._ctl_random, + 2*aes_keysize + 2*mac_keysize) + + client_spec["enc_key"] = prf_seq[:aes_keysize] + prf_seq = prf_seq[aes_keysize:] + server_spec["enc_key"] = prf_seq[:aes_keysize] + prf_seq = prf_seq[aes_keysize:] + + client_spec["mac_key"] = prf_seq[:mac_keysize] + prf_seq = prf_seq[mac_keysize:] + server_spec["mac_key"] = prf_seq[:mac_keysize] + prf_seq = prf_seq[mac_keysize:] + return + + def _sign_data(self, data, privkey): + if isinstance(privkey, DSAPrivateKey): + signer = privkey.signer(hashes.SHA256()) + elif isinstance(privkey, RSAPrivateKey): + signer = privkey.signer(padding.PSS(padding.MGF1(hashes.SHA256()), + padding.PSS.MAX_LENGTH), + hashes.SHA256()) + elif isinstance(privkey, EllipticCurvePrivateKey): + signer = privkey.signer(ec.ECDSA(hashes.SHA256())) + else: + raise SecSocketException("Unsupported Assymetric Key!") + + signer.update(data) + return signer.finalize() + + def _verify_signature(self, pubkey, data, signature): + if isinstance(pubkey, DSAPublicKey): + verifier = pubkey.verifier(signature, hashes.SHA256()) + elif isinstance(pubkey, RSAPublicKey): + verifier = pubkey.verifier(signature, + padding.PSS(padding.MGF1(hashes.SHA256()), + padding.PSS.MAX_LENGTH), + hashes.SHA256()) + elif isinstance(pubkey, EllipticCurvePublicKey): + verifier = pubkey.verifier(signature, ec.ECDSA(hashes.SHA256())) + else: + raise SecSocketException("Unsupported Assymetric Key!") + + verifier.update(data) + try: + verifier.verify() + except cryptography.exceptions.InvalidSignature: + return False + except: + return False + return True + + def _cmp_pub_keys(self, first, second): + if first.public_numbers() != second.public_numbers(): + return False + else: + return True diff --git a/lnst/Controller/CtlSecSocket.py b/lnst/Controller/CtlSecSocket.py new file mode 100644 index 0000000..db27289 --- /dev/null +++ b/lnst/Controller/CtlSecSocket.py @@ -0,0 +1,318 @@ +""" +The CtlSecSocket implements the controller (client) side of the handshake +protocols. + +Copyright 2016 Red Hat, Inc. +Licensed under the GNU General Public License, version 2 as +published by the Free Software Foundation; see COPYING for details. +""" + +__author__ = """ +olichtne@redhat.com (Ondrej Lichtner) +""" + +import os +import hashlib +import math +from lnst.Common.SecureSocket import SecureSocket +from lnst.Common.SecureSocket import DH_GROUP, SRP_GROUP +from lnst.Common.SecureSocket import SecSocketException +from lnst.Common.Config import lnst_config +from cryptography.hazmat.primitives import serialization as ser +from cryptography.hazmat.primitives.serialization import load_pem_private_key +from cryptography.hazmat.primitives.serialization import load_pem_public_key +from cryptography.hazmat.primitives.serialization import load_ssh_public_key +from cryptography.hazmat.backends import default_backend + +backend = default_backend() + +class CtlSecSocket(SecureSocket): + def __init__(self, soc): + super(CtlSecSocket, self).__init__(soc) + self._role = "client" + + def handshake(self, sec_params): + self._ctl_random = os.urandom(28) + + ctl_hello = {"type": "ctl_hello", + "ctl_random": self._ctl_random} + self.send_msg(ctl_hello) + slave_hello = self.recv_msg() + + if slave_hello["type"] != "slave_hello": + raise SecSocketException("Handshake failed.") + + self._slave_random = slave_hello["slave_random"] + + if sec_params["auth_type"] == "none": + self._dh_handshake() + elif sec_params["auth_type"] == "ssh": + self._ssh_handshake() + elif sec_params["auth_type"] == "pubkey": + ctl_identity = sec_params["identity"] + ctl_key_path = sec_params["privkey"] + try: + with open(ctl_key_path, 'r') as f: + ctl_key = load_pem_private_key(f.read(), None, backend) + except: + ctl_key = None + + srv_key_path = sec_params["srv_pubkey_path"] + try: + with open(srv_key_path, 'r') as f: + srv_key = load_pem_public_key(f.read(), backend) + except: + srv_key = None + + if srv_key is None or ctl_key is None: + raise SecSocketException("Handshake failed.") + + self._pubkey_handshake(ctl_identity, ctl_key, srv_key) + elif sec_params["auth_type"] == "password": + self._passwd_handshake(sec_params["auth_passwd"]) + else: + raise SecSocketException("Unknown authentication method.") + + def _validate_secret(self, handshake_data): + hashed_handshake_data = hashlib.sha256() + hashed_handshake_data.update(handshake_data) + + ctl_verify_data = self.PRF(self._master_secret, + "ctl finished", + hashed_handshake_data.digest(), + 12) + + finished_msg = {"type": "ctl finished", + "verify_data": ctl_verify_data} + self.send_msg(finished_msg) + + server_reply = self.recv_msg() + if server_reply["type"] != "server finished": + raise SecSocketException("Handshake failed.") + + srv_verify_data = self.PRF(self._master_secret, + "server finished", + hashed_handshake_data.digest(), + 12) + + if srv_verify_data != server_reply["verify_data"]: + raise SecSocketException("Handshake failed.") + return + + def _dh_handshake(self): + modp_group = DH_GROUP + #private exponent + ctl_privkey = int(os.urandom(modp_group["q_size"]+1).encode('hex'), 16) + ctl_privkey = ctl_privkey % modp_group["q"] + #public key + ctl_pubkey = pow(modp_group["g"], ctl_privkey, modp_group["p"]) + + msg = {"type": "pub_dh", + "value": ctl_pubkey} + self.send_msg(msg) + + reply = self.recv_msg() + if reply["type"] != "pub_dh": + raise SecSocketException("Handshake failed.") + + srv_pubkey = reply["value"] + + ZZ = pow(srv_pubkey, ctl_privkey, modp_group["p"]) + ZZ = "{1:0{0}x}".format(modp_group['p_size']*2, ZZ) + ZZ = self._master_secret.decode('hex') + + self._master_secret = self.PRF(ZZ, + "master secret", + self._ctl_random + self._slave_random, + 48) + + handshake_data = "" + handshake_data += ("{1:0{0}x}".format(modp_group['p_size']*2, + ctl_pubkey)).decode('hex') + handshake_data += ("{1:0{0}x}".format(modp_group['p_size']*2, + srv_pubkey)).decode('hex') + + self._init_cipher_spec() + self._send_change_cipher_spec() + self._validate_secret(handshake_data) + + def _ssh_handshake(self): + ctl_ssh_key = None + known_hosts = [] + ssh_dir_path = os.path.expanduser("~/.ssh") + with open(ssh_dir_path+"/known_hosts", 'r') as f: + for line in f.readlines(): + key = line[line.find(' ')+1:] + known_hosts.append(load_ssh_public_key(key, backend)) + + with open(ssh_dir_path+"/id_rsa", 'r') as f: + ctl_ssh_key = load_pem_private_key(f.read(), None, backend) + + if not ctl_ssh_key: + raise SecSocketException("Handshake failed.") + + ctl_ssh_pubkey = ctl_ssh_key.public_key() + ctl_ssh_pubkey_pem = ctl_ssh_pubkey.public_bytes( + encoding=ser.Encoding.PEM, + format=ser.PublicFormat.SubjectPublicKeyInfo) + msg = {"type": "ssh_client_hello", + "ctl_ssh_pubkey": ctl_ssh_pubkey_pem} + + self.send_msg(msg) + msg = self.recv_msg() + if msg["type"] != "ssh_server_hello": + raise SecSocketException("Handshake failed.") + srv_ssh_pubkeys = [] + for key in msg["srv_ssh_pubkeys"]: + srv_ssh_pubkeys.append(load_pem_public_key(key, backend)) + + srv_ssh_pubkey = None + i = 0 + for key in srv_ssh_pubkeys: + for host in known_hosts: + if self._cmp_pub_keys(key, host): + srv_ssh_pubkey = host + break + if srv_ssh_pubkey is not None: + break + i += 1 + if not srv_ssh_pubkey: + raise SecSocketException("Handshake failed.") + + msg = {"type": "ssh_client_key_select", + "index": i, + "signature": self._sign_data(str(i), ctl_ssh_key)} + self.send_msg(msg) + + self._pubkey_handshake("ssh", ctl_ssh_key, srv_ssh_pubkey) + + def _pubkey_handshake(self, ctl_identity, ctl_privkey, local_srv_pubkey): + modp_group = DH_GROUP + ctl_dh_privkey = int(os.urandom(modp_group["q_size"]+1).encode('hex'), + 16) + ctl_dh_privkey = ctl_dh_privkey % modp_group["q"] + #public key + ctl_dh_pubkey_int = pow(modp_group["g"], + ctl_dh_privkey, + modp_group["p"]) + ctl_dh_pubkey = "{1:0{0}x}".format(modp_group['p_size']*2, + ctl_dh_pubkey_int) + ctl_dh_pubkey = ctl_dh_pubkey.decode('hex') + + ctl_pubkey = ctl_privkey.public_key() + ctl_pubkey_pem = ctl_pubkey.public_bytes( + encoding=ser.Encoding.PEM, + format=ser.PublicFormat.SubjectPublicKeyInfo) + + signature = self._sign_data(ctl_dh_pubkey, ctl_privkey) + msg = {"type": "pubkey_client_hello", + "identity": ctl_identity, + "ctl_pubkey": ctl_pubkey_pem, + "ctl_pub_dh": ctl_dh_pubkey, + "signature": signature} + + self.send_msg(msg) + + msg = self.recv_msg() + if msg["type"] != "pubkey_server_hello": + raise SecSocketException("Handshake failed.") + + srv_pubkey = load_pem_public_key(msg["srv_pubkey"], backend) + if not self._cmp_pub_keys(local_srv_pubkey, srv_pubkey): + raise SecSocketException("Handshake failed.") + + srv_dh_pubkey = msg["srv_pub_dh"] + if not self._verify_signature(local_srv_pubkey, + srv_dh_pubkey, + msg["signature"]): + raise SecSocketException("Handshake failed.") + + srv_dh_pubkey_int = int(srv_dh_pubkey.encode('hex'), 16) + + ZZ = pow(srv_dh_pubkey_int, ctl_dh_privkey, modp_group["p"]) + ZZ = "{1:0{0}x}".format(modp_group['p_size']*2, ZZ) + ZZ = self._master_secret.decode('hex') + + self._master_secret = self.PRF(ZZ, + "master secret", + self._ctl_random + self._slave_random, + 48) + + self._init_cipher_spec() + self._send_change_cipher_spec() + + def _passwd_handshake(self, auth_passwd): + srp_group = SRP_GROUP + p_bytes = "{1:0{0}x}".format(srp_group['p_size']*2, srp_group['p']) + p_bytes = p_bytes.decode('hex') + g_bytes = "{0:02x}".format(srp_group['g']) + g_bytes = g_bytes.decode('hex') + k = hashlib.sha256(p_bytes + g_bytes).digest() + k = int(k.encode('hex'), 16) + + username = "lnst_user" + + msg = {"type": "srp_client_begin", + "username": username} + self.send_msg(msg) + + reply = self.recv_msg() + if reply["type"] != "srp_server_salt": + raise SecSocketException("Handshake failed.") + + salt = reply["salt"] + + x = hashlib.sha256(salt + username + auth_passwd).digest() + x_int = int(x.encode('hex'), 16) + + ctl_privkey = os.urandom(srp_group["q_size"]+1) + ctl_privkey_int = int(ctl_privkey.encode('hex'), 16) % srp_group["q"] + + ctl_pubkey_int = pow(srp_group["g"], ctl_privkey_int, srp_group["p"]) + ctl_pubkey = "{1:0{0}x}".format(srp_group['p_size']*2, ctl_pubkey_int) + ctl_pubkey = ctl_pubkey.decode('hex') + + msg = {"type": "srp_client_pub", + "ctl_pubkey": ctl_pubkey} + self.send_msg(msg) + + reply = self.recv_msg() + if reply["type"] != "srp_server_pub": + raise SecSocketException("Handshake failed.") + + srv_pubkey = reply["srv_pubkey"] + srv_pubkey_int = int(srv_pubkey.encode('hex'), 16) + + if (srv_pubkey_int % srp_group["p"]) == 0: + raise SecSocketException("Handshake failed.") + + u = hashlib.sha256(ctl_pubkey + srv_pubkey).digest() + u_int = int(u.encode('hex'), 16) + + S_int = srv_pubkey_int - k * pow(srp_group['g'], x_int, srp_group['p']) + S_int = pow(S_int, ctl_privkey_int + u_int * x_int, srp_group['p']) + S = "{1:0{0}x}".format(srp_group['p_size']*2, S_int) + S = S.decode('hex') + + m1 = hashlib.sha256(ctl_pubkey + srv_pubkey + S).digest() + msg = {"type": "srp_client_m1", + "m1": m1} + self.send_msg(msg) + + reply = self.recv_msg() + if reply["type"] != "srp_server_m2": + raise SecSocketException("Handshake failed.") + srv_m2 = reply["m2"] + + client_m2 = hashlib.sha256(ctl_pubkey + m1 + S).digest() + if srv_m2 != client_m2: + raise SecSocketException("Handshake failed.") + + K = hashlib.sha256(S).digest() + self._master_secret = self.PRF(K, + "master secret", + self._ctl_random + self._slave_random, + 48) + + self._init_cipher_spec() + self._send_change_cipher_spec() diff --git a/lnst/Slave/SlaveSecSocket.py b/lnst/Slave/SlaveSecSocket.py new file mode 100644 index 0000000..263b9de --- /dev/null +++ b/lnst/Slave/SlaveSecSocket.py @@ -0,0 +1,323 @@ +""" +The SlaveSecSocket implements the slave (server) side of the handshake +protocols. + +Copyright 2016 Red Hat, Inc. +Licensed under the GNU General Public License, version 2 as +published by the Free Software Foundation; see COPYING for details. +""" + +__author__ = """ +olichtne@redhat.com (Ondrej Lichtner) +""" + +import os +import hashlib +import math +import re +from lnst.Common.SecureSocket import SecureSocket +from lnst.Common.SecureSocket import DH_GROUP, SRP_GROUP +from lnst.Common.SecureSocket import SecSocketException +from cryptography.hazmat.primitives import serialization as ser +from cryptography.hazmat.primitives.serialization import load_pem_private_key +from cryptography.hazmat.primitives.serialization import load_pem_public_key +from cryptography.hazmat.primitives.serialization import load_ssh_public_key +from cryptography.hazmat.backends import default_backend + +backend = default_backend() + +class SlaveSecSocket(SecureSocket): + def __init__(self, soc): + super(SlaveSecSocket, self).__init__(soc) + self._role = "server" + + def handshake(self, sec_params): + ctl_hello = self.recv_msg() + if ctl_hello["type"] != "ctl_hello": + raise SecSocketException("Handshake failed.") + + self._ctl_random = ctl_hello["ctl_random"] + self._slave_random = os.urandom(28) + + slave_hello = {"type": "slave_hello", + "slave_random": self._slave_random} + self.send_msg(slave_hello) + + if sec_params["auth_types"] == "none": + self._dh_handshake() + elif sec_params["auth_types"] == "ssh": + self._ssh_handshake() + elif sec_params["auth_types"] == "pubkey": + srv_key = None + with open(sec_params["privkey"], 'r') as f: + srv_key = load_pem_private_key(f.read(), None, backend) + + ctl_pubkeys = {} + for fname in os.listdir(sec_params["ctl_pubkeys"]): + path = os.path.join(sec_params["ctl_pubkeys"], fname) + if not os.path.isfile(path): + continue + with open(path, 'r') as f: + ctl_pubkeys[fname] = load_pem_public_key(f.read(), backend) + + self._pubkey_handshake(srv_key, ctl_pubkeys) + elif sec_params["auth_types"] == "password": + self._passwd_handshake(sec_params["auth_password"]) + else: + raise SecSocketException("Unknown authentication method.") + + def _validate_secret(self, handshake_data): + hashed_handshake_data = hashlib.sha256() + hashed_handshake_data.update(handshake_data) + + srv_verify_data = self.PRF(self._master_secret, + "server finished", + hashed_handshake_data.digest(), + 12) + + finished_msg = {"type": "server finished", + "verify_data": srv_verify_data} + self.send_msg(finished_msg) + + ctl_reply = self.recv_msg() + if ctl_reply["type"] != "ctl finished": + raise SecSocketException("Handshake failed.") + + ctl_verify_data = self.PRF(self._master_secret, + "ctl finished", + hashed_handshake_data.digest(), + 12) + + if ctl_verify_data != ctl_reply["verify_data"]: + raise SecSocketException("Handshake failed.") + return + + def _dh_handshake(self): + modp_group = DH_GROUP + #private exponent + srv_privkey = int(os.urandom(modp_group["q_size"]+1).encode('hex'), 16) + srv_privkey = srv_privkey % modp_group["q"] + #public key + srv_pubkey = pow(modp_group["g"], srv_privkey, modp_group["p"]) + + msg = {"type": "pub_dh", + "value": srv_pubkey} + self.send_msg(msg) + + reply = self.recv_msg() + if reply["type"] != "pub_dh": + raise SecSocketException("Handshake failed.") + + ctl_pubkey = reply["value"] + + ZZ = pow(ctl_pubkey, srv_privkey, modp_group["p"]) + ZZ = "{1:0{0}x}".format(modp_group['p_size']*2, ZZ) + ZZ = self._master_secret.decode('hex') + + self._master_secret = self.PRF(ZZ, + "master secret", + self._ctl_random + self._slave_random, + 48) + + handshake_data = "" + handshake_data += ("{1:0{0}x}".format(modp_group['p_size']*2, + ctl_pubkey)).decode('hex') + handshake_data += ("{1:0{0}x}".format(modp_group['p_size']*2, + srv_pubkey)).decode('hex') + + self._init_cipher_spec() + self._send_change_cipher_spec() + self._validate_secret(handshake_data) + + def _ssh_handshake(self): + srv_keys = [] + srv_pubkeys = [] + authorized_keys = [] + sshd_key_paths = ["/etc/ssh/ssh_host_rsa_key", + "/etc/ssh/ssh_host_ecdsa_key"] + ssh_dir_path = os.path.expanduser("~/.ssh") + for f_name in sshd_key_paths: + with open(f_name, 'r') as f: + srv_keys.append(load_pem_private_key(f.read(), None, backend)) + srv_pubkeys.append(srv_keys[-1].public_key()) + + with open(ssh_dir_path+"/authorized_keys", 'r') as f: + for line in f.readlines(): + authorized_keys.append(load_ssh_public_key(line, backend)) + + msg = self.recv_msg() + if msg["type"] != "ssh_client_hello": + raise SecSocketException("Handshake failed.") + ctl_ssh_pubkey = load_pem_public_key(msg["ctl_ssh_pubkey"], backend) + + authorized = False + for key in authorized_keys: + if self._cmp_pub_keys(key, ctl_ssh_pubkey): + authorized = True + break + if not authorized: + raise SecSocketException("Handshake failed.") + + pem_pubkeys = [] + for key in srv_pubkeys: + pem_key = key.public_bytes( + encoding=ser.Encoding.PEM, + format=ser.PublicFormat.SubjectPublicKeyInfo) + pem_pubkeys.append(pem_key) + + msg = {"type": "ssh_server_hello", + "srv_ssh_pubkeys": pem_pubkeys} + self.send_msg(msg) + + msg = self.recv_msg() + if msg["type"] != "ssh_client_key_select": + raise SecSocketException("Handshake failed.") + + if not self._verify_signature(ctl_ssh_pubkey, + str(msg["index"]), + msg["signature"]): + raise SecSocketException("Handshake failed.") + + self._pubkey_handshake(srv_keys[msg["index"]], {"ssh": ctl_ssh_pubkey}) + + def _pubkey_handshake(self, srv_privkey, client_pubkeys): + modp_group = DH_GROUP + #private exponent + srv_dh_privkey = int(os.urandom(modp_group["q_size"]+1).encode('hex'), + 16) + srv_dh_privkey = srv_dh_privkey % modp_group["q"] + #public key + srv_dh_pubkey_int = pow(modp_group["g"], + srv_dh_privkey, + modp_group["p"]) + srv_dh_pubkey = "{1:0{0}x}".format(modp_group['p_size']*2, + srv_dh_pubkey_int) + srv_dh_pubkey = srv_dh_pubkey.decode('hex') + + msg = self.recv_msg() + if msg["type"] != "pubkey_client_hello": + raise SecSocketException("Handshake failed.") + ctl_identity = msg["identity"] + ctl_pubkey = load_pem_public_key(msg["ctl_pubkey"], backend) + + local_ctl_pubkey = client_pubkeys[ctl_identity] + + if not self._cmp_pub_keys(local_ctl_pubkey, ctl_pubkey): + raise SecSocketException("Handshake failed.") + + ctl_dh_pubkey = msg["ctl_pub_dh"] + signature = msg["signature"] + if not self._verify_signature(local_ctl_pubkey, + ctl_dh_pubkey, + signature): + raise SecSocketException("Handshake failed.") + + ctl_dh_pubkey_int = int(ctl_dh_pubkey.encode('hex'), 16) + + srv_pubkey = srv_privkey.public_key() + srv_pubkey_pem = srv_pubkey.public_bytes( + encoding=ser.Encoding.PEM, + format=ser.PublicFormat.SubjectPublicKeyInfo) + + signature = self._sign_data(srv_dh_pubkey, srv_privkey) + msg = {"type": "pubkey_server_hello", + "srv_pubkey": srv_pubkey_pem, + "srv_pub_dh": srv_dh_pubkey, + "signature": signature} + self.send_msg(msg) + + ZZ = pow(ctl_dh_pubkey_int, srv_dh_privkey, modp_group["p"]) + ZZ = "{1:0{0}x}".format(modp_group['p_size']*2, ZZ) + ZZ = self._master_secret.decode('hex') + + self._master_secret = self.PRF(ZZ, + "master secret", + self._ctl_random + self._slave_random, + 48) + + self._init_cipher_spec() + self._send_change_cipher_spec() + + def _passwd_handshake(self, auth_passwd): + msg = self.recv_msg() + if msg["type"] != "srp_client_begin": + raise SecSocketException("Handshake failed.") + + if msg["username"] != "lnst_user": + raise SecSocketException("Handshake failed.") + + srp_group = SRP_GROUP + p_bytes = "{1:0{0}x}".format(srp_group['p_size']*2, srp_group['p']) + p_bytes = p_bytes.decode('hex') + g_bytes = "{0:02x}".format(srp_group['g']) + g_bytes = g_bytes.decode('hex') + k = hashlib.sha256(p_bytes + g_bytes).digest() + k = int(k.encode('hex'), 16) + username = msg["username"] + + salt = os.urandom(16) + + x = hashlib.sha256(salt + username + auth_passwd).digest() + + x_int = int(x.encode('hex'), 16) + + v = pow(srp_group["g"], x_int, srp_group["p"]) + + msg = {"type": "srp_server_salt", + "salt": salt} + + self.send_msg(msg) + + reply = self.recv_msg() + if reply["type"] != "srp_client_pub": + raise SecSocketException("Handshake failed.") + + ctl_pubkey = reply["ctl_pubkey"] + ctl_pubkey_int = int(ctl_pubkey.encode('hex'), 16) + + if (ctl_pubkey_int % srp_group["p"]) == 0: + raise SecSocketException("Handshake failed.") + + srv_privkey = os.urandom(srp_group["q_size"]+1) + srv_privkey_int = int(srv_privkey.encode('hex'), 16) % srp_group["q"] + + srv_pubkey_int = pow(srp_group["g"], srv_privkey_int, srp_group["p"]) + srv_pubkey_int = (srv_pubkey_int + k*v) % srp_group["p"] + srv_pubkey = "{1:0{0}x}".format(srp_group['p_size']*2, srv_pubkey_int) + srv_pubkey = srv_pubkey.decode('hex') + + msg = {"type": "srp_server_pub", + "srv_pubkey": srv_pubkey} + self.send_msg(msg) + + u = hashlib.sha256(ctl_pubkey + srv_pubkey).digest() + u_int = int(u.encode('hex'), 16) + + S_int = pow(v, u_int, srp_group['p'])*ctl_pubkey_int + S_int = pow(S_int, srv_privkey_int, srp_group["p"]) + S = "{1:0{0}x}".format(srp_group['p_size']*2, S_int) + S = S.decode('hex') + + msg = self.recv_msg() + if msg["type"] != "srp_client_m1": + raise SecSocketException("Handshake failed.") + + client_m1 = msg["m1"] + + srv_m1 = hashlib.sha256(ctl_pubkey + srv_pubkey + S).digest() + if client_m1 != srv_m1: + raise SecSocketException("Handshake failed.") + + srv_m2 = hashlib.sha256(ctl_pubkey + srv_m1 + S).digest() + msg = {"type": "srp_server_m2", + "m2": srv_m2} + self.send_msg(msg) + + K = hashlib.sha256(S).digest() + self._master_secret = self.PRF(K, + "master secret", + self._ctl_random + self._slave_random, + 48) + + self._init_cipher_spec() + self._send_change_cipher_spec()
Fri, Feb 26, 2016 at 04:00:00PM CET, olichtne@redhat.com wrote:
From: Ondrej Lichtner olichtne@redhat.com
Added 3 new classes: SecureSocket, CtlSecSocket and SlaveSecSocket. The SecureSocket class encapsulates a normal socket object and adds what is basically a TLS Record Layer - adds a signature, padding and encrypts the entire message.
The CtlSecSocket and SlaveSecSocket classes are child classes inheriting from the SecureSocket class and implement the handshake protocols used from the Controller or from the Slave side. The important method is handshake() that takes a sec_params parameter which is a dictionary describing how to do the handshake.
Signed-off-by: Ondrej Lichtner olichtne@redhat.com
lnst/Common/SecureSocket.py | 377 ++++++++++++++++++++++++++++++++++++++++ lnst/Controller/CtlSecSocket.py | 318 +++++++++++++++++++++++++++++++++ lnst/Slave/SlaveSecSocket.py | 323 ++++++++++++++++++++++++++++++++++ 3 files changed, 1018 insertions(+) create mode 100644 lnst/Common/SecureSocket.py create mode 100644 lnst/Controller/CtlSecSocket.py create mode 100644 lnst/Slave/SlaveSecSocket.py
diff --git a/lnst/Common/SecureSocket.py b/lnst/Common/SecureSocket.py new file mode 100644 index 0000000..d319c54 --- /dev/null +++ b/lnst/Common/SecureSocket.py @@ -0,0 +1,377 @@ +""" +This module defines a SecureSocket class that wraps the normal socket by adding +TLS-like functionality of providing data integrity, confidentiality and +authenticity. The reason why we're not using TLS is because the Python +implementation enforces the use of certificates and we want to also allow +password based authentication. This implements the common class, and the Slave +and Controller implement their sides of the handshake algorithms.
+Copyright 2016 Red Hat, Inc. +Licensed under the GNU General Public License, version 2 as +published by the Free Software Foundation; see COPYING for details. +"""
+__author__ = """ +olichtne@redhat.com (Ondrej Lichtner) +"""
+import os +import cPickle +import hashlib +import hmac +import cryptography.exceptions +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives.asymmetric import padding, ec +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey +from cryptography.hazmat.primitives.asymmetric.dsa import DSAPrivateKey +from cryptography.hazmat.primitives.asymmetric.dsa import DSAPublicKey +from cryptography.hazmat.backends import default_backend
+DH_GROUP = {"p": int("0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1"\
"29024E088A67CC74020BBEA63B139B22514A08798E3404DD"\
"EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245"\
"E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED"\
"EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D"\
"C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F"\
"83655D23DCA3AD961C62F356208552BB9ED529077096966D"\
"670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B"\
"E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9"\
"DE2BCBF6955817183995497CEA956AE515D2261898FA0510"\
"15728E5A8AACAA68FFFFFFFFFFFFFFFF", 16),
Wouldn't it make sense to make this configurable and not hard-coded?
"g": 2}
+DH_GROUP["q"] = (DH_GROUP["p"]-1)/2 +DH_GROUP["q_size"] = DH_GROUP["q"].bit_length()/8 +if DH_GROUP["q"].bit_length()%8:
- DH_GROUP["q_size"] += 1
+DH_GROUP["p_size"] = DH_GROUP["p"].bit_length()/8 +if DH_GROUP["p"].bit_length()%8:
- DH_GROUP["p_size"] += 1
+SRP_GROUP = {"p": int("0xAC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC"
"3192943DB56050A37329CBB4A099ED8193E0757767A13DD52312"
"AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF6095179A"
"163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975E"
"EAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773BCA"
"97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32"
"E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF5"
"2FB3786160279004E57AE6AF874E7303CE53299CCC041C7BC308"
"D82A5698F3A8D0C38271AE35F8E9DBFBB694B5C803D89F7AE435"
"DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73", 16),
"g": 2}
same here
+SRP_GROUP["q"] = (SRP_GROUP["p"]-1)/2 +SRP_GROUP["q_size"] = SRP_GROUP["q"].bit_length()/8 +if SRP_GROUP["q"].bit_length()%8:
- SRP_GROUP["q_size"] += 1
+SRP_GROUP["p_size"] = SRP_GROUP["p"].bit_length()/8 +if SRP_GROUP["p"].bit_length()%8:
- SRP_GROUP["p_size"] += 1
+class SecSocketException(Exception):
- pass
+class SecureSocket(object):
- def __init__(self, soc):
self._role = None
self._socket = soc
self._master_secret = ""
self._ctl_random = None
self._slave_random = None
self._current_write_spec = {"enc_key": None,
"mac_key": None,
"seq_num": 0}
self._current_read_spec = {"enc_key": None,
"mac_key": None,
"seq_num": 0}
self._next_write_spec = {"enc_key": None,
"mac_key": None,
"seq_num": 0}
self._next_read_spec = {"enc_key": None,
"mac_key": None,
"seq_num": 0}
- def send_msg(self, msg):
pickled_msg = cPickle.dumps(msg)
return self.send(pickled_msg)
- def recv_msg(self):
pickled_msg = self.recv()
if pickled_msg == "":
raise SecSocketException("Disconnected")
msg = cPickle.loads(pickled_msg)
return msg
- def _add_mac_sign(self, data):
if not self._current_write_spec["mac_key"]:
return data
msg = str(self._current_write_spec["seq_num"]) + str(len(data)) + data
signature = hmac.new(self._current_write_spec["mac_key"],
msg,
hashlib.sha256)
signed_msg = {"data": data,
"signature": signature.digest()}
return cPickle.dumps(signed_msg)
- def _del_mac_sign(self, signed_data):
if not self._current_read_spec["mac_key"]:
return signed_data
signed_msg = cPickle.loads(signed_data)
data = signed_msg["data"]
msg = str(self._current_read_spec["seq_num"]) + str(len(data)) + data
signature = hmac.new(self._current_read_spec["mac_key"],
msg,
hashlib.sha256)
if signature.digest() != signed_msg["signature"]:
return None
return data
- def _add_padding(self, data):
if not self._current_write_spec["enc_key"]:
return data
block_size = algorithms.AES.block_size/8
pad_length = block_size - (len(data) % block_size)
pad_char = ("%02x" % pad_length).decode("hex")
padding = pad_length * pad_char
padded_data = data+padding
return padded_data
- def _del_padding(self, data):
if not self._current_read_spec["enc_key"]:
return data
pad_length = int(data[-1].encode("hex"), 16)
for char in data[-pad_length]:
if int(char.encode("hex"), 16) != pad_length:
return None
return data[:-pad_length]
- def _add_encrypt(self, data):
if not self._current_write_spec["enc_key"]:
return data
iv = os.urandom(algorithms.AES.block_size/8)
mode = modes.CBC(iv)
key = self._current_write_spec["enc_key"]
cipher = Cipher(algorithms.AES(key), mode, default_backend())
encryptor = cipher.encryptor()
encrypted_data = encryptor.update(data) + encryptor.finalize()
encrypted_msg = {"iv": iv,
"enc_data": encrypted_data}
return cPickle.dumps(encrypted_msg)
- def _del_encrypt(self, data):
if not self._current_read_spec["enc_key"]:
return data
encrypted_msg = cPickle.loads(data)
encrypted_data = encrypted_msg["enc_data"]
iv = encrypted_msg["iv"]
mode = modes.CBC(iv)
key = self._current_read_spec["enc_key"]
cipher = Cipher(algorithms.AES(key), mode, default_backend())
decryptor = cipher.decryptor()
decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize()
return decrypted_data
- def _protect_data(self, data):
signed = self._add_mac_sign(data)
padded = self._add_padding(signed)
encrypted = self._add_encrypt(padded)
self._current_write_spec["seq_num"] += 1
return encrypted
- def _uprotect_data(self, encrypted):
padded = self._del_encrypt(encrypted)
signed = self._del_padding(padded)
if signed is None:
#preventing timing attacks
self._del_mac_sign(padded)
return None
data = self._del_mac_sign(signed)
self._current_read_spec["seq_num"] += 1
return data
- def send(self, data):
protected_data = self._protect_data(data)
transmit_data = str(len(protected_data)) + " " + protected_data
return self._socket.sendall(transmit_data)
- def recv(self):
length = ""
while True:
c = self._socket.recv(1)
if c == ' ':
length = int(length)
break
elif c == "":
return ""
else:
length += c
data = ""
while len(data) < length:
c = self._socket.recv(length - len(data))
if c == "":
return ""
else:
data += c
msg = self._uprotect_data(data)
if msg is None:
return self.recv()
return self._handle_internal(msg)
- def _handle_internal(self, orig_msg):
try:
msg = cPickle.loads(orig_msg)
except:
return orig_msg
if "type" in msg and msg["type"] == "change_cipher_spec":
self._change_read_cipher_spec()
return self.recv()
else:
return orig_msg
- def _send_change_cipher_spec(self):
change_cipher_spec_msg = {"type": "change_cipher_spec"}
self.send_msg(change_cipher_spec_msg)
self._change_write_cipher_spec()
return
- def fileno(self):
"""needed to work with select()"""
return self._socket.fileno()
- def close(self):
return self._socket.close()
- def shutdown(self, how):
return self._socket.shutdown(how)
- def _change_read_cipher_spec(self):
self._current_read_spec = self._next_read_spec
self._next_read_spec = {"enc_key": None,
"mac_key": None,
"seq_num": 0}
return
- def _change_write_cipher_spec(self):
self._current_write_spec = self._next_write_spec
self._next_write_spec = {"enc_key": None,
"mac_key": None,
"seq_num": 0}
return
- def p_SHA256(self, secret, seed, length):
prev_a = seed
result = ""
while len(result) < length:
a = hmac.new(secret, msg=prev_a, digestmod=hashlib.sha256)
prev_a = a.digest()
hmac_hash = hmac.new(secret,
msg=a.digest()+seed,
digestmod=hashlib.sha256)
result += hmac_hash.digest()
return result[:length]
- def PRF(self, secret, label, seed, length):
return self.p_SHA256(secret, label+seed, length)
- def _init_cipher_spec(self):
if self._role == "server":
client_spec = self._next_read_spec
server_spec = self._next_write_spec
elif self._role == "client":
client_spec = self._next_write_spec
server_spec = self._next_read_spec
else:
raise SecSocketException("Socket without a role!")
aes_keysize = max(algorithms.AES.key_sizes)/8
mac_keysize = hashlib.sha256().block_size
prf_seq = self.PRF(self._master_secret,
"key expansion",
self._slave_random + self._ctl_random,
2*aes_keysize + 2*mac_keysize)
client_spec["enc_key"] = prf_seq[:aes_keysize]
prf_seq = prf_seq[aes_keysize:]
server_spec["enc_key"] = prf_seq[:aes_keysize]
prf_seq = prf_seq[aes_keysize:]
client_spec["mac_key"] = prf_seq[:mac_keysize]
prf_seq = prf_seq[mac_keysize:]
server_spec["mac_key"] = prf_seq[:mac_keysize]
prf_seq = prf_seq[mac_keysize:]
return
- def _sign_data(self, data, privkey):
if isinstance(privkey, DSAPrivateKey):
signer = privkey.signer(hashes.SHA256())
elif isinstance(privkey, RSAPrivateKey):
signer = privkey.signer(padding.PSS(padding.MGF1(hashes.SHA256()),
padding.PSS.MAX_LENGTH),
hashes.SHA256())
elif isinstance(privkey, EllipticCurvePrivateKey):
signer = privkey.signer(ec.ECDSA(hashes.SHA256()))
else:
raise SecSocketException("Unsupported Assymetric Key!")
signer.update(data)
return signer.finalize()
- def _verify_signature(self, pubkey, data, signature):
if isinstance(pubkey, DSAPublicKey):
verifier = pubkey.verifier(signature, hashes.SHA256())
elif isinstance(pubkey, RSAPublicKey):
verifier = pubkey.verifier(signature,
padding.PSS(padding.MGF1(hashes.SHA256()),
padding.PSS.MAX_LENGTH),
hashes.SHA256())
elif isinstance(pubkey, EllipticCurvePublicKey):
verifier = pubkey.verifier(signature, ec.ECDSA(hashes.SHA256()))
else:
raise SecSocketException("Unsupported Assymetric Key!")
verifier.update(data)
try:
verifier.verify()
except cryptography.exceptions.InvalidSignature:
return False
except:
return False
return True
- def _cmp_pub_keys(self, first, second):
if first.public_numbers() != second.public_numbers():
return False
else:
return True
diff --git a/lnst/Controller/CtlSecSocket.py b/lnst/Controller/CtlSecSocket.py new file mode 100644 index 0000000..db27289 --- /dev/null +++ b/lnst/Controller/CtlSecSocket.py @@ -0,0 +1,318 @@ +""" +The CtlSecSocket implements the controller (client) side of the handshake +protocols.
+Copyright 2016 Red Hat, Inc. +Licensed under the GNU General Public License, version 2 as +published by the Free Software Foundation; see COPYING for details. +"""
+__author__ = """ +olichtne@redhat.com (Ondrej Lichtner) +"""
+import os +import hashlib +import math +from lnst.Common.SecureSocket import SecureSocket +from lnst.Common.SecureSocket import DH_GROUP, SRP_GROUP +from lnst.Common.SecureSocket import SecSocketException +from lnst.Common.Config import lnst_config +from cryptography.hazmat.primitives import serialization as ser +from cryptography.hazmat.primitives.serialization import load_pem_private_key +from cryptography.hazmat.primitives.serialization import load_pem_public_key +from cryptography.hazmat.primitives.serialization import load_ssh_public_key +from cryptography.hazmat.backends import default_backend
+backend = default_backend()
+class CtlSecSocket(SecureSocket):
- def __init__(self, soc):
super(CtlSecSocket, self).__init__(soc)
self._role = "client"
- def handshake(self, sec_params):
self._ctl_random = os.urandom(28)
ctl_hello = {"type": "ctl_hello",
"ctl_random": self._ctl_random}
self.send_msg(ctl_hello)
slave_hello = self.recv_msg()
if slave_hello["type"] != "slave_hello":
raise SecSocketException("Handshake failed.")
self._slave_random = slave_hello["slave_random"]
if sec_params["auth_type"] == "none":
self._dh_handshake()
elif sec_params["auth_type"] == "ssh":
self._ssh_handshake()
elif sec_params["auth_type"] == "pubkey":
ctl_identity = sec_params["identity"]
ctl_key_path = sec_params["privkey"]
try:
with open(ctl_key_path, 'r') as f:
ctl_key = load_pem_private_key(f.read(), None, backend)
except:
ctl_key = None
srv_key_path = sec_params["srv_pubkey_path"]
try:
with open(srv_key_path, 'r') as f:
srv_key = load_pem_public_key(f.read(), backend)
except:
srv_key = None
if srv_key is None or ctl_key is None:
raise SecSocketException("Handshake failed.")
self._pubkey_handshake(ctl_identity, ctl_key, srv_key)
elif sec_params["auth_type"] == "password":
self._passwd_handshake(sec_params["auth_passwd"])
else:
raise SecSocketException("Unknown authentication method.")
- def _validate_secret(self, handshake_data):
hashed_handshake_data = hashlib.sha256()
hashed_handshake_data.update(handshake_data)
ctl_verify_data = self.PRF(self._master_secret,
"ctl finished",
hashed_handshake_data.digest(),
12)
finished_msg = {"type": "ctl finished",
"verify_data": ctl_verify_data}
self.send_msg(finished_msg)
server_reply = self.recv_msg()
if server_reply["type"] != "server finished":
raise SecSocketException("Handshake failed.")
srv_verify_data = self.PRF(self._master_secret,
"server finished",
hashed_handshake_data.digest(),
12)
if srv_verify_data != server_reply["verify_data"]:
raise SecSocketException("Handshake failed.")
return
- def _dh_handshake(self):
modp_group = DH_GROUP
#private exponent
ctl_privkey = int(os.urandom(modp_group["q_size"]+1).encode('hex'), 16)
ctl_privkey = ctl_privkey % modp_group["q"]
#public key
ctl_pubkey = pow(modp_group["g"], ctl_privkey, modp_group["p"])
msg = {"type": "pub_dh",
"value": ctl_pubkey}
self.send_msg(msg)
reply = self.recv_msg()
if reply["type"] != "pub_dh":
raise SecSocketException("Handshake failed.")
srv_pubkey = reply["value"]
ZZ = pow(srv_pubkey, ctl_privkey, modp_group["p"])
ZZ = "{1:0{0}x}".format(modp_group['p_size']*2, ZZ)
ZZ = self._master_secret.decode('hex')
self._master_secret = self.PRF(ZZ,
"master secret",
self._ctl_random + self._slave_random,
48)
handshake_data = ""
handshake_data += ("{1:0{0}x}".format(modp_group['p_size']*2,
ctl_pubkey)).decode('hex')
handshake_data += ("{1:0{0}x}".format(modp_group['p_size']*2,
srv_pubkey)).decode('hex')
self._init_cipher_spec()
self._send_change_cipher_spec()
self._validate_secret(handshake_data)
- def _ssh_handshake(self):
ctl_ssh_key = None
known_hosts = []
ssh_dir_path = os.path.expanduser("~/.ssh")
with open(ssh_dir_path+"/known_hosts", 'r') as f:
for line in f.readlines():
key = line[line.find(' ')+1:]
known_hosts.append(load_ssh_public_key(key, backend))
with open(ssh_dir_path+"/id_rsa", 'r') as f:
ctl_ssh_key = load_pem_private_key(f.read(), None, backend)
if not ctl_ssh_key:
raise SecSocketException("Handshake failed.")
ctl_ssh_pubkey = ctl_ssh_key.public_key()
ctl_ssh_pubkey_pem = ctl_ssh_pubkey.public_bytes(
encoding=ser.Encoding.PEM,
format=ser.PublicFormat.SubjectPublicKeyInfo)
msg = {"type": "ssh_client_hello",
"ctl_ssh_pubkey": ctl_ssh_pubkey_pem}
self.send_msg(msg)
msg = self.recv_msg()
if msg["type"] != "ssh_server_hello":
raise SecSocketException("Handshake failed.")
srv_ssh_pubkeys = []
for key in msg["srv_ssh_pubkeys"]:
srv_ssh_pubkeys.append(load_pem_public_key(key, backend))
srv_ssh_pubkey = None
i = 0
for key in srv_ssh_pubkeys:
for host in known_hosts:
if self._cmp_pub_keys(key, host):
srv_ssh_pubkey = host
break
if srv_ssh_pubkey is not None:
break
i += 1
if not srv_ssh_pubkey:
raise SecSocketException("Handshake failed.")
msg = {"type": "ssh_client_key_select",
"index": i,
"signature": self._sign_data(str(i), ctl_ssh_key)}
self.send_msg(msg)
self._pubkey_handshake("ssh", ctl_ssh_key, srv_ssh_pubkey)
- def _pubkey_handshake(self, ctl_identity, ctl_privkey, local_srv_pubkey):
modp_group = DH_GROUP
ctl_dh_privkey = int(os.urandom(modp_group["q_size"]+1).encode('hex'),
16)
ctl_dh_privkey = ctl_dh_privkey % modp_group["q"]
#public key
ctl_dh_pubkey_int = pow(modp_group["g"],
ctl_dh_privkey,
modp_group["p"])
ctl_dh_pubkey = "{1:0{0}x}".format(modp_group['p_size']*2,
ctl_dh_pubkey_int)
ctl_dh_pubkey = ctl_dh_pubkey.decode('hex')
ctl_pubkey = ctl_privkey.public_key()
ctl_pubkey_pem = ctl_pubkey.public_bytes(
encoding=ser.Encoding.PEM,
format=ser.PublicFormat.SubjectPublicKeyInfo)
signature = self._sign_data(ctl_dh_pubkey, ctl_privkey)
msg = {"type": "pubkey_client_hello",
"identity": ctl_identity,
"ctl_pubkey": ctl_pubkey_pem,
"ctl_pub_dh": ctl_dh_pubkey,
"signature": signature}
self.send_msg(msg)
msg = self.recv_msg()
if msg["type"] != "pubkey_server_hello":
raise SecSocketException("Handshake failed.")
srv_pubkey = load_pem_public_key(msg["srv_pubkey"], backend)
if not self._cmp_pub_keys(local_srv_pubkey, srv_pubkey):
raise SecSocketException("Handshake failed.")
srv_dh_pubkey = msg["srv_pub_dh"]
if not self._verify_signature(local_srv_pubkey,
srv_dh_pubkey,
msg["signature"]):
raise SecSocketException("Handshake failed.")
srv_dh_pubkey_int = int(srv_dh_pubkey.encode('hex'), 16)
ZZ = pow(srv_dh_pubkey_int, ctl_dh_privkey, modp_group["p"])
ZZ = "{1:0{0}x}".format(modp_group['p_size']*2, ZZ)
ZZ = self._master_secret.decode('hex')
self._master_secret = self.PRF(ZZ,
"master secret",
self._ctl_random + self._slave_random,
48)
self._init_cipher_spec()
self._send_change_cipher_spec()
- def _passwd_handshake(self, auth_passwd):
srp_group = SRP_GROUP
p_bytes = "{1:0{0}x}".format(srp_group['p_size']*2, srp_group['p'])
p_bytes = p_bytes.decode('hex')
g_bytes = "{0:02x}".format(srp_group['g'])
g_bytes = g_bytes.decode('hex')
k = hashlib.sha256(p_bytes + g_bytes).digest()
k = int(k.encode('hex'), 16)
username = "lnst_user"
msg = {"type": "srp_client_begin",
"username": username}
self.send_msg(msg)
reply = self.recv_msg()
if reply["type"] != "srp_server_salt":
raise SecSocketException("Handshake failed.")
salt = reply["salt"]
x = hashlib.sha256(salt + username + auth_passwd).digest()
x_int = int(x.encode('hex'), 16)
ctl_privkey = os.urandom(srp_group["q_size"]+1)
ctl_privkey_int = int(ctl_privkey.encode('hex'), 16) % srp_group["q"]
ctl_pubkey_int = pow(srp_group["g"], ctl_privkey_int, srp_group["p"])
ctl_pubkey = "{1:0{0}x}".format(srp_group['p_size']*2, ctl_pubkey_int)
ctl_pubkey = ctl_pubkey.decode('hex')
msg = {"type": "srp_client_pub",
"ctl_pubkey": ctl_pubkey}
self.send_msg(msg)
reply = self.recv_msg()
if reply["type"] != "srp_server_pub":
raise SecSocketException("Handshake failed.")
srv_pubkey = reply["srv_pubkey"]
srv_pubkey_int = int(srv_pubkey.encode('hex'), 16)
if (srv_pubkey_int % srp_group["p"]) == 0:
raise SecSocketException("Handshake failed.")
u = hashlib.sha256(ctl_pubkey + srv_pubkey).digest()
u_int = int(u.encode('hex'), 16)
S_int = srv_pubkey_int - k * pow(srp_group['g'], x_int, srp_group['p'])
S_int = pow(S_int, ctl_privkey_int + u_int * x_int, srp_group['p'])
S = "{1:0{0}x}".format(srp_group['p_size']*2, S_int)
S = S.decode('hex')
m1 = hashlib.sha256(ctl_pubkey + srv_pubkey + S).digest()
msg = {"type": "srp_client_m1",
"m1": m1}
self.send_msg(msg)
reply = self.recv_msg()
if reply["type"] != "srp_server_m2":
raise SecSocketException("Handshake failed.")
srv_m2 = reply["m2"]
client_m2 = hashlib.sha256(ctl_pubkey + m1 + S).digest()
if srv_m2 != client_m2:
raise SecSocketException("Handshake failed.")
K = hashlib.sha256(S).digest()
self._master_secret = self.PRF(K,
"master secret",
self._ctl_random + self._slave_random,
48)
self._init_cipher_spec()
self._send_change_cipher_spec()
diff --git a/lnst/Slave/SlaveSecSocket.py b/lnst/Slave/SlaveSecSocket.py new file mode 100644 index 0000000..263b9de --- /dev/null +++ b/lnst/Slave/SlaveSecSocket.py @@ -0,0 +1,323 @@ +""" +The SlaveSecSocket implements the slave (server) side of the handshake +protocols.
+Copyright 2016 Red Hat, Inc. +Licensed under the GNU General Public License, version 2 as +published by the Free Software Foundation; see COPYING for details. +"""
+__author__ = """ +olichtne@redhat.com (Ondrej Lichtner) +"""
+import os +import hashlib +import math +import re +from lnst.Common.SecureSocket import SecureSocket +from lnst.Common.SecureSocket import DH_GROUP, SRP_GROUP +from lnst.Common.SecureSocket import SecSocketException +from cryptography.hazmat.primitives import serialization as ser +from cryptography.hazmat.primitives.serialization import load_pem_private_key +from cryptography.hazmat.primitives.serialization import load_pem_public_key +from cryptography.hazmat.primitives.serialization import load_ssh_public_key +from cryptography.hazmat.backends import default_backend
+backend = default_backend()
+class SlaveSecSocket(SecureSocket):
- def __init__(self, soc):
super(SlaveSecSocket, self).__init__(soc)
self._role = "server"
- def handshake(self, sec_params):
ctl_hello = self.recv_msg()
if ctl_hello["type"] != "ctl_hello":
raise SecSocketException("Handshake failed.")
self._ctl_random = ctl_hello["ctl_random"]
self._slave_random = os.urandom(28)
slave_hello = {"type": "slave_hello",
"slave_random": self._slave_random}
self.send_msg(slave_hello)
if sec_params["auth_types"] == "none":
self._dh_handshake()
elif sec_params["auth_types"] == "ssh":
self._ssh_handshake()
elif sec_params["auth_types"] == "pubkey":
srv_key = None
with open(sec_params["privkey"], 'r') as f:
srv_key = load_pem_private_key(f.read(), None, backend)
ctl_pubkeys = {}
for fname in os.listdir(sec_params["ctl_pubkeys"]):
path = os.path.join(sec_params["ctl_pubkeys"], fname)
if not os.path.isfile(path):
continue
with open(path, 'r') as f:
ctl_pubkeys[fname] = load_pem_public_key(f.read(), backend)
self._pubkey_handshake(srv_key, ctl_pubkeys)
elif sec_params["auth_types"] == "password":
self._passwd_handshake(sec_params["auth_password"])
else:
raise SecSocketException("Unknown authentication method.")
- def _validate_secret(self, handshake_data):
hashed_handshake_data = hashlib.sha256()
hashed_handshake_data.update(handshake_data)
srv_verify_data = self.PRF(self._master_secret,
"server finished",
hashed_handshake_data.digest(),
12)
finished_msg = {"type": "server finished",
"verify_data": srv_verify_data}
self.send_msg(finished_msg)
ctl_reply = self.recv_msg()
if ctl_reply["type"] != "ctl finished":
raise SecSocketException("Handshake failed.")
ctl_verify_data = self.PRF(self._master_secret,
"ctl finished",
hashed_handshake_data.digest(),
12)
if ctl_verify_data != ctl_reply["verify_data"]:
raise SecSocketException("Handshake failed.")
return
- def _dh_handshake(self):
modp_group = DH_GROUP
#private exponent
srv_privkey = int(os.urandom(modp_group["q_size"]+1).encode('hex'), 16)
srv_privkey = srv_privkey % modp_group["q"]
#public key
srv_pubkey = pow(modp_group["g"], srv_privkey, modp_group["p"])
msg = {"type": "pub_dh",
"value": srv_pubkey}
self.send_msg(msg)
reply = self.recv_msg()
if reply["type"] != "pub_dh":
raise SecSocketException("Handshake failed.")
ctl_pubkey = reply["value"]
ZZ = pow(ctl_pubkey, srv_privkey, modp_group["p"])
ZZ = "{1:0{0}x}".format(modp_group['p_size']*2, ZZ)
ZZ = self._master_secret.decode('hex')
self._master_secret = self.PRF(ZZ,
"master secret",
self._ctl_random + self._slave_random,
48)
handshake_data = ""
handshake_data += ("{1:0{0}x}".format(modp_group['p_size']*2,
ctl_pubkey)).decode('hex')
handshake_data += ("{1:0{0}x}".format(modp_group['p_size']*2,
srv_pubkey)).decode('hex')
self._init_cipher_spec()
self._send_change_cipher_spec()
self._validate_secret(handshake_data)
- def _ssh_handshake(self):
srv_keys = []
srv_pubkeys = []
authorized_keys = []
sshd_key_paths = ["/etc/ssh/ssh_host_rsa_key",
"/etc/ssh/ssh_host_ecdsa_key"]
ssh_dir_path = os.path.expanduser("~/.ssh")
for f_name in sshd_key_paths:
with open(f_name, 'r') as f:
srv_keys.append(load_pem_private_key(f.read(), None, backend))
srv_pubkeys.append(srv_keys[-1].public_key())
with open(ssh_dir_path+"/authorized_keys", 'r') as f:
for line in f.readlines():
authorized_keys.append(load_ssh_public_key(line, backend))
msg = self.recv_msg()
if msg["type"] != "ssh_client_hello":
raise SecSocketException("Handshake failed.")
ctl_ssh_pubkey = load_pem_public_key(msg["ctl_ssh_pubkey"], backend)
authorized = False
for key in authorized_keys:
if self._cmp_pub_keys(key, ctl_ssh_pubkey):
authorized = True
break
if not authorized:
raise SecSocketException("Handshake failed.")
pem_pubkeys = []
for key in srv_pubkeys:
pem_key = key.public_bytes(
encoding=ser.Encoding.PEM,
format=ser.PublicFormat.SubjectPublicKeyInfo)
pem_pubkeys.append(pem_key)
msg = {"type": "ssh_server_hello",
"srv_ssh_pubkeys": pem_pubkeys}
self.send_msg(msg)
msg = self.recv_msg()
if msg["type"] != "ssh_client_key_select":
raise SecSocketException("Handshake failed.")
if not self._verify_signature(ctl_ssh_pubkey,
str(msg["index"]),
msg["signature"]):
raise SecSocketException("Handshake failed.")
self._pubkey_handshake(srv_keys[msg["index"]], {"ssh": ctl_ssh_pubkey})
- def _pubkey_handshake(self, srv_privkey, client_pubkeys):
modp_group = DH_GROUP
#private exponent
srv_dh_privkey = int(os.urandom(modp_group["q_size"]+1).encode('hex'),
16)
srv_dh_privkey = srv_dh_privkey % modp_group["q"]
#public key
srv_dh_pubkey_int = pow(modp_group["g"],
srv_dh_privkey,
modp_group["p"])
srv_dh_pubkey = "{1:0{0}x}".format(modp_group['p_size']*2,
srv_dh_pubkey_int)
srv_dh_pubkey = srv_dh_pubkey.decode('hex')
msg = self.recv_msg()
if msg["type"] != "pubkey_client_hello":
raise SecSocketException("Handshake failed.")
ctl_identity = msg["identity"]
ctl_pubkey = load_pem_public_key(msg["ctl_pubkey"], backend)
local_ctl_pubkey = client_pubkeys[ctl_identity]
if not self._cmp_pub_keys(local_ctl_pubkey, ctl_pubkey):
raise SecSocketException("Handshake failed.")
ctl_dh_pubkey = msg["ctl_pub_dh"]
signature = msg["signature"]
if not self._verify_signature(local_ctl_pubkey,
ctl_dh_pubkey,
signature):
raise SecSocketException("Handshake failed.")
ctl_dh_pubkey_int = int(ctl_dh_pubkey.encode('hex'), 16)
srv_pubkey = srv_privkey.public_key()
srv_pubkey_pem = srv_pubkey.public_bytes(
encoding=ser.Encoding.PEM,
format=ser.PublicFormat.SubjectPublicKeyInfo)
signature = self._sign_data(srv_dh_pubkey, srv_privkey)
msg = {"type": "pubkey_server_hello",
"srv_pubkey": srv_pubkey_pem,
"srv_pub_dh": srv_dh_pubkey,
"signature": signature}
self.send_msg(msg)
ZZ = pow(ctl_dh_pubkey_int, srv_dh_privkey, modp_group["p"])
ZZ = "{1:0{0}x}".format(modp_group['p_size']*2, ZZ)
ZZ = self._master_secret.decode('hex')
self._master_secret = self.PRF(ZZ,
"master secret",
self._ctl_random + self._slave_random,
48)
self._init_cipher_spec()
self._send_change_cipher_spec()
- def _passwd_handshake(self, auth_passwd):
msg = self.recv_msg()
if msg["type"] != "srp_client_begin":
raise SecSocketException("Handshake failed.")
if msg["username"] != "lnst_user":
raise SecSocketException("Handshake failed.")
srp_group = SRP_GROUP
p_bytes = "{1:0{0}x}".format(srp_group['p_size']*2, srp_group['p'])
p_bytes = p_bytes.decode('hex')
g_bytes = "{0:02x}".format(srp_group['g'])
g_bytes = g_bytes.decode('hex')
k = hashlib.sha256(p_bytes + g_bytes).digest()
k = int(k.encode('hex'), 16)
username = msg["username"]
salt = os.urandom(16)
x = hashlib.sha256(salt + username + auth_passwd).digest()
x_int = int(x.encode('hex'), 16)
v = pow(srp_group["g"], x_int, srp_group["p"])
msg = {"type": "srp_server_salt",
"salt": salt}
self.send_msg(msg)
reply = self.recv_msg()
if reply["type"] != "srp_client_pub":
raise SecSocketException("Handshake failed.")
ctl_pubkey = reply["ctl_pubkey"]
ctl_pubkey_int = int(ctl_pubkey.encode('hex'), 16)
if (ctl_pubkey_int % srp_group["p"]) == 0:
raise SecSocketException("Handshake failed.")
srv_privkey = os.urandom(srp_group["q_size"]+1)
srv_privkey_int = int(srv_privkey.encode('hex'), 16) % srp_group["q"]
srv_pubkey_int = pow(srp_group["g"], srv_privkey_int, srp_group["p"])
srv_pubkey_int = (srv_pubkey_int + k*v) % srp_group["p"]
srv_pubkey = "{1:0{0}x}".format(srp_group['p_size']*2, srv_pubkey_int)
srv_pubkey = srv_pubkey.decode('hex')
msg = {"type": "srp_server_pub",
"srv_pubkey": srv_pubkey}
self.send_msg(msg)
u = hashlib.sha256(ctl_pubkey + srv_pubkey).digest()
u_int = int(u.encode('hex'), 16)
S_int = pow(v, u_int, srp_group['p'])*ctl_pubkey_int
S_int = pow(S_int, srv_privkey_int, srp_group["p"])
S = "{1:0{0}x}".format(srp_group['p_size']*2, S_int)
S = S.decode('hex')
msg = self.recv_msg()
if msg["type"] != "srp_client_m1":
raise SecSocketException("Handshake failed.")
client_m1 = msg["m1"]
srv_m1 = hashlib.sha256(ctl_pubkey + srv_pubkey + S).digest()
if client_m1 != srv_m1:
raise SecSocketException("Handshake failed.")
srv_m2 = hashlib.sha256(ctl_pubkey + srv_m1 + S).digest()
msg = {"type": "srp_server_m2",
"m2": srv_m2}
self.send_msg(msg)
K = hashlib.sha256(S).digest()
self._master_secret = self.PRF(K,
"master secret",
self._ctl_random + self._slave_random,
48)
self._init_cipher_spec()
self._send_change_cipher_spec()
-- 2.7.1 _______________________________________________ LNST-developers mailing list lnst-developers@lists.fedorahosted.org https://lists.fedorahosted.org/admin/lists/lnst-developers@lists.fedorahoste...
On Sat, Feb 27, 2016 at 12:07:03PM +0100, Jiri Pirko wrote:
Fri, Feb 26, 2016 at 04:00:00PM CET, olichtne@redhat.com wrote:
From: Ondrej Lichtner olichtne@redhat.com
Added 3 new classes: SecureSocket, CtlSecSocket and SlaveSecSocket. The SecureSocket class encapsulates a normal socket object and adds what is basically a TLS Record Layer - adds a signature, padding and encrypts the entire message.
The CtlSecSocket and SlaveSecSocket classes are child classes inheriting from the SecureSocket class and implement the handshake protocols used from the Controller or from the Slave side. The important method is handshake() that takes a sec_params parameter which is a dictionary describing how to do the handshake.
Signed-off-by: Ondrej Lichtner olichtne@redhat.com
lnst/Common/SecureSocket.py | 377 ++++++++++++++++++++++++++++++++++++++++ lnst/Controller/CtlSecSocket.py | 318 +++++++++++++++++++++++++++++++++ lnst/Slave/SlaveSecSocket.py | 323 ++++++++++++++++++++++++++++++++++ 3 files changed, 1018 insertions(+) create mode 100644 lnst/Common/SecureSocket.py create mode 100644 lnst/Controller/CtlSecSocket.py create mode 100644 lnst/Slave/SlaveSecSocket.py
diff --git a/lnst/Common/SecureSocket.py b/lnst/Common/SecureSocket.py new file mode 100644 index 0000000..d319c54 --- /dev/null +++ b/lnst/Common/SecureSocket.py @@ -0,0 +1,377 @@ +""" +This module defines a SecureSocket class that wraps the normal socket by adding +TLS-like functionality of providing data integrity, confidentiality and +authenticity. The reason why we're not using TLS is because the Python +implementation enforces the use of certificates and we want to also allow +password based authentication. This implements the common class, and the Slave +and Controller implement their sides of the handshake algorithms.
+Copyright 2016 Red Hat, Inc. +Licensed under the GNU General Public License, version 2 as +published by the Free Software Foundation; see COPYING for details. +"""
+__author__ = """ +olichtne@redhat.com (Ondrej Lichtner) +"""
+import os +import cPickle +import hashlib +import hmac +import cryptography.exceptions +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives.asymmetric import padding, ec +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey +from cryptography.hazmat.primitives.asymmetric.dsa import DSAPrivateKey +from cryptography.hazmat.primitives.asymmetric.dsa import DSAPublicKey +from cryptography.hazmat.backends import default_backend
+DH_GROUP = {"p": int("0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1"\
"29024E088A67CC74020BBEA63B139B22514A08798E3404DD"\
"EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245"\
"E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED"\
"EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D"\
"C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F"\
"83655D23DCA3AD961C62F356208552BB9ED529077096966D"\
"670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B"\
"E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9"\
"DE2BCBF6955817183995497CEA956AE515D2261898FA0510"\
"15728E5A8AACAA68FFFFFFFFFFFFFFFF", 16),
Wouldn't it make sense to make this configurable and not hard-coded?
Not really... these are VERY long primes - 2048 bit long _safe_ primes, plus the SRP parameters have one more restriction. I originally wanted to generate these on the fly, but that operation can take minutes... If I wanted to have these configurable I'd have to do a similar operation to check if their usable.
Besides, this is as far as I understand it completely ok to do and there are even RFCs with these precomputed (recommended) parameters which are public. The parameters I'm using are ones that I've generated through openssl so these should be a bit better than the RFC ones since their unique to us.
One other thing is that these parameters need to be IDENTICAL on both the client and the server side of the connection so if these were configurable I'd have to add another step to the handshake methods.
-Ondrej
From: Ondrej Lichtner olichtne@redhat.com
The slave machine XML files now contain security information used to negotiate a secure channel.
Signed-off-by: Ondrej Lichtner olichtne@redhat.com --- lnst/Controller/SlaveMachineParser.py | 31 +++++++++++++++++++++++++++++++ schema-sm.rng | 29 +++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+)
diff --git a/lnst/Controller/SlaveMachineParser.py b/lnst/Controller/SlaveMachineParser.py index 1b87a89..3fee680 100644 --- a/lnst/Controller/SlaveMachineParser.py +++ b/lnst/Controller/SlaveMachineParser.py @@ -11,6 +11,7 @@ __author__ = """ rpazdera@redhat.com (Radek Pazdera) """
+import os from lnst.Controller.XmlParser import XmlParser from lnst.Controller.XmlProcessing import XmlProcessingError, XmlData from lnst.Controller.XmlProcessing import XmlCollection @@ -39,6 +40,8 @@ class SlaveMachineParser(XmlParser): interface = self._process_interface(eth_tag) sm["interfaces"].append(interface)
+ security_tag = sm_tag.find("security") + sm["security"] = self._process_security(security_tag) return sm
def _process_params(self, params_tag): @@ -64,3 +67,31 @@ class SlaveMachineParser(XmlParser): iface["params"] = params
return iface + + def _process_security(self, sec_tag): + sec = XmlData(sec_tag) + + if sec_tag is None: + sec["auth_type"] = "none" + return sec + + auth_type_tag = sec_tag.find("auth_type") + sec["auth_type"] = auth_type_tag.text + + auth_passwd_tag = sec_tag.find("auth_password") + if auth_passwd_tag is not None: + sec["auth_passwd"] = auth_passwd_tag.text + else: + sec["auth_passwd"] = "" + + key_tag = sec_tag.find("pubkey_path") + if key_tag is not None: + path = key_tag.text + exp_path = os.path.expanduser(path) + abs_path = os.path.join(os.path.dirname(self._path), exp_path) + norm_path = os.path.normpath(abs_path) + sec["pubkey_path"] = norm_path + else: + sec["pubkey_path"] = "" + + return sec diff --git a/schema-sm.rng b/schema-sm.rng index f9d5a14..8bd6a35 100644 --- a/schema-sm.rng +++ b/schema-sm.rng @@ -7,6 +7,10 @@ <ref name="define"/> </optional>
+ <optional> + <ref name="security"/> + </optional> + <zeroOrMore> <ref name="params"/> </zeroOrMore> @@ -79,4 +83,29 @@ </interleave> </element> </define> + + <define name="security"> + <element name="security"> + <element name="auth_type"> + <choice> + <value>none</value> + <value>pubkey</value> + <value>ssh</value> + <value>password</value> + </choice> + </element> + + <optional> + <element name="pubkey_path"> + <text/> + </element> + </optional> + + <optional> + <element name="auth_password"> + <text/> + </element> + </optional> + </element> + </define> </grammar>
From: Ondrej Lichtner olichtne@redhat.com
The Machine object now includes the security information of the machine that was parsed from the Slave Machine XML file. To complete the security information it also includes the Controller identification information to the dictionary holding the security information.
Signed-off-by: Ondrej Lichtner olichtne@redhat.com --- lnst/Controller/Machine.py | 8 +++++++- lnst/Controller/SlavePool.py | 9 ++++++--- 2 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/lnst/Controller/Machine.py b/lnst/Controller/Machine.py index 9897580..62393b8 100644 --- a/lnst/Controller/Machine.py +++ b/lnst/Controller/Machine.py @@ -42,13 +42,19 @@ class Machine(object): deconfiguration, and running commands. """
- def __init__(self, m_id, hostname=None, libvirt_domain=None, rpcport=None): + def __init__(self, m_id, hostname=None, libvirt_domain=None, rpcport=None, + security=None): self._id = m_id self._hostname = hostname self._slave_desc = None self._connection = None self._configured = False self._system_config = {} + self._security = security + self._security["identity"] = lnst_config.get_option("security", + "identity") + self._security["privkey"] = lnst_config.get_option("security", + "privkey")
self._domain_ctl = None self._network_bridges = None diff --git a/lnst/Controller/SlavePool.py b/lnst/Controller/SlavePool.py index 87bc850..24a21d8 100644 --- a/lnst/Controller/SlavePool.py +++ b/lnst/Controller/SlavePool.py @@ -167,7 +167,7 @@ class SlavePool: return (None, None)
def _process_machine_xml_data(self, m_id, machine_xml_data): - machine_spec = {"interfaces": {}, "params":{}} + machine_spec = {"interfaces": {}, "params":{}, "security": {}}
# process parameters if "params" in machine_xml_data: @@ -205,6 +205,8 @@ class SlavePool: % m_id raise SlaveMachineError(msg, machine_xml_data)
+ machine_spec["security"] = machine_xml_data["security"] + return machine_spec
def _process_iface_xml_data(self, m_id, iface): @@ -304,7 +306,7 @@ class SlavePool: if "rpc_port" in pm["params"]: rpcport = pm["params"]["rpc_port"]
- machine = Machine(tm_id, hostname, None, rpcport) + machine = Machine(tm_id, hostname, None, rpcport, pm["security"])
used = [] if_map = self._map["machines"][tm_id]["interfaces"] @@ -340,7 +342,8 @@ class SlavePool: if "rpc_port" in pm["params"]: rpcport = pm["params"]["rpc_port"]
- machine = Machine(tm_id, hostname, libvirt_domain, rpcport) + machine = Machine(tm_id, hostname, libvirt_domain, rpcport, + pm["security"])
# make all the existing unused for if_id, if_data in pm["interfaces"].iteritems():
From: Ondrej Lichtner olichtne@redhat.com
This patch replaces the sockets used for the Controller<->Slave communication with CtlSecSocket and SlaveSecSocket. This includes some changes to the ConnectionHandler by adding SecureSocket support and removing the socket support since it's not needed anymore (at least for now).
Caution: Right now this breaks the functionality of the lnst-ctl deconfigure command and the lnst-pool-wizard, which are at the moment not able to use the secured sockets.
Signed-off-by: Ondrej Lichtner olichtne@redhat.com --- lnst/Common/ConnectionHandler.py | 39 ++++++++++----------------------------- lnst/Controller/Machine.py | 5 ++++- lnst/Slave/NetTestSlave.py | 13 +++++++++++-- 3 files changed, 25 insertions(+), 32 deletions(-)
diff --git a/lnst/Common/ConnectionHandler.py b/lnst/Common/ConnectionHandler.py index 36349dc..5d3170a 100644 --- a/lnst/Common/ConnectionHandler.py +++ b/lnst/Common/ConnectionHandler.py @@ -12,19 +12,15 @@ olichtne@redhat.com (Ondrej Lichtner) """
import select -import cPickle import socket from _multiprocessing import Connection from pyroute2 import IPRSocket +from lnst.Common.SecureSocket import SecureSocket, SecSocketException
def send_data(s, data): try: - if isinstance(s, socket.SocketType): - pickled_data = cPickle.dumps(data) - length = len(pickled_data) - - data_to_send = str(length) + " " + pickled_data - s.sendall(data_to_send) + if isinstance(s, SecureSocket): + s.send_msg(data) elif isinstance(s, Connection): s.send(data) else: @@ -37,27 +33,11 @@ def recv_data(s): if isinstance(s, IPRSocket): msg = s.get() data = {"type": "netlink", "data": msg} - elif isinstance(s, socket.SocketType): - length = "" - while True: - c = s.recv(1) - if c == ' ': - length = int(length) - break - elif c == "": - return "" - else: - length += c - data = "" - - while len(data)<length: - c = s.recv(length - len(data)) - if c == "": - return "" - else: - data += c - - data = cPickle.loads(data) + elif isinstance(s, SecureSocket): + try: + data = s.recv_msg() + except SecSocketException: + return "" elif isinstance(s, Connection): data = s.recv() else: @@ -95,10 +75,11 @@ class ConnectionHandler(object): f.close() self.remove_connection(f) f_ready = False - else: + elif data is not None: id = self.get_connection_id(f) requests.append((id, data))
+ if f_ready: #poll the file descriptor if there is another message rll, _, _ = select.select([f], [], [], 0) if rll == []: diff --git a/lnst/Controller/Machine.py b/lnst/Controller/Machine.py index 62393b8..839ec16 100644 --- a/lnst/Controller/Machine.py +++ b/lnst/Controller/Machine.py @@ -23,6 +23,7 @@ from lnst.Common.NetUtils import normalize_hwaddr from lnst.Common.Utils import wait_for, create_tar_archive from lnst.Common.Utils import check_process_running from lnst.Common.NetTestCommand import DEFAULT_TIMEOUT +from lnst.Controller.CtlSecSocket import CtlSecSocket
# conditional support for libvirt if check_process_running("libvirtd"): @@ -215,7 +216,9 @@ class Machine(object): m_id = self._id
logging.info("Connecting to RPC on machine %s (%s)", m_id, hostname) - connection = socket.create_connection((hostname, port)) + connection = CtlSecSocket(socket.create_connection((hostname, port))) + connection.handshake(self._security) + self._msg_dispatcher.add_slave(self, connection)
hello, slave_desc = self._rpc_call("hello", recipe_name) diff --git a/lnst/Slave/NetTestSlave.py b/lnst/Slave/NetTestSlave.py index 1428497..3b8bf79 100644 --- a/lnst/Slave/NetTestSlave.py +++ b/lnst/Slave/NetTestSlave.py @@ -37,6 +37,7 @@ from lnst.Common.Config import lnst_config from lnst.Common.Config import DefaultRPCPort from lnst.Slave.InterfaceManager import InterfaceManager from lnst.Slave.BridgeTool import BridgeTool +from lnst.Slave.SlaveSecSocket import SlaveSecSocket, SecSocketException
class SlaveMethods: ''' @@ -837,11 +838,19 @@ class ServerHandler(ConnectionHandler): self._netns = None self._c_socket = None
+ self._security = lnst_config.get_section_values("security") + def accept_connection(self): self._c_socket, addr = self._s_socket.accept() - self._c_socket = (self._c_socket, addr[0]) + self._c_socket = (SlaveSecSocket(self._c_socket), addr[0]) logging.info("Recieved connection from %s" % self._c_socket[1])
+ try: + self._c_socket[0].handshake(self._security) + except: + self.close_c_sock() + raise + self.add_connection(self._c_socket[1], self._c_socket[0]) return self._c_socket
@@ -974,7 +983,7 @@ class NetTestSlave: try: logging.info("Waiting for connection.") self._server_handler.accept_connection() - except socket.error: + except (socket.error, SecSocketException): continue self._log_ctl.set_connection( self._server_handler.get_ctl_sock())
From: Ondrej Lichtner olichtne@redhat.com
This name and beggining of description fits better to what the method actually does.
Signed-off-by: Ondrej Lichtner olichtne@redhat.com --- lnst/Controller/Machine.py | 4 ++-- lnst/Controller/NetTestController.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/lnst/Controller/Machine.py b/lnst/Controller/Machine.py index 839ec16..4112bd0 100644 --- a/lnst/Controller/Machine.py +++ b/lnst/Controller/Machine.py @@ -203,8 +203,8 @@ class Machine(object): return self._rpc_call(method_name, *args) return self._rpc_call_to_netns(netns, method_name, *args)
- def configure(self, recipe_name): - """ Prepare the machine + def init_connection(self, recipe_name): + """ Initialize the slave connection
Calling this method will initialize the rpc connection to the machine and initialize all the interfaces. Note, that it will diff --git a/lnst/Controller/NetTestController.py b/lnst/Controller/NetTestController.py index 8fdd43e..e1b2054 100644 --- a/lnst/Controller/NetTestController.py +++ b/lnst/Controller/NetTestController.py @@ -258,7 +258,7 @@ class NetTestController: machine.set_network_bridges(self._network_bridges)
recipe_name = os.path.basename(self._recipe_path) - machine.configure(recipe_name) + machine.init_connection(recipe_name)
sync_table = {'module': {}, 'tools': {}} if resource_sync:
Fri, Feb 26, 2016 at 03:59:57PM CET, olichtne@redhat.com wrote:
From: Ondrej Lichtner olichtne@redhat.com
First of all, this isn't a patchset to be pulled into the repository yet, just a request for comments to see if anything can be improved, while I work on finishing some minor problem areas:
- this breaks the lnst-pool-wizard because it doesn't use SecureSockets yet
- this breaks the lnst-ctl deconfigure command for the same reason
The following patch set implements secure communication between the Controller and the Slave. It implements 4 different authentication mechanisms: none - no authentication, just DH secret negotiation and encryption password - password authenticated secret negotiation using SRP-6a protocol and encryption pubkey - DH secret negotiation, authentication by using a private-public key pairs ssh - the same as pubkey, but uses ssh keys already present on the system
By default, both the Controller and Slave assume the "none" mechanism so everything should work just as before without any additional work needed. To try the other mechanisms you'll need to configure both the Controller and the Slave as follows: password: on Slave edit the lnst-slave.conf like this: [security] auth_types = password auth_password = your_chosen_password
on Controller edit the SlaveMachineXML file of the slave and add this:
<security> <auth_type>password</auth_type> <auth_password>your_chosen_password</auth_password> </security> under the main <slavemachine> element
pubkey: generate a private-public key pair on both the Slave and the Controller and exchange the public keys between the machines.
on Slave edit the lnst-slave.conf file like this: [security] auth_types = pubkey privkey = path/to/slave_private_key ctl_pubkeys = path/to/pubkeys_dir/
on Controller edit the SlaveMachineXML and add this:
<security> <auth_type>pubkey</auth_type> <pubkey_path>path/to/slave_pubkey</pubkey_path> </security> under the main <slavemachine> element and edit the lnst-ctl.conf like this: [security] identity = ctl_name privkey = path/to/ctl_privatekey
On the Slave the Controller public key needs to be placed into the ctl_pubkeys directory and the filename must match the value of the identity option in the lnst-ctl.conf
ssh: have ssh presetup between the Controller and Slave machines - the slave needs its sshd private keys to be located in /etc/ssh (ssh_host.*key files) and the controllers public key must be in the ~/.ssh/authorized_keys file On the Controller you need to have the ~/.ssh/id_rsa file to contain a private RSA key (the public part is in the slaves authorized_keys file) and you also need to have at least one of the Slaves public keys located in the ~/.ssh/known_hosts file
you can easily achieve this by just starting the sshd daemon on the Slave machine and on the Controller generate the key with ssh-keygen -t rsa -C "your_email@example.com" and run ssh-copy-id username@slavehostname
then configure the LNST Slave - lnst-slave.conf: [security] auth_types = ssh
and on the Controller edit the Slave Machine XML file like this:
<security> <auth_type>pubkey</auth_type> <pubkey_path>path/to/slave_pubkey</pubkey_path> </security>
This is awesome. I like this a lot! Looking forward to merge this so running lnst-slave is no longer a huge security issue :)
Ondrej Lichtner (7): Config: add get_section_values method Config: add security section and options to config Add SecureSocket classes SlaveMachineXML: add security information Machine: add security parameters of the slave machine Use SecureSockets for Ctl<->Slave communication Machine: rename method configure to init_connection
lnst/Common/Config.py | 44 ++++ lnst/Common/ConnectionHandler.py | 39 +--- lnst/Common/SecureSocket.py | 377 ++++++++++++++++++++++++++++++++++ lnst/Controller/CtlSecSocket.py | 318 ++++++++++++++++++++++++++++ lnst/Controller/Machine.py | 17 +- lnst/Controller/NetTestController.py | 2 +- lnst/Controller/SlaveMachineParser.py | 31 +++ lnst/Controller/SlavePool.py | 9 +- lnst/Slave/NetTestSlave.py | 13 +- lnst/Slave/SlaveSecSocket.py | 323 +++++++++++++++++++++++++++++ schema-sm.rng | 29 +++ 11 files changed, 1163 insertions(+), 39 deletions(-) create mode 100644 lnst/Common/SecureSocket.py create mode 100644 lnst/Controller/CtlSecSocket.py create mode 100644 lnst/Slave/SlaveSecSocket.py
-- 2.7.1 _______________________________________________ LNST-developers mailing list lnst-developers@lists.fedorahosted.org https://lists.fedorahosted.org/admin/lists/lnst-developers@lists.fedorahoste...
lnst-developers@lists.fedorahosted.org