From c40491e1e7a9475b965d2c41215c81482f4d7959823158d775e5ee86a29f11fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= Date: Thu, 31 Mar 2022 08:43:42 +0000 Subject: [PATCH] osc copypac from project:systemsmanagement:saltstack:testing package:salt revision:436 OBS-URL: https://build.opensuse.org/package/show/systemsmanagement:saltstack/salt?expand=0&rev=198 --- _lastrevision | 2 +- ...multiple-security-issues-bsc-1197417.patch | 2946 +++++++++++++++++ salt.changes | 12 + salt.spec | 3 + 4 files changed, 2962 insertions(+), 1 deletion(-) create mode 100644 fix-multiple-security-issues-bsc-1197417.patch diff --git a/_lastrevision b/_lastrevision index 9790d04..efea0ae 100644 --- a/_lastrevision +++ b/_lastrevision @@ -1 +1 @@ -8fe3232b41facbf938d591053c0f457ba6b5e3dc \ No newline at end of file +d46259fd4b90be19c6c5a2b083157c26e3beb72b \ No newline at end of file diff --git a/fix-multiple-security-issues-bsc-1197417.patch b/fix-multiple-security-issues-bsc-1197417.patch new file mode 100644 index 0000000..902aa91 --- /dev/null +++ b/fix-multiple-security-issues-bsc-1197417.patch @@ -0,0 +1,2946 @@ +From a5a3839eae2aed3e2fe98c314e770560eed2ed70 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= + +Date: Wed, 23 Mar 2022 12:09:36 +0000 +Subject: [PATCH] Fix multiple security issues (bsc#1197417) + +* Sign authentication replies to prevent MiTM (CVE-2020-22935) +* Sign pillar data to prevent MiTM attacks. (CVE-2022-22934) +* Prevent job and fileserver replays (CVE-2022-22936) +* Fixed targeting bug, especially visible when using syndic and user auth. (CVE-2022-22941) +--- + salt/crypt.py | 275 +++-- + salt/master.py | 57 +- + salt/minion.py | 1 + + salt/pillar/__init__.py | 4 + + salt/transport/mixins/auth.py | 115 +- + salt/transport/tcp.py | 103 +- + salt/transport/zeromq.py | 91 +- + salt/utils/minions.py | 19 +- + salt/utils/network.py | 4 +- + tests/integration/files/ssh/known_hosts | 2 + + tests/integration/modules/test_ssh.py | 9 +- + .../states/test_ssh_known_hosts.py | 3 +- + .../transport/server/test_req_channel.py | 16 +- + .../zeromq/test_pub_server_channel.py | 57 +- + tests/pytests/unit/test_crypt.py | 151 +++ + tests/pytests/unit/test_minion.py | 1 + + tests/pytests/unit/transport/test_tcp.py | 20 +- + tests/pytests/unit/transport/test_zeromq.py | 1037 +++++++++++++++++ + tests/pytests/unit/utils/test_minions.py | 59 + + tests/pytests/unit/utils/test_network.py | 8 + + tests/unit/transport/test_ipc.py | 2 + + 21 files changed, 1779 insertions(+), 255 deletions(-) + create mode 100644 tests/pytests/unit/utils/test_network.py + +diff --git a/salt/crypt.py b/salt/crypt.py +index 776ffaba58..76870216fd 100644 +--- a/salt/crypt.py ++++ b/salt/crypt.py +@@ -17,6 +17,7 @@ import stat + import sys + import time + import traceback ++import uuid + import weakref + + import salt.defaults.exitcodes +@@ -262,7 +263,11 @@ def verify_signature(pubkey_path, message, signature): + md = EVP.MessageDigest("sha1") + md.update(salt.utils.stringutils.to_bytes(message)) + digest = md.final() +- return pubkey.verify(digest, signature) ++ try: ++ return pubkey.verify(digest, signature) ++ except RSA.RSAError as exc: ++ log.debug("Signature verification failed: %s", exc.args[0]) ++ return False + else: + verifier = PKCS1_v1_5.new(pubkey) + return verifier.verify( +@@ -696,9 +701,17 @@ class AsyncAuth: + self._authenticate_future.set_exception(error) + else: + key = self.__key(self.opts) +- AsyncAuth.creds_map[key] = creds +- self._creds = creds +- self._crypticle = Crypticle(self.opts, creds["aes"]) ++ if key not in AsyncAuth.creds_map: ++ log.debug("%s Got new master aes key.", self) ++ AsyncAuth.creds_map[key] = creds ++ self._creds = creds ++ self._crypticle = Crypticle(self.opts, creds["aes"]) ++ elif self._creds["aes"] != creds["aes"]: ++ log.debug("%s The master's aes key has changed.", self) ++ AsyncAuth.creds_map[key] = creds ++ self._creds = creds ++ self._crypticle = Crypticle(self.opts, creds["aes"]) ++ + self._authenticate_future.set_result( + True + ) # mark the sign-in as complete +@@ -729,7 +742,6 @@ class AsyncAuth: + with the publication port and the shared AES key. + + """ +- auth = {} + + auth_timeout = self.opts.get("auth_timeout", None) + if auth_timeout is not None: +@@ -741,10 +753,6 @@ class AsyncAuth: + if auth_tries is not None: + tries = auth_tries + +- m_pub_fn = os.path.join(self.opts["pki_dir"], self.mpub) +- +- auth["master_uri"] = self.opts["master_uri"] +- + close_channel = False + if not channel: + close_channel = True +@@ -769,59 +777,85 @@ class AsyncAuth: + finally: + if close_channel: + channel.close() ++ ret = self.handle_signin_response(sign_in_payload, payload) ++ raise salt.ext.tornado.gen.Return(ret) + +- if not isinstance(payload, dict): ++ def handle_signin_response(self, sign_in_payload, payload): ++ auth = {} ++ m_pub_fn = os.path.join(self.opts["pki_dir"], self.mpub) ++ auth["master_uri"] = self.opts["master_uri"] ++ if not isinstance(payload, dict) or "load" not in payload: + log.error("Sign-in attempt failed: %s", payload) +- raise salt.ext.tornado.gen.Return(False) +- if "load" in payload: +- if "ret" in payload["load"]: +- if not payload["load"]["ret"]: +- if self.opts["rejected_retry"]: +- log.error( +- "The Salt Master has rejected this minion's public " +- "key.\nTo repair this issue, delete the public key " +- "for this minion on the Salt Master.\nThe Salt " +- "Minion will attempt to to re-authenicate." +- ) +- raise salt.ext.tornado.gen.Return("retry") +- else: +- log.critical( +- "The Salt Master has rejected this minion's public " +- "key!\nTo repair this issue, delete the public key " +- "for this minion on the Salt Master and restart this " +- "minion.\nOr restart the Salt Master in open mode to " +- "clean out the keys. The Salt Minion will now exit." +- ) +- # Add a random sleep here for systems that are using a +- # a service manager to immediately restart the service +- # to avoid overloading the system +- time.sleep(random.randint(10, 20)) +- sys.exit(salt.defaults.exitcodes.EX_NOPERM) +- # has the master returned that its maxed out with minions? +- elif payload["load"]["ret"] == "full": +- raise salt.ext.tornado.gen.Return("full") +- else: ++ return False ++ ++ clear_signed_data = payload["load"] ++ clear_signature = payload["sig"] ++ payload = salt.payload.loads(clear_signed_data) ++ ++ if "pub_key" in payload: ++ auth["aes"] = self.verify_master( ++ payload, master_pub="token" in sign_in_payload ++ ) ++ if not auth["aes"]: ++ log.critical( ++ "The Salt Master server's public key did not authenticate!\n" ++ "The master may need to be updated if it is a version of Salt " ++ "lower than %s, or\n" ++ "If you are confident that you are connecting to a valid Salt " ++ "Master, then remove the master public key and restart the " ++ "Salt Minion.\nThe master public key can be found " ++ "at:\n%s", ++ salt.version.__version__, ++ m_pub_fn, ++ ) ++ raise SaltClientError("Invalid master key") ++ ++ master_pubkey_path = os.path.join(self.opts["pki_dir"], self.mpub) ++ if os.path.exists(master_pubkey_path) and not verify_signature( ++ master_pubkey_path, clear_signed_data, clear_signature ++ ): ++ log.critical("The payload signature did not validate.") ++ raise SaltClientError("Invalid signature") ++ ++ if payload["nonce"] != sign_in_payload["nonce"]: ++ log.critical("The payload nonce did not validate.") ++ raise SaltClientError("Invalid nonce") ++ ++ if "ret" in payload: ++ if not payload["ret"]: ++ if self.opts["rejected_retry"]: + log.error( +- "The Salt Master has cached the public key for this " +- "node, this salt minion will wait for %s seconds " +- "before attempting to re-authenticate", +- self.opts["acceptance_wait_time"], ++ "The Salt Master has rejected this minion's public " ++ "key.\nTo repair this issue, delete the public key " ++ "for this minion on the Salt Master.\nThe Salt " ++ "Minion will attempt to re-authenicate." + ) +- raise salt.ext.tornado.gen.Return("retry") +- auth["aes"] = self.verify_master(payload, master_pub="token" in sign_in_payload) +- if not auth["aes"]: +- log.critical( +- "The Salt Master server's public key did not authenticate!\n" +- "The master may need to be updated if it is a version of Salt " +- "lower than %s, or\n" +- "If you are confident that you are connecting to a valid Salt " +- "Master, then remove the master public key and restart the " +- "Salt Minion.\nThe master public key can be found " +- "at:\n%s", +- salt.version.__version__, +- m_pub_fn, +- ) +- raise SaltClientError("Invalid master key") ++ return "retry" ++ else: ++ log.critical( ++ "The Salt Master has rejected this minion's public " ++ "key!\nTo repair this issue, delete the public key " ++ "for this minion on the Salt Master and restart this " ++ "minion.\nOr restart the Salt Master in open mode to " ++ "clean out the keys. The Salt Minion will now exit." ++ ) ++ # Add a random sleep here for systems that are using a ++ # a service manager to immediately restart the service ++ # to avoid overloading the system ++ time.sleep(random.randint(10, 20)) ++ sys.exit(salt.defaults.exitcodes.EX_NOPERM) ++ # has the master returned that its maxed out with minions? ++ elif payload["ret"] == "full": ++ return "full" ++ else: ++ log.error( ++ "The Salt Master has cached the public key for this " ++ "node, this salt minion will wait for %s seconds " ++ "before attempting to re-authenticate", ++ self.opts["acceptance_wait_time"], ++ ) ++ return "retry" ++ + if self.opts.get("syndic_master", False): # Is syndic + syndic_finger = self.opts.get( + "syndic_finger", self.opts.get("master_finger", False) +@@ -843,8 +877,9 @@ class AsyncAuth: + != self.opts["master_finger"] + ): + self._finger_fail(self.opts["master_finger"], m_pub_fn) ++ + auth["publish_port"] = payload["publish_port"] +- raise salt.ext.tornado.gen.Return(auth) ++ return auth + + def get_keys(self): + """ +@@ -892,6 +927,7 @@ class AsyncAuth: + payload = {} + payload["cmd"] = "_auth" + payload["id"] = self.opts["id"] ++ payload["nonce"] = uuid.uuid4().hex + if "autosign_grains" in self.opts: + autosign_grains = {} + for grain in self.opts["autosign_grains"]: +@@ -1254,6 +1290,7 @@ class SAuth(AsyncAuth): + self.token = salt.utils.stringutils.to_bytes(Crypticle.generate_key_string()) + self.pub_path = os.path.join(self.opts["pki_dir"], "minion.pub") + self.rsa_path = os.path.join(self.opts["pki_dir"], "minion.pem") ++ self._creds = None + if "syndic_master" in self.opts: + self.mpub = "syndic_master.pub" + elif "alert_master" in self.opts: +@@ -1323,8 +1360,14 @@ class SAuth(AsyncAuth): + ) + continue + break +- self._creds = creds +- self._crypticle = Crypticle(self.opts, creds["aes"]) ++ if self._creds is None: ++ log.error("%s Got new master aes key.", self) ++ self._creds = creds ++ self._crypticle = Crypticle(self.opts, creds["aes"]) ++ elif self._creds["aes"] != creds["aes"]: ++ log.error("%s The master's aes key has changed.", self) ++ self._creds = creds ++ self._crypticle = Crypticle(self.opts, creds["aes"]) + + def sign_in(self, timeout=60, safe=True, tries=1, channel=None): + """ +@@ -1377,78 +1420,7 @@ class SAuth(AsyncAuth): + if close_channel: + channel.close() + +- if "load" in payload: +- if "ret" in payload["load"]: +- if not payload["load"]["ret"]: +- if self.opts["rejected_retry"]: +- log.error( +- "The Salt Master has rejected this minion's public " +- "key.\nTo repair this issue, delete the public key " +- "for this minion on the Salt Master.\nThe Salt " +- "Minion will attempt to to re-authenicate." +- ) +- return "retry" +- else: +- log.critical( +- "The Salt Master has rejected this minion's public " +- "key!\nTo repair this issue, delete the public key " +- "for this minion on the Salt Master and restart this " +- "minion.\nOr restart the Salt Master in open mode to " +- "clean out the keys. The Salt Minion will now exit." +- ) +- sys.exit(salt.defaults.exitcodes.EX_NOPERM) +- # has the master returned that its maxed out with minions? +- elif payload["load"]["ret"] == "full": +- return "full" +- else: +- log.error( +- "The Salt Master has cached the public key for this " +- "node. If this is the first time connecting to this " +- "master then this key may need to be accepted using " +- "'salt-key -a %s' on the salt master. This salt " +- "minion will wait for %s seconds before attempting " +- "to re-authenticate.", +- self.opts["id"], +- self.opts["acceptance_wait_time"], +- ) +- return "retry" +- auth["aes"] = self.verify_master(payload, master_pub="token" in sign_in_payload) +- if not auth["aes"]: +- log.critical( +- "The Salt Master server's public key did not authenticate!\n" +- "The master may need to be updated if it is a version of Salt " +- "lower than %s, or\n" +- "If you are confident that you are connecting to a valid Salt " +- "Master, then remove the master public key and restart the " +- "Salt Minion.\nThe master public key can be found " +- "at:\n%s", +- salt.version.__version__, +- m_pub_fn, +- ) +- sys.exit(42) +- if self.opts.get("syndic_master", False): # Is syndic +- syndic_finger = self.opts.get( +- "syndic_finger", self.opts.get("master_finger", False) +- ) +- if syndic_finger: +- if ( +- salt.utils.crypt.pem_finger( +- m_pub_fn, sum_type=self.opts["hash_type"] +- ) +- != syndic_finger +- ): +- self._finger_fail(syndic_finger, m_pub_fn) +- else: +- if self.opts.get("master_finger", False): +- if ( +- salt.utils.crypt.pem_finger( +- m_pub_fn, sum_type=self.opts["hash_type"] +- ) +- != self.opts["master_finger"] +- ): +- self._finger_fail(self.opts["master_finger"], m_pub_fn) +- auth["publish_port"] = payload["publish_port"] +- return auth ++ return self.handle_signin_response(sign_in_payload, payload) + + + class Crypticle: +@@ -1463,10 +1435,11 @@ class Crypticle: + AES_BLOCK_SIZE = 16 + SIG_SIZE = hashlib.sha256().digest_size + +- def __init__(self, opts, key_string, key_size=192): ++ def __init__(self, opts, key_string, key_size=192, serial=0): + self.key_string = key_string + self.keys = self.extract_keys(self.key_string, key_size) + self.key_size = key_size ++ self.serial = serial + + @classmethod + def generate_key_string(cls, key_size=192): +@@ -1536,13 +1509,17 @@ class Crypticle: + data = cypher.decrypt(data) + return data[: -data[-1]] + +- def dumps(self, obj): ++ def dumps(self, obj, nonce=None): + """ + Serialize and encrypt a python object + """ +- return self.encrypt(self.PICKLE_PAD + salt.payload.dumps(obj)) ++ if nonce: ++ toencrypt = self.PICKLE_PAD + nonce.encode() + salt.payload.dumps(obj) ++ else: ++ toencrypt = self.PICKLE_PAD + salt.payload.dumps(obj) ++ return self.encrypt(toencrypt) + +- def loads(self, data, raw=False): ++ def loads(self, data, raw=False, nonce=None): + """ + Decrypt and un-serialize a python object + """ +@@ -1550,5 +1527,25 @@ class Crypticle: + # simple integrity check to verify that we got meaningful data + if not data.startswith(self.PICKLE_PAD): + return {} +- load = salt.payload.loads(data[len(self.PICKLE_PAD) :], raw=raw) +- return load ++ data = data[len(self.PICKLE_PAD) :] ++ if nonce: ++ ret_nonce = data[:32].decode() ++ data = data[32:] ++ if ret_nonce != nonce: ++ raise SaltClientError("Nonce verification error") ++ payload = salt.payload.loads(data, raw=raw) ++ if isinstance(payload, dict): ++ if "serial" in payload: ++ serial = payload.pop("serial") ++ if serial <= self.serial: ++ log.critical( ++ "A message with an invalid serial was received.\n" ++ "this serial: %d\n" ++ "last serial: %d\n" ++ "The minion will not honor this request.", ++ serial, ++ self.serial, ++ ) ++ return {} ++ self.serial = serial ++ return payload +diff --git a/salt/master.py b/salt/master.py +index ee33bd8171..65b526c019 100644 +--- a/salt/master.py ++++ b/salt/master.py +@@ -129,6 +129,44 @@ class SMaster: + """ + return salt.daemons.masterapi.access_keys(self.opts) + ++ @classmethod ++ def get_serial(cls, opts=None, event=None): ++ with cls.secrets["aes"]["secret"].get_lock(): ++ if cls.secrets["aes"]["serial"].value == sys.maxsize: ++ cls.rotate_secrets(opts, event, use_lock=False) ++ else: ++ cls.secrets["aes"]["serial"].value += 1 ++ return cls.secrets["aes"]["serial"].value ++ ++ @classmethod ++ def rotate_secrets(cls, opts=None, event=None, use_lock=True): ++ log.info("Rotating master AES key") ++ if opts is None: ++ opts = {} ++ ++ for secret_key, secret_map in cls.secrets.items(): ++ # should be unnecessary-- since no one else should be modifying ++ if use_lock: ++ with secret_map["secret"].get_lock(): ++ secret_map["secret"].value = salt.utils.stringutils.to_bytes( ++ secret_map["reload"]() ++ ) ++ if "serial" in secret_map: ++ secret_map["serial"].value = 0 ++ else: ++ secret_map["secret"].value = salt.utils.stringutils.to_bytes( ++ secret_map["reload"]() ++ ) ++ if "serial" in secret_map: ++ secret_map["serial"].value = 0 ++ if event: ++ event.fire_event({"rotate_{}_key".format(secret_key): True}, tag="key") ++ ++ if opts.get("ping_on_rotate"): ++ # Ping all minions to get them to pick up the new key ++ log.debug("Pinging all connected minions due to key rotation") ++ salt.utils.master.ping_all_connected_minions(opts) ++ + + class Maintenance(salt.utils.process.SignalHandlingProcess): + """ +@@ -281,21 +319,8 @@ class Maintenance(salt.utils.process.SignalHandlingProcess): + to_rotate = True + + if to_rotate: +- log.info("Rotating master AES key") +- for secret_key, secret_map in SMaster.secrets.items(): +- # should be unnecessary-- since no one else should be modifying +- with secret_map["secret"].get_lock(): +- secret_map["secret"].value = salt.utils.stringutils.to_bytes( +- secret_map["reload"]() +- ) +- self.event.fire_event( +- {"rotate_{}_key".format(secret_key): True}, tag="key" +- ) ++ SMaster.rotate_secrets(self.opts, self.event) + self.rotate = now +- if self.opts.get("ping_on_rotate"): +- # Ping all minions to get them to pick up the new key +- log.debug("Pinging all connected minions due to key rotation") +- salt.utils.master.ping_all_connected_minions(self.opts) + + def handle_git_pillar(self): + """ +@@ -671,8 +696,12 @@ class Master(SMaster): + salt.crypt.Crypticle.generate_key_string() + ), + ), ++ "serial": multiprocessing.Value( ++ ctypes.c_longlong, lock=False # We'll use the lock from 'secret' ++ ), + "reload": salt.crypt.Crypticle.generate_key_string, + } ++ + log.info("Creating master process manager") + # Since there are children having their own ProcessManager we should wait for kill more time. + self.process_manager = salt.utils.process.ProcessManager(wait_for_kill=5) +diff --git a/salt/minion.py b/salt/minion.py +index dbce3986ab..de3ad50b5c 100644 +--- a/salt/minion.py ++++ b/salt/minion.py +@@ -1691,6 +1691,7 @@ class Minion(MinionBase): + Override this method if you wish to handle the decoded data + differently. + """ ++ + # Ensure payload is unicode. Disregard failure to decode binary blobs. + if "user" in data: + log.info( +diff --git a/salt/pillar/__init__.py b/salt/pillar/__init__.py +index 22f5c3a0a9..e595b3fb1b 100644 +--- a/salt/pillar/__init__.py ++++ b/salt/pillar/__init__.py +@@ -9,6 +9,7 @@ import logging + import os + import sys + import traceback ++import uuid + + import salt.ext.tornado.gen + import salt.fileclient +@@ -240,6 +241,9 @@ class AsyncRemotePillar(RemotePillarMixin): + load, + dictkey="pillar", + ) ++ except salt.crypt.AuthenticationError as exc: ++ log.error(exc.message) ++ raise SaltClientError("Exception getting pillar.") + except Exception: # pylint: disable=broad-except + log.exception("Exception getting pillar:") + raise SaltClientError("Exception getting pillar.") +diff --git a/salt/transport/mixins/auth.py b/salt/transport/mixins/auth.py +index 90197fb506..1e2e8e6b7b 100644 +--- a/salt/transport/mixins/auth.py ++++ b/salt/transport/mixins/auth.py +@@ -112,7 +112,7 @@ class AESReqServerMixin: + + self.master_key = salt.crypt.MasterKeys(self.opts) + +- def _encrypt_private(self, ret, dictkey, target): ++ def _encrypt_private(self, ret, dictkey, target, nonce=None, sign_messages=True): + """ + The server equivalent of ReqChannel.crypted_transfer_decode_dictentry + """ +@@ -127,7 +127,6 @@ class AESReqServerMixin: + except OSError: + log.error("AES key not found") + return {"error": "AES key not found"} +- + pret = {} + key = salt.utils.stringutils.to_bytes(key) + if HAS_M2: +@@ -135,9 +134,33 @@ class AESReqServerMixin: + else: + cipher = PKCS1_OAEP.new(pub) + pret["key"] = cipher.encrypt(key) +- pret[dictkey] = pcrypt.dumps(ret if ret is not False else {}) ++ if ret is False: ++ ret = {} ++ if sign_messages: ++ if nonce is None: ++ return {"error": "Nonce not included in request"} ++ tosign = salt.payload.dumps( ++ {"key": pret["key"], "pillar": ret, "nonce": nonce} ++ ) ++ master_pem_path = os.path.join(self.opts["pki_dir"], "master.pem") ++ signed_msg = { ++ "data": tosign, ++ "sig": salt.crypt.sign_message(master_pem_path, tosign), ++ } ++ pret[dictkey] = pcrypt.dumps(signed_msg) ++ else: ++ pret[dictkey] = pcrypt.dumps(ret) + return pret + ++ def _clear_signed(self, load): ++ master_pem_path = os.path.join(self.opts["pki_dir"], "master.pem") ++ tosign = salt.payload.dumps(load) ++ return { ++ "enc": "clear", ++ "load": tosign, ++ "sig": salt.crypt.sign_message(master_pem_path, tosign), ++ } ++ + def _update_aes(self): + """ + Check to see if a fresh AES key is available and update the components +@@ -164,7 +187,7 @@ class AESReqServerMixin: + payload["load"] = self.crypticle.loads(payload["load"]) + return payload + +- def _auth(self, load): ++ def _auth(self, load, sign_messages=False): + """ + Authenticate the client, use the sent public key to encrypt the AES key + which was generated at start up. +@@ -182,7 +205,10 @@ class AESReqServerMixin: + + if not salt.utils.verify.valid_id(self.opts, load["id"]): + log.info("Authentication request from invalid id %s", load["id"]) +- return {"enc": "clear", "load": {"ret": False}} ++ if sign_messages: ++ return self._clear_signed({"ret": False, "nonce": load["nonce"]}) ++ else: ++ return {"enc": "clear", "load": {"ret": False}} + log.info("Authentication request from %s", load["id"]) + + # 0 is default which should be 'unlimited' +@@ -220,7 +246,12 @@ class AESReqServerMixin: + self.event.fire_event( + eload, salt.utils.event.tagify(prefix="auth") + ) +- return {"enc": "clear", "load": {"ret": "full"}} ++ if sign_messages: ++ return self._clear_signed( ++ {"ret": "full", "nonce": load["nonce"]} ++ ) ++ else: ++ return {"enc": "clear", "load": {"ret": "full"}} + + # Check if key is configured to be auto-rejected/signed + auto_reject = self.auto_key.check_autoreject(load["id"]) +@@ -247,8 +278,10 @@ class AESReqServerMixin: + eload = {"result": False, "id": load["id"], "pub": load["pub"]} + if self.opts.get("auth_events") is True: + self.event.fire_event(eload, salt.utils.event.tagify(prefix="auth")) +- return {"enc": "clear", "load": {"ret": False}} +- ++ if sign_messages: ++ return self._clear_signed({"ret": False, "nonce": load["nonce"]}) ++ else: ++ return {"enc": "clear", "load": {"ret": False}} + elif os.path.isfile(pubfn): + # The key has been accepted, check it + with salt.utils.files.fopen(pubfn, "r") as pubfn_handle: +@@ -272,7 +305,12 @@ class AESReqServerMixin: + self.event.fire_event( + eload, salt.utils.event.tagify(prefix="auth") + ) +- return {"enc": "clear", "load": {"ret": False}} ++ if sign_messages: ++ return self._clear_signed( ++ {"ret": False, "nonce": load["nonce"]} ++ ) ++ else: ++ return {"enc": "clear", "load": {"ret": False}} + + elif not os.path.isfile(pubfn_pend): + # The key has not been accepted, this is a new minion +@@ -282,7 +320,10 @@ class AESReqServerMixin: + eload = {"result": False, "id": load["id"], "pub": load["pub"]} + if self.opts.get("auth_events") is True: + self.event.fire_event(eload, salt.utils.event.tagify(prefix="auth")) +- return {"enc": "clear", "load": {"ret": False}} ++ if sign_messages: ++ return self._clear_signed({"ret": False, "nonce": load["nonce"]}) ++ else: ++ return {"enc": "clear", "load": {"ret": False}} + + if auto_reject: + key_path = pubfn_rejected +@@ -305,7 +346,6 @@ class AESReqServerMixin: + # Write the key to the appropriate location + with salt.utils.files.fopen(key_path, "w+") as fp_: + fp_.write(load["pub"]) +- ret = {"enc": "clear", "load": {"ret": key_result}} + eload = { + "result": key_result, + "act": key_act, +@@ -314,7 +354,12 @@ class AESReqServerMixin: + } + if self.opts.get("auth_events") is True: + self.event.fire_event(eload, salt.utils.event.tagify(prefix="auth")) +- return ret ++ if sign_messages: ++ return self._clear_signed( ++ {"ret": key_result, "nonce": load["nonce"]} ++ ) ++ else: ++ return {"enc": "clear", "load": {"ret": key_result}} + + elif os.path.isfile(pubfn_pend): + # This key is in the pending dir and is awaiting acceptance +@@ -330,7 +375,6 @@ class AESReqServerMixin: + "Pending public key for %s rejected via autoreject_file", + load["id"], + ) +- ret = {"enc": "clear", "load": {"ret": False}} + eload = { + "result": False, + "act": "reject", +@@ -339,7 +383,10 @@ class AESReqServerMixin: + } + if self.opts.get("auth_events") is True: + self.event.fire_event(eload, salt.utils.event.tagify(prefix="auth")) +- return ret ++ if sign_messages: ++ return self._clear_signed({"ret": False, "nonce": load["nonce"]}) ++ else: ++ return {"enc": "clear", "load": {"ret": False}} + + elif not auto_sign: + # This key is in the pending dir and is not being auto-signed. +@@ -367,7 +414,12 @@ class AESReqServerMixin: + self.event.fire_event( + eload, salt.utils.event.tagify(prefix="auth") + ) +- return {"enc": "clear", "load": {"ret": False}} ++ if sign_messages: ++ return self._clear_signed( ++ {"ret": False, "nonce": load["nonce"]} ++ ) ++ else: ++ return {"enc": "clear", "load": {"ret": False}} + else: + log.info( + "Authentication failed from host %s, the key is in " +@@ -386,7 +438,12 @@ class AESReqServerMixin: + self.event.fire_event( + eload, salt.utils.event.tagify(prefix="auth") + ) +- return {"enc": "clear", "load": {"ret": True}} ++ if sign_messages: ++ return self._clear_signed( ++ {"ret": True, "nonce": load["nonce"]} ++ ) ++ else: ++ return {"enc": "clear", "load": {"ret": True}} + else: + # This key is in pending and has been configured to be + # auto-signed. Check to see if it is the same key, and if +@@ -408,7 +465,12 @@ class AESReqServerMixin: + self.event.fire_event( + eload, salt.utils.event.tagify(prefix="auth") + ) +- return {"enc": "clear", "load": {"ret": False}} ++ if sign_messages: ++ return self._clear_signed( ++ {"ret": False, "nonce": load["nonce"]} ++ ) ++ else: ++ return {"enc": "clear", "load": {"ret": False}} + else: + os.remove(pubfn_pend) + +@@ -418,7 +480,10 @@ class AESReqServerMixin: + eload = {"result": False, "id": load["id"], "pub": load["pub"]} + if self.opts.get("auth_events") is True: + self.event.fire_event(eload, salt.utils.event.tagify(prefix="auth")) +- return {"enc": "clear", "load": {"ret": False}} ++ if sign_messages: ++ return self._clear_signed({"ret": False, "nonce": load["nonce"]}) ++ else: ++ return {"enc": "clear", "load": {"ret": False}} + + log.info("Authentication accepted from %s", load["id"]) + # only write to disk if you are adding the file, and in open mode, +@@ -437,7 +502,10 @@ class AESReqServerMixin: + fp_.write(load["pub"]) + elif not load["pub"]: + log.error("Public key is empty: %s", load["id"]) +- return {"enc": "clear", "load": {"ret": False}} ++ if sign_messages: ++ return self._clear_signed({"ret": False, "nonce": load["nonce"]}) ++ else: ++ return {"enc": "clear", "load": {"ret": False}} + + pub = None + +@@ -451,7 +519,10 @@ class AESReqServerMixin: + pub = salt.crypt.get_rsa_pub_key(pubfn) + except salt.crypt.InvalidKeyError as err: + log.error('Corrupt public key "%s": %s', pubfn, err) +- return {"enc": "clear", "load": {"ret": False}} ++ if sign_messages: ++ return self._clear_signed({"ret": False, "nonce": load["nonce"]}) ++ else: ++ return {"enc": "clear", "load": {"ret": False}} + + if not HAS_M2: + cipher = PKCS1_OAEP.new(pub) +@@ -532,10 +603,14 @@ class AESReqServerMixin: + ret["aes"] = pub.public_encrypt(aes, RSA.pkcs1_oaep_padding) + else: + ret["aes"] = cipher.encrypt(aes) ++ + # Be aggressive about the signature + digest = salt.utils.stringutils.to_bytes(hashlib.sha256(aes).hexdigest()) + ret["sig"] = salt.crypt.private_encrypt(self.master_key.key, digest) + eload = {"result": True, "act": "accept", "id": load["id"], "pub": load["pub"]} + if self.opts.get("auth_events") is True: + self.event.fire_event(eload, salt.utils.event.tagify(prefix="auth")) ++ if sign_messages: ++ ret["nonce"] = load["nonce"] ++ return self._clear_signed(ret) + return ret +diff --git a/salt/transport/tcp.py b/salt/transport/tcp.py +index f8f51eab66..f00b3c40eb 100644 +--- a/salt/transport/tcp.py ++++ b/salt/transport/tcp.py +@@ -13,6 +13,7 @@ import threading + import time + import traceback + import urllib.parse ++import uuid + + import salt.crypt + import salt.exceptions +@@ -266,12 +267,15 @@ class AsyncTCPReqChannel(salt.transport.client.ReqChannel): + return { + "enc": self.crypt, + "load": load, ++ "version": 2, + } + + @salt.ext.tornado.gen.coroutine + def crypted_transfer_decode_dictentry( + self, load, dictkey=None, tries=3, timeout=60 + ): ++ nonce = uuid.uuid4().hex ++ load["nonce"] = nonce + if not self.auth.authenticated: + yield self.auth.authenticate() + ret = yield self.message_client.send( +@@ -285,10 +289,29 @@ class AsyncTCPReqChannel(salt.transport.client.ReqChannel): + else: + cipher = PKCS1_OAEP.new(key) + aes = cipher.decrypt(ret["key"]) ++ ++ # Decrypt using the public key. + pcrypt = salt.crypt.Crypticle(self.opts, aes) +- data = pcrypt.loads(ret[dictkey]) +- data = salt.transport.frame.decode_embedded_strs(data) +- raise salt.ext.tornado.gen.Return(data) ++ signed_msg = pcrypt.loads(ret[dictkey]) ++ ++ # Validate the master's signature. ++ master_pubkey_path = os.path.join(self.opts["pki_dir"], "minion_master.pub") ++ if not salt.crypt.verify_signature( ++ master_pubkey_path, signed_msg["data"], signed_msg["sig"] ++ ): ++ raise salt.crypt.AuthenticationError( ++ "Pillar payload signature failed to validate." ++ ) ++ ++ # Make sure the signed key matches the key we used to decrypt the data. ++ data = salt.payload.loads(signed_msg["data"]) ++ if data["key"] != ret["key"]: ++ raise salt.crypt.AuthenticationError("Key verification failed.") ++ ++ # Validate the nonce. ++ if data["nonce"] != nonce: ++ raise salt.crypt.AuthenticationError("Pillar nonce verification failed.") ++ raise salt.ext.tornado.gen.Return(data["pillar"]) + + @salt.ext.tornado.gen.coroutine + def _crypted_transfer(self, load, tries=3, timeout=60): +@@ -298,6 +321,9 @@ class AsyncTCPReqChannel(salt.transport.client.ReqChannel): + Indeed, we can fail too early in case of a master restart during a + minion state execution call + """ ++ nonce = uuid.uuid4().hex ++ if load and isinstance(load, dict): ++ load["nonce"] = nonce + + @salt.ext.tornado.gen.coroutine + def _do_transfer(): +@@ -311,7 +337,7 @@ class AsyncTCPReqChannel(salt.transport.client.ReqChannel): + # communication, we do not subscribe to return events, we just + # upload the results to the master + if data: +- data = self.auth.crypticle.loads(data) ++ data = self.auth.crypticle.loads(data, nonce=nonce) + data = salt.transport.frame.decode_embedded_strs(data) + raise salt.ext.tornado.gen.Return(data) + +@@ -395,6 +421,7 @@ class AsyncTCPPubChannel( + return { + "enc": self.crypt, + "load": load, ++ "version": 2, + } + + @salt.ext.tornado.gen.coroutine +@@ -696,6 +723,14 @@ class TCPReqServerChannel( + ) + raise salt.ext.tornado.gen.Return() + ++ version = 0 ++ if "version" in payload: ++ version = payload["version"] ++ ++ sign_messages = False ++ if version > 1: ++ sign_messages = True ++ + # intercept the "_auth" commands, since the main daemon shouldn't know + # anything about our key auth + if ( +@@ -704,11 +739,15 @@ class TCPReqServerChannel( + ): + yield stream.write( + salt.transport.frame.frame_msg( +- self._auth(payload["load"]), header=header ++ self._auth(payload["load"], sign_messages), header=header + ) + ) + raise salt.ext.tornado.gen.Return() + ++ nonce = None ++ if version > 1: ++ nonce = payload["load"].pop("nonce", None) ++ + # TODO: test + try: + ret, req_opts = yield self.payload_handler(payload) +@@ -727,7 +766,7 @@ class TCPReqServerChannel( + elif req_fun == "send": + stream.write( + salt.transport.frame.frame_msg( +- self.crypticle.dumps(ret), header=header ++ self.crypticle.dumps(ret, nonce), header=header + ) + ) + elif req_fun == "send_private": +@@ -737,6 +776,8 @@ class TCPReqServerChannel( + ret, + req_opts["key"], + req_opts["tgt"], ++ nonce, ++ sign_messages, + ), + header=header, + ) +@@ -1381,7 +1422,7 @@ class PubServer(salt.ext.tornado.tcpserver.TCPServer): + TCP publisher + """ + +- def __init__(self, opts, io_loop=None): ++ def __init__(self, opts, io_loop=None, pack_publish=lambda _: _): + super().__init__(ssl_options=opts.get("ssl")) + self.io_loop = io_loop + self.opts = opts +@@ -1408,6 +1449,10 @@ class PubServer(salt.ext.tornado.tcpserver.TCPServer): + ) + else: + self.event = None ++ self._pack_publish = pack_publish ++ ++ def pack_publish(self, load): ++ return self._pack_publish(load) + + def close(self): + if self._closing: +@@ -1516,6 +1561,7 @@ class PubServer(salt.ext.tornado.tcpserver.TCPServer): + @salt.ext.tornado.gen.coroutine + def publish_payload(self, package, _): + log.debug("TCP PubServer sending payload: %s", package) ++ payload = self.pack_publish(package) + payload = salt.transport.frame.frame_msg(package["payload"]) + + to_remove = [] +@@ -1591,7 +1637,9 @@ class TCPPubServerChannel(salt.transport.server.PubServerChannel): + self.io_loop = salt.ext.tornado.ioloop.IOLoop.current() + + # Spin up the publisher +- pub_server = PubServer(self.opts, io_loop=self.io_loop) ++ pub_server = PubServer( ++ self.opts, io_loop=self.io_loop, pack_publish=self.pack_publish ++ ) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + _set_tcp_keepalive(sock, self.opts) +@@ -1634,12 +1682,9 @@ class TCPPubServerChannel(salt.transport.server.PubServerChannel): + """ + process_manager.add_process(self._publish_daemon, kwargs=kwargs) + +- def publish(self, load): +- """ +- Publish "load" to minions +- """ ++ def pack_publish(self, load): + payload = {"enc": "aes"} +- ++ load["serial"] = salt.master.SMaster.get_serial() + crypticle = salt.crypt.Crypticle( + self.opts, salt.master.SMaster.secrets["aes"]["secret"].value + ) +@@ -1648,20 +1693,6 @@ class TCPPubServerChannel(salt.transport.server.PubServerChannel): + master_pem_path = os.path.join(self.opts["pki_dir"], "master.pem") + log.debug("Signing data packet") + payload["sig"] = salt.crypt.sign_message(master_pem_path, payload["load"]) +- # Use the Salt IPC server +- if self.opts.get("ipc_mode", "") == "tcp": +- pull_uri = int(self.opts.get("tcp_master_publish_pull", 4514)) +- else: +- pull_uri = os.path.join(self.opts["sock_dir"], "publish_pull.ipc") +- # TODO: switch to the actual asynchronous interface +- # pub_sock = salt.transport.ipc.IPCMessageClient(self.opts, io_loop=self.io_loop) +- pub_sock = salt.utils.asynchronous.SyncWrapper( +- salt.transport.ipc.IPCMessageClient, +- (pull_uri,), +- loop_kwarg="io_loop", +- ) +- pub_sock.connect() +- + int_payload = {"payload": salt.payload.dumps(payload)} + + # add some targeting stuff for lists only (for now) +@@ -1678,5 +1709,21 @@ class TCPPubServerChannel(salt.transport.server.PubServerChannel): + int_payload["topic_lst"] = match_ids + else: + int_payload["topic_lst"] = load["tgt"] ++ return int_payload ++ ++ def publish(self, load): ++ """ ++ Publish "load" to minions ++ """ + # Send it over IPC! +- pub_sock.send(int_payload) ++ if self.opts.get("ipc_mode", "") == "tcp": ++ pull_uri = int(self.opts.get("tcp_master_publish_pull", 4514)) ++ else: ++ pull_uri = os.path.join(self.opts["sock_dir"], "publish_pull.ipc") ++ pub_sock = salt.utils.asynchronous.SyncWrapper( ++ salt.transport.ipc.IPCMessageClient, ++ (pull_uri,), ++ loop_kwarg="io_loop", ++ ) ++ pub_sock.connect() ++ pub_sock.send(load) +diff --git a/salt/transport/zeromq.py b/salt/transport/zeromq.py +index 357fb08553..9e61b23255 100644 +--- a/salt/transport/zeromq.py ++++ b/salt/transport/zeromq.py +@@ -8,6 +8,7 @@ import os + import signal + import sys + import threading ++import uuid + from random import randint + + import salt.auth +@@ -55,6 +56,7 @@ except ImportError: + except ImportError: + from Crypto.Cipher import PKCS1_OAEP # nosec + ++ + log = logging.getLogger(__name__) + + +@@ -66,12 +68,12 @@ def _get_master_uri(master_ip, master_port, source_ip=None, source_port=None): + rc = zmq_connect(socket, "tcp://192.168.1.17:5555;192.168.1.1:5555"); assert (rc == 0); + Source: http://api.zeromq.org/4-1:zmq-tcp + """ ++ + from salt.utils.zeromq import ip_bracket + + master_uri = "tcp://{master_ip}:{master_port}".format( + master_ip=ip_bracket(master_ip), master_port=master_port + ) +- + if source_ip or source_port: + if LIBZMQ_VERSION_INFO >= (4, 1, 6) and ZMQ_VERSION_INFO >= (16, 0, 1): + # The source:port syntax for ZeroMQ has been added in libzmq 4.1.6 +@@ -211,22 +213,27 @@ class AsyncZeroMQReqChannel(salt.transport.client.ReqChannel): + return { + "enc": self.crypt, + "load": load, ++ "version": 2, + } + + @salt.ext.tornado.gen.coroutine + def crypted_transfer_decode_dictentry( + self, load, dictkey=None, tries=3, timeout=60 + ): ++ nonce = uuid.uuid4().hex ++ load["nonce"] = nonce + if not self.auth.authenticated: + # Return control back to the caller, continue when authentication succeeds + yield self.auth.authenticate() +- # Return control to the caller. When send() completes, resume by populating ret with the Future.result ++ ++ # Return control to the caller. When send() completes, resume by ++ # populating ret with the Future.result + ret = yield self.message_client.send( + self._package_load(self.auth.crypticle.dumps(load)), + timeout=timeout, + tries=tries, + ) +- key = self.auth.get_keys() ++ + if "key" not in ret: + # Reauth in the case our key is deleted on the master side. + yield self.auth.authenticate() +@@ -235,15 +242,36 @@ class AsyncZeroMQReqChannel(salt.transport.client.ReqChannel): + timeout=timeout, + tries=tries, + ) ++ ++ key = self.auth.get_keys() + if HAS_M2: + aes = key.private_decrypt(ret["key"], RSA.pkcs1_oaep_padding) + else: + cipher = PKCS1_OAEP.new(key) + aes = cipher.decrypt(ret["key"]) ++ ++ # Decrypt using the public key. + pcrypt = salt.crypt.Crypticle(self.opts, aes) +- data = pcrypt.loads(ret[dictkey]) +- data = salt.transport.frame.decode_embedded_strs(data) +- raise salt.ext.tornado.gen.Return(data) ++ signed_msg = pcrypt.loads(ret[dictkey]) ++ ++ # Validate the master's signature. ++ master_pubkey_path = os.path.join(self.opts["pki_dir"], "minion_master.pub") ++ if not salt.crypt.verify_signature( ++ master_pubkey_path, signed_msg["data"], signed_msg["sig"] ++ ): ++ raise salt.crypt.AuthenticationError( ++ "Pillar payload signature failed to validate." ++ ) ++ ++ # Make sure the signed key matches the key we used to decrypt the data. ++ data = salt.payload.loads(signed_msg["data"]) ++ if data["key"] != ret["key"]: ++ raise salt.crypt.AuthenticationError("Key verification failed.") ++ ++ # Validate the nonce. ++ if data["nonce"] != nonce: ++ raise salt.crypt.AuthenticationError("Pillar nonce verification failed.") ++ raise salt.ext.tornado.gen.Return(data["pillar"]) + + @salt.ext.tornado.gen.coroutine + def _crypted_transfer(self, load, tries=3, timeout=60, raw=False): +@@ -260,6 +288,9 @@ class AsyncZeroMQReqChannel(salt.transport.client.ReqChannel): + :param int tries: The number of times to make before failure + :param int timeout: The number of seconds on a response before failing + """ ++ nonce = uuid.uuid4().hex ++ if load and isinstance(load, dict): ++ load["nonce"] = nonce + + @salt.ext.tornado.gen.coroutine + def _do_transfer(): +@@ -274,7 +305,7 @@ class AsyncZeroMQReqChannel(salt.transport.client.ReqChannel): + # communication, we do not subscribe to return events, we just + # upload the results to the master + if data: +- data = self.auth.crypticle.loads(data, raw) ++ data = self.auth.crypticle.loads(data, raw, nonce) + if not raw: + data = salt.transport.frame.decode_embedded_strs(data) + raise salt.ext.tornado.gen.Return(data) +@@ -735,12 +766,24 @@ class ZeroMQReqServerChannel( + ) + raise salt.ext.tornado.gen.Return() + ++ version = 0 ++ if "version" in payload: ++ version = payload["version"] ++ ++ sign_messages = False ++ if version > 1: ++ sign_messages = True ++ + # intercept the "_auth" commands, since the main daemon shouldn't know + # anything about our key auth + if payload["enc"] == "clear" and payload.get("load", {}).get("cmd") == "_auth": +- stream.send(salt.payload.dumps(self._auth(payload["load"]))) ++ stream.send(salt.payload.dumps(self._auth(payload["load"], sign_messages))) + raise salt.ext.tornado.gen.Return() + ++ nonce = None ++ if version > 1: ++ nonce = payload["load"].pop("nonce", None) ++ + # TODO: test + try: + # Take the payload_handler function that was registered when we created the channel +@@ -756,7 +799,7 @@ class ZeroMQReqServerChannel( + if req_fun == "send_clear": + stream.send(salt.payload.dumps(ret)) + elif req_fun == "send": +- stream.send(salt.payload.dumps(self.crypticle.dumps(ret))) ++ stream.send(salt.payload.dumps(self.crypticle.dumps(ret, nonce))) + elif req_fun == "send_private": + stream.send( + salt.payload.dumps( +@@ -764,6 +807,8 @@ class ZeroMQReqServerChannel( + ret, + req_opts["key"], + req_opts["tgt"], ++ nonce, ++ sign_messages, + ) + ) + ) +@@ -894,6 +939,8 @@ class ZeroMQPubServerChannel(salt.transport.server.PubServerChannel): + try: + log.debug("Publish daemon getting data from puller %s", pull_uri) + package = pull_sock.recv() ++ package = salt.payload.loads(package) ++ package = self.pack_publish(package) + log.debug("Publish daemon received payload. size=%d", len(package)) + + unpacked_package = salt.payload.unpackage(package) +@@ -986,8 +1033,8 @@ class ZeroMQPubServerChannel(salt.transport.server.PubServerChannel): + """ + if self.pub_sock: + self.pub_close() +- ctx = zmq.Context.instance() +- self._sock_data.sock = ctx.socket(zmq.PUSH) ++ self._sock_data._ctx = zmq.Context() ++ self._sock_data.sock = self._sock_data._ctx.socket(zmq.PUSH) + self.pub_sock.setsockopt(zmq.LINGER, -1) + if self.opts.get("ipc_mode", "") == "tcp": + pull_uri = "tcp://127.0.0.1:{}".format( +@@ -1009,15 +1056,12 @@ class ZeroMQPubServerChannel(salt.transport.server.PubServerChannel): + if hasattr(self._sock_data, "sock"): + self._sock_data.sock.close() + delattr(self._sock_data, "sock") ++ if hasattr(self._sock_data, "_ctx"): ++ self._sock_data._ctx.destroy() + +- def publish(self, load): +- """ +- Publish "load" to minions. This send the load to the publisher daemon +- process with does the actual sending to minions. +- +- :param dict load: A load to be sent across the wire to minions +- """ ++ def pack_publish(self, load): + payload = {"enc": "aes"} ++ load["serial"] = salt.master.SMaster.get_serial() + crypticle = salt.crypt.Crypticle( + self.opts, salt.master.SMaster.secrets["aes"]["secret"].value + ) +@@ -1048,9 +1092,18 @@ class ZeroMQPubServerChannel(salt.transport.server.PubServerChannel): + load.get("jid", None), + len(payload), + ) ++ return payload ++ ++ def publish(self, load): ++ """ ++ Publish "load" to minions. This send the load to the publisher daemon ++ process with does the actual sending to minions. ++ ++ :param dict load: A load to be sent across the wire to minions ++ """ + if not self.pub_sock: + self.pub_connect() +- self.pub_sock.send(payload) ++ self.pub_sock.send(salt.payload.dumps(load)) + log.debug("Sent payload to publish daemon.") + + +diff --git a/salt/utils/minions.py b/salt/utils/minions.py +index a639bbb513..3e2f448db6 100644 +--- a/salt/utils/minions.py ++++ b/salt/utils/minions.py +@@ -736,20 +736,27 @@ class CkMinions: + + def validate_tgt(self, valid, expr, tgt_type, minions=None, expr_form=None): + """ +- Return a Bool. This function returns if the expression sent in is +- within the scope of the valid expression ++ Validate the target minions against the possible valid minions. ++ ++ If ``minions`` is provided, they will be compared against the valid ++ minions. Otherwise, ``expr`` and ``tgt_type`` will be used to expand ++ to a list of target minions. ++ ++ Return True if all of the requested minions are valid minions, ++ otherwise return False. + """ + + v_minions = set(self.check_minions(valid, "compound").get("minions", [])) ++ if not v_minions: ++ # There are no valid minions, so it doesn't matter what we are ++ # targeting - this is a fail. ++ return False + if minions is None: + _res = self.check_minions(expr, tgt_type) + minions = set(_res["minions"]) + else: + minions = set(minions) +- d_bool = not bool(minions.difference(v_minions)) +- if len(v_minions) == len(minions) and d_bool: +- return True +- return d_bool ++ return minions.issubset(v_minions) + + def match_check(self, regex, fun): + """ +diff --git a/salt/utils/network.py b/salt/utils/network.py +index 349cfb6fce..90be389a59 100644 +--- a/salt/utils/network.py ++++ b/salt/utils/network.py +@@ -1003,10 +1003,10 @@ def _junos_interfaces_ifconfig(out): + + pip = re.compile( + r".*?inet\s*(primary)*\s+mtu" +- r" (\d+)\s+local=[^\d]*(.*?)\s+dest=[^\d]*(.*?)\/([\d]*)\s+bcast=((?:[0-9]{1,3}\.){3}[0-9]{1,3})" ++ r" (\d+)\s+local=[^\d]*(.*?)\s{0,40}dest=[^\d]*(.*?)\/([\d]*)\s{0,40}bcast=((?:[0-9]{1,3}\.){3}[0-9]{1,3})" + ) + pip6 = re.compile( +- r".*?inet6 mtu [^\d]+\s+local=([0-9a-f:]+)%([a-zA-Z0-9]*)/([\d]*)\s" ++ r".*?inet6 mtu [^\d]+\s{0,40}local=([0-9a-f:]+)%([a-zA-Z0-9]*)/([\d]*)\s" + ) + + pupdown = re.compile("UP") +diff --git a/tests/integration/files/ssh/known_hosts b/tests/integration/files/ssh/known_hosts +index b46ae35a6b..aa02480ca8 100644 +--- a/tests/integration/files/ssh/known_hosts ++++ b/tests/integration/files/ssh/known_hosts +@@ -1 +1,3 @@ + |1|muzcBqgq7+ByUY7aLICytOff8UI=|rZ1JBNlIOqRnwwsJl9yP+xMxgf8= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ== ++github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg= ++github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl +diff --git a/tests/integration/modules/test_ssh.py b/tests/integration/modules/test_ssh.py +index 4bae9c1019..ffa052402e 100644 +--- a/tests/integration/modules/test_ssh.py ++++ b/tests/integration/modules/test_ssh.py +@@ -132,7 +132,9 @@ class SSHModuleTest(ModuleCase): + """ + Check that known host information is returned from remote host + """ +- ret = self.run_function("ssh.recv_known_host_entries", ["github.com"]) ++ ret = self.run_function( ++ "ssh.recv_known_host_entries", ["github.com"], enc="ssh-rsa" ++ ) + try: + self.assertNotEqual(ret, None) + self.assertEqual(ret[0]["enc"], "ssh-rsa") +@@ -219,7 +221,10 @@ class SSHModuleTest(ModuleCase): + """ + # add item + ret = self.run_function( +- "ssh.set_known_host", ["root", "github.com"], config=self.known_hosts ++ "ssh.set_known_host", ++ ["root", "github.com"], ++ enc="ssh-rsa", ++ config=self.known_hosts, + ) + try: + self.assertEqual(ret["status"], "updated") +diff --git a/tests/integration/states/test_ssh_known_hosts.py b/tests/integration/states/test_ssh_known_hosts.py +index beeb0342bd..cb4b40d3a0 100644 +--- a/tests/integration/states/test_ssh_known_hosts.py ++++ b/tests/integration/states/test_ssh_known_hosts.py +@@ -11,7 +11,7 @@ from tests.support.mixins import SaltReturnAssertsMixin + from tests.support.runtests import RUNTIME_VARS + + GITHUB_FINGERPRINT = "9d:38:5b:83:a9:17:52:92:56:1a:5e:c4:d4:81:8e:0a:ca:51:a2:64:f1:74:20:11:2e:f8:8a:c3:a1:39:49:8f" +-GITHUB_IP = "192.30.253.113" ++GITHUB_IP = "140.82.121.4" + + + @pytest.mark.skip_if_binaries_missing("ssh", "ssh-keygen", check_all=True) +@@ -37,6 +37,7 @@ class SSHKnownHostsStateTest(ModuleCase, SaltReturnAssertsMixin): + kwargs = { + "name": "github.com", + "user": "root", ++ "enc": "ssh-rsa", + "fingerprint": GITHUB_FINGERPRINT, + "config": self.known_hosts, + } +diff --git a/tests/pytests/functional/transport/server/test_req_channel.py b/tests/pytests/functional/transport/server/test_req_channel.py +index 7a392cd758..17d8861ccf 100644 +--- a/tests/pytests/functional/transport/server/test_req_channel.py ++++ b/tests/pytests/functional/transport/server/test_req_channel.py +@@ -1,3 +1,4 @@ ++import ctypes + import logging + import multiprocessing + +@@ -6,6 +7,7 @@ import salt.config + import salt.exceptions + import salt.ext.tornado.gen + import salt.log.setup ++import salt.master + import salt.transport.client + import salt.transport.server + import salt.utils.platform +@@ -33,6 +35,18 @@ class ReqServerChannelProcess(salt.utils.process.SignalHandlingProcess): + self.running = multiprocessing.Event() + + def run(self): ++ salt.master.SMaster.secrets["aes"] = { ++ "secret": multiprocessing.Array( ++ ctypes.c_char, ++ salt.utils.stringutils.to_bytes( ++ salt.crypt.Crypticle.generate_key_string() ++ ), ++ ), ++ "serial": multiprocessing.Value( ++ ctypes.c_longlong, lock=False # We'll use the lock from 'secret' ++ ), ++ } ++ + self.io_loop = salt.ext.tornado.ioloop.IOLoop() + self.io_loop.make_current() + self.req_server_channel.post_fork(self._handle_payload, io_loop=self.io_loop) +@@ -121,7 +135,7 @@ def test_basic(req_channel): + {"baz": "qux", "list": [1, 2, 3]}, + ] + for msg in msgs: +- ret = req_channel.send(msg, timeout=5, tries=1) ++ ret = req_channel.send(dict(msg), timeout=5, tries=1) + assert ret["load"] == msg + + +diff --git a/tests/pytests/functional/transport/zeromq/test_pub_server_channel.py b/tests/pytests/functional/transport/zeromq/test_pub_server_channel.py +index 9e183c11e0..e7033f810a 100644 +--- a/tests/pytests/functional/transport/zeromq/test_pub_server_channel.py ++++ b/tests/pytests/functional/transport/zeromq/test_pub_server_channel.py +@@ -10,6 +10,7 @@ import salt.exceptions + import salt.ext.tornado.gen + import salt.ext.tornado.ioloop + import salt.log.setup ++import salt.master + import salt.transport.client + import salt.transport.server + import salt.transport.zeromq +@@ -40,6 +41,21 @@ class Collector(salt.utils.process.SignalHandlingProcess): + self.started = multiprocessing.Event() + self.running = multiprocessing.Event() + ++ def _rotate_secrets(self, now=None): ++ salt.master.SMaster.secrets["aes"] = { ++ "secret": multiprocessing.Array( ++ ctypes.c_char, ++ salt.utils.stringutils.to_bytes( ++ salt.crypt.Crypticle.generate_key_string() ++ ), ++ ), ++ "serial": multiprocessing.Value( ++ ctypes.c_longlong, lock=False # We'll use the lock from 'secret' ++ ), ++ "reload": salt.crypt.Crypticle.generate_key_string, ++ "rotate_master_key": self._rotate_secrets, ++ } ++ + def run(self): + """ + Gather results until then number of seconds specified by timeout passes +@@ -67,6 +83,8 @@ class Collector(salt.utils.process.SignalHandlingProcess): + try: + serial_payload = salt.payload.loads(payload) + payload = crypticle.loads(serial_payload["load"]) ++ if not payload: ++ continue + if "start" in payload: + self.running.set() + continue +@@ -108,10 +126,16 @@ class PubServerChannelProcess(salt.utils.process.SignalHandlingProcess): + self.master_config = master_config + self.minion_config = minion_config + self.collector_kwargs = collector_kwargs +- self.aes_key = multiprocessing.Array( +- ctypes.c_char, +- salt.utils.stringutils.to_bytes(salt.crypt.Crypticle.generate_key_string()), +- ) ++ self.aes_key = salt.crypt.Crypticle.generate_key_string() ++ salt.master.SMaster.secrets["aes"] = { ++ "secret": multiprocessing.Array( ++ ctypes.c_char, ++ salt.utils.stringutils.to_bytes(self.aes_key), ++ ), ++ "serial": multiprocessing.Value( ++ ctypes.c_longlong, lock=False # We'll use the lock from 'secret' ++ ), ++ } + self.process_manager = salt.utils.process.ProcessManager( + name="ZMQ-PubServer-ProcessManager" + ) +@@ -126,14 +150,10 @@ class PubServerChannelProcess(salt.utils.process.SignalHandlingProcess): + self.queue = multiprocessing.Queue() + self.stopped = multiprocessing.Event() + self.collector = Collector( +- self.minion_config, +- self.pub_uri, +- self.aes_key.value, +- **self.collector_kwargs ++ self.minion_config, self.pub_uri, self.aes_key, **self.collector_kwargs + ) + + def run(self): +- salt.master.SMaster.secrets["aes"] = {"secret": self.aes_key} + try: + while True: + payload = self.queue.get() +@@ -227,12 +247,16 @@ def test_issue_36469_tcp(salt_master, salt_minion): + https://github.com/saltstack/salt/issues/36469 + """ + +- def _send_small(server_channel, sid, num=10): ++ def _send_small(opts, sid, num=10): ++ server_channel = salt.transport.zeromq.ZeroMQPubServerChannel(opts) + for idx in range(num): + load = {"tgt_type": "glob", "tgt": "*", "jid": "{}-s{}".format(sid, idx)} + server_channel.publish(load) ++ time.sleep(0.3) ++ server_channel.close_pub() + +- def _send_large(server_channel, sid, num=10, size=250000 * 3): ++ def _send_large(opts, sid, num=10, size=250000 * 3): ++ server_channel = salt.transport.zeromq.ZeroMQPubServerChannel(opts) + for idx in range(num): + load = { + "tgt_type": "glob", +@@ -241,16 +265,19 @@ def test_issue_36469_tcp(salt_master, salt_minion): + "xdata": "0" * size, + } + server_channel.publish(load) ++ time.sleep(0.3) ++ server_channel.close_pub() + + opts = dict(salt_master.config.copy(), ipc_mode="tcp", pub_hwm=0) + send_num = 10 * 4 + expect = [] + with PubServerChannelProcess(opts, salt_minion.config.copy()) as server_channel: ++ assert "aes" in salt.master.SMaster.secrets + with ThreadPoolExecutor(max_workers=4) as executor: +- executor.submit(_send_small, server_channel, 1) +- executor.submit(_send_large, server_channel, 2) +- executor.submit(_send_small, server_channel, 3) +- executor.submit(_send_large, server_channel, 4) ++ executor.submit(_send_small, opts, 1) ++ executor.submit(_send_large, opts, 2) ++ executor.submit(_send_small, opts, 3) ++ executor.submit(_send_large, opts, 4) + expect.extend(["{}-s{}".format(a, b) for a in range(10) for b in (1, 3)]) + expect.extend(["{}-l{}".format(a, b) for a in range(10) for b in (2, 4)]) + results = server_channel.collector.results +diff --git a/tests/pytests/unit/test_crypt.py b/tests/pytests/unit/test_crypt.py +index aa8f439b8c..a40c34b9d5 100644 +--- a/tests/pytests/unit/test_crypt.py ++++ b/tests/pytests/unit/test_crypt.py +@@ -4,10 +4,100 @@ tests.pytests.unit.test_crypt + + Unit tests for salt's crypt module + """ ++ ++import uuid ++ + import pytest + import salt.crypt ++import salt.master + import salt.utils.files + ++PRIV_KEY = """ ++-----BEGIN RSA PRIVATE KEY----- ++MIIEogIBAAKCAQEAoAsMPt+4kuIG6vKyw9r3+OuZrVBee/2vDdVetW+Js5dTlgrJ ++aghWWn3doGmKlEjqh7E4UTa+t2Jd6w8RSLnyHNJ/HpVhMG0M07MF6FMfILtDrrt8 ++ZX7eDVt8sx5gCEpYI+XG8Y07Ga9i3Hiczt+fu6HYwu96HggmG2pqkOrn3iGfqBvV ++YVFJzSZYe7e4c1PeEs0xYcrA4k+apyGsMtpef8vRUrNicRLc7dAcvfhtgt2DXEZ2 ++d72t/CR4ygtUvPXzisaTPW0G7OWAheCloqvTIIPQIjR8htFxGTz02STVXfnhnJ0Z ++k8KhqKF2v1SQvIYxsZU7jaDgl5i3zpeh58cYOwIDAQABAoIBABZUJEO7Y91+UnfC ++H6XKrZEZkcnH7j6/UIaOD9YhdyVKxhsnax1zh1S9vceNIgv5NltzIsfV6vrb6v2K ++Dx/F7Z0O0zR5o+MlO8ZncjoNKskex10gBEWG00Uqz/WPlddiQ/TSMJTv3uCBAzp+ ++S2Zjdb4wYPUlgzSgb2ygxrhsRahMcSMG9PoX6klxMXFKMD1JxiY8QfAHahPzQXy9 ++F7COZ0fCVo6BE+MqNuQ8tZeIxu8mOULQCCkLFwXmkz1FpfK/kNRmhIyhxwvCS+z4 ++JuErW3uXfE64RLERiLp1bSxlDdpvRO2R41HAoNELTsKXJOEt4JANRHm/CeyA5wsh ++NpscufUCgYEAxhgPfcMDy2v3nL6KtkgYjdcOyRvsAF50QRbEa8ldO+87IoMDD/Oe ++osFERJ5hhyyEO78QnaLVegnykiw5DWEF02RKMhD/4XU+1UYVhY0wJjKQIBadsufB ++2dnaKjvwzUhPh5BrBqNHl/FXwNCRDiYqXa79eWCPC9OFbZcUWWq70s8CgYEAztOI ++61zRfmXJ7f70GgYbHg+GA7IrsAcsGRITsFR82Ho0lqdFFCxz7oK8QfL6bwMCGKyk ++nzk+twh6hhj5UNp18KN8wktlo02zTgzgemHwaLa2cd6xKgmAyuPiTgcgnzt5LVNG ++FOjIWkLwSlpkDTl7ZzY2QSy7t+mq5d750fpIrtUCgYBWXZUbcpPL88WgDB7z/Bjg ++dlvW6JqLSqMK4b8/cyp4AARbNp12LfQC55o5BIhm48y/M70tzRmfvIiKnEc/gwaE ++NJx4mZrGFFURrR2i/Xx5mt/lbZbRsmN89JM+iKWjCpzJ8PgIi9Wh9DIbOZOUhKVB ++9RJEAgo70LvCnPTdS0CaVwKBgDJW3BllAvw/rBFIH4OB/vGnF5gosmdqp3oGo1Ik ++jipmPAx6895AH4tquIVYrUl9svHsezjhxvjnkGK5C115foEuWXw0u60uiTiy+6Pt ++2IS0C93VNMulenpnUrppE7CN2iWFAiaura0CY9fE/lsVpYpucHAWgi32Kok+ZxGL ++WEttAoGAN9Ehsz4LeQxEj3x8wVeEMHF6OsznpwYsI2oVh6VxpS4AjgKYqeLVcnNi ++TlZFsuQcqgod8OgzA91tdB+Rp86NygmWD5WzeKXpCOg9uA+y/YL+0sgZZHsuvbK6 ++PllUgXdYxqClk/hdBFB7v9AQoaj7K9Ga22v32msftYDQRJ94xOI= ++-----END RSA PRIVATE KEY----- ++""" ++ ++ ++PUB_KEY = """ ++-----BEGIN PUBLIC KEY----- ++MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoAsMPt+4kuIG6vKyw9r3 +++OuZrVBee/2vDdVetW+Js5dTlgrJaghWWn3doGmKlEjqh7E4UTa+t2Jd6w8RSLny ++HNJ/HpVhMG0M07MF6FMfILtDrrt8ZX7eDVt8sx5gCEpYI+XG8Y07Ga9i3Hiczt+f ++u6HYwu96HggmG2pqkOrn3iGfqBvVYVFJzSZYe7e4c1PeEs0xYcrA4k+apyGsMtpe ++f8vRUrNicRLc7dAcvfhtgt2DXEZ2d72t/CR4ygtUvPXzisaTPW0G7OWAheCloqvT ++IIPQIjR8htFxGTz02STVXfnhnJ0Zk8KhqKF2v1SQvIYxsZU7jaDgl5i3zpeh58cY ++OwIDAQAB ++-----END PUBLIC KEY----- ++""" ++ ++PRIV_KEY2 = """ ++-----BEGIN RSA PRIVATE KEY----- ++MIIEogIBAAKCAQEAp+8cTxguO6Vg+YO92VfHgNld3Zy8aM3JbZvpJcjTnis+YFJ7 ++Zlkcc647yPRRwY9nYBNywahnt5kIeuT1rTvTsMBZWvmUoEVUj1Xg8XXQkBvb9Ozy ++Gqy/G/p8KDDpzMP/U+XCnUeHiXTZrgnqgBIc2cKeCVvWFqDi0GRFGzyaXLaX3PPm ++M7DJ0MIPL1qgmcDq6+7Ze0gJ9SrDYFAeLmbuT1OqDfufXWQl/82JXeiwU2cOpqWq ++7n5fvPOWim7l1tzQ+dSiMRRm0xa6uNexCJww3oJSwvMbAmgzvOhqqhlqv+K7u0u7 ++FrFFojESsL36Gq4GBrISnvu2tk7u4GGNTYYQbQIDAQABAoIBAADrqWDQnd5DVZEA ++lR+WINiWuHJAy/KaIC7K4kAMBgbxrz2ZbiY9Ok/zBk5fcnxIZDVtXd1sZicmPlro ++GuWodIxdPZAnWpZ3UtOXUayZK/vCP1YsH1agmEqXuKsCu6Fc+K8VzReOHxLUkmXn ++FYM+tixGahXcjEOi/aNNTWitEB6OemRM1UeLJFzRcfyXiqzHpHCIZwBpTUAsmzcG ++QiVDkMTKubwo/m+PVXburX2CGibUydctgbrYIc7EJvyx/cpRiPZXo1PhHQWdu4Y1 ++SOaC66WLsP/wqvtHo58JQ6EN/gjSsbAgGGVkZ1xMo66nR+pLpR27coS7o03xCks6 ++DY/0mukCgYEAuLIGgBnqoh7YsOBLd/Bc1UTfDMxJhNseo+hZemtkSXz2Jn51322F ++Zw/FVN4ArXgluH+XsOhvG/MFFpojwZSrb0Qq5b1MRdo9qycq8lGqNtlN1WHqosDQ ++zW29kpL0tlRrSDpww3wRESsN9rH5XIrJ1b3ZXuO7asR+KBVQMy/+NcUCgYEA6MSC ++c+fywltKPgmPl5j0DPoDe5SXE/6JQy7w/vVGrGfWGf/zEJmhzS2R+CcfTTEqaT0T ++Yw8+XbFgKAqsxwtE9MUXLTVLI3sSUyE4g7blCYscOqhZ8ItCUKDXWkSpt++rG0Um ++1+cEJP/0oCazG6MWqvBC4NpQ1nzh46QpjWqMwokCgYAKDLXJ1p8rvx3vUeUJW6zR ++dfPlEGCXuAyMwqHLxXgpf4EtSwhC5gSyPOtx2LqUtcrnpRmt6JfTH4ARYMW9TMef ++QEhNQ+WYj213mKP/l235mg1gJPnNbUxvQR9lkFV8bk+AGJ32JRQQqRUTbU+yN2MQ ++HEptnVqfTp3GtJIultfwOQKBgG+RyYmu8wBP650izg33BXu21raEeYne5oIqXN+I ++R5DZ0JjzwtkBGroTDrVoYyuH1nFNEh7YLqeQHqvyufBKKYo9cid8NQDTu+vWr5UK ++tGvHnwdKrJmM1oN5JOAiq0r7+QMAOWchVy449VNSWWV03aeftB685iR5BXkstbIQ ++EVopAoGAfcGBTAhmceK/4Q83H/FXBWy0PAa1kZGg/q8+Z0KY76AqyxOVl0/CU/rB ++3tO3sKhaMTHPME/MiQjQQGoaK1JgPY6JHYvly2KomrJ8QTugqNGyMzdVJkXAK2AM ++GAwC8ivAkHf8CHrHa1W7l8t2IqBjW1aRt7mOW92nfG88Hck0Mbo= ++-----END RSA PRIVATE KEY----- ++""" ++ ++ ++PUB_KEY2 = """ ++-----BEGIN PUBLIC KEY----- ++MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp+8cTxguO6Vg+YO92VfH ++gNld3Zy8aM3JbZvpJcjTnis+YFJ7Zlkcc647yPRRwY9nYBNywahnt5kIeuT1rTvT ++sMBZWvmUoEVUj1Xg8XXQkBvb9OzyGqy/G/p8KDDpzMP/U+XCnUeHiXTZrgnqgBIc ++2cKeCVvWFqDi0GRFGzyaXLaX3PPmM7DJ0MIPL1qgmcDq6+7Ze0gJ9SrDYFAeLmbu ++T1OqDfufXWQl/82JXeiwU2cOpqWq7n5fvPOWim7l1tzQ+dSiMRRm0xa6uNexCJww ++3oJSwvMbAmgzvOhqqhlqv+K7u0u7FrFFojESsL36Gq4GBrISnvu2tk7u4GGNTYYQ ++bQIDAQAB ++-----END PUBLIC KEY----- ++""" ++ + + def test_get_rsa_pub_key_bad_key(tmp_path): + """ +@@ -18,3 +108,64 @@ def test_get_rsa_pub_key_bad_key(tmp_path): + fp.write("") + with pytest.raises(salt.crypt.InvalidKeyError): + salt.crypt.get_rsa_pub_key(key_path) ++ ++ ++def test_cryptical_dumps_no_nonce(): ++ master_crypt = salt.crypt.Crypticle({}, salt.crypt.Crypticle.generate_key_string()) ++ data = {"foo": "bar"} ++ ret = master_crypt.dumps(data) ++ ++ # Validate message structure ++ assert isinstance(ret, bytes) ++ une = master_crypt.decrypt(ret) ++ une.startswith(master_crypt.PICKLE_PAD) ++ assert salt.payload.loads(une[len(master_crypt.PICKLE_PAD) :]) == data ++ ++ # Validate load back to orig data ++ assert master_crypt.loads(ret) == data ++ ++ ++def test_cryptical_dumps_valid_nonce(): ++ nonce = uuid.uuid4().hex ++ master_crypt = salt.crypt.Crypticle({}, salt.crypt.Crypticle.generate_key_string()) ++ data = {"foo": "bar"} ++ ret = master_crypt.dumps(data, nonce=nonce) ++ ++ assert isinstance(ret, bytes) ++ une = master_crypt.decrypt(ret) ++ une.startswith(master_crypt.PICKLE_PAD) ++ nonce_and_data = une[len(master_crypt.PICKLE_PAD) :] ++ assert nonce_and_data.startswith(nonce.encode()) ++ assert salt.payload.loads(nonce_and_data[len(nonce) :]) == data ++ ++ assert master_crypt.loads(ret, nonce=nonce) == data ++ ++ ++def test_cryptical_dumps_invalid_nonce(): ++ nonce = uuid.uuid4().hex ++ master_crypt = salt.crypt.Crypticle({}, salt.crypt.Crypticle.generate_key_string()) ++ data = {"foo": "bar"} ++ ret = master_crypt.dumps(data, nonce=nonce) ++ assert isinstance(ret, bytes) ++ with pytest.raises(salt.crypt.SaltClientError, match="Nonce verification error"): ++ assert master_crypt.loads(ret, nonce="abcde") ++ ++ ++def test_verify_signature(tmpdir): ++ tmpdir.join("foo.pem").write(PRIV_KEY.strip()) ++ tmpdir.join("foo.pub").write(PUB_KEY.strip()) ++ tmpdir.join("bar.pem").write(PRIV_KEY2.strip()) ++ tmpdir.join("bar.pub").write(PUB_KEY2.strip()) ++ msg = b"foo bar" ++ sig = salt.crypt.sign_message(str(tmpdir.join("foo.pem")), msg) ++ assert salt.crypt.verify_signature(str(tmpdir.join("foo.pub")), msg, sig) ++ ++ ++def test_verify_signature_bad_sig(tmpdir): ++ tmpdir.join("foo.pem").write(PRIV_KEY.strip()) ++ tmpdir.join("foo.pub").write(PUB_KEY.strip()) ++ tmpdir.join("bar.pem").write(PRIV_KEY2.strip()) ++ tmpdir.join("bar.pub").write(PUB_KEY2.strip()) ++ msg = b"foo bar" ++ sig = salt.crypt.sign_message(str(tmpdir.join("foo.pem")), msg) ++ assert not salt.crypt.verify_signature(str(tmpdir.join("bar.pub")), msg, sig) +diff --git a/tests/pytests/unit/test_minion.py b/tests/pytests/unit/test_minion.py +index 7de60c49e3..985ec99276 100644 +--- a/tests/pytests/unit/test_minion.py ++++ b/tests/pytests/unit/test_minion.py +@@ -10,6 +10,7 @@ import salt.minion + import salt.syspaths + import salt.utils.crypt + import salt.utils.event as event ++import salt.utils.jid + import salt.utils.platform + import salt.utils.process + from salt._compat import ipaddress +diff --git a/tests/pytests/unit/transport/test_tcp.py b/tests/pytests/unit/transport/test_tcp.py +index d003797d29..3b6e175472 100644 +--- a/tests/pytests/unit/transport/test_tcp.py ++++ b/tests/pytests/unit/transport/test_tcp.py +@@ -210,15 +210,17 @@ def test_tcp_pub_server_channel_publish_filtering(temp_salt_master): + SyncWrapper.return_value = wrap + + # try simple publish with glob tgt_type +- channel.publish({"test": "value", "tgt_type": "glob", "tgt": "*"}) +- payload = wrap.send.call_args[0][0] ++ payload = channel.pack_publish( ++ {"test": "value", "tgt_type": "glob", "tgt": "*"} ++ ) + + # verify we send it without any specific topic + assert "topic_lst" not in payload + + # try simple publish with list tgt_type +- channel.publish({"test": "value", "tgt_type": "list", "tgt": ["minion01"]}) +- payload = wrap.send.call_args[0][0] ++ payload = channel.pack_publish( ++ {"test": "value", "tgt_type": "list", "tgt": ["minion01"]} ++ ) + + # verify we send it with correct topic + assert "topic_lst" in payload +@@ -226,8 +228,9 @@ def test_tcp_pub_server_channel_publish_filtering(temp_salt_master): + + # try with syndic settings + opts["order_masters"] = True +- channel.publish({"test": "value", "tgt_type": "list", "tgt": ["minion01"]}) +- payload = wrap.send.call_args[0][0] ++ payload = channel.pack_publish( ++ {"test": "value", "tgt_type": "list", "tgt": ["minion01"]} ++ ) + + # verify we send it without topic for syndics + assert "topic_lst" not in payload +@@ -257,8 +260,9 @@ def test_tcp_pub_server_channel_publish_filtering_str_list(temp_salt_master): + check_minions.return_value = {"minions": ["minion02"]} + + # try simple publish with list tgt_type +- channel.publish({"test": "value", "tgt_type": "list", "tgt": "minion02"}) +- payload = wrap.send.call_args[0][0] ++ payload = channel.pack_publish( ++ {"test": "value", "tgt_type": "list", "tgt": "minion02"} ++ ) + + # verify we send it with correct topic + assert "topic_lst" in payload +diff --git a/tests/pytests/unit/transport/test_zeromq.py b/tests/pytests/unit/transport/test_zeromq.py +index 44f38ee998..1f0515c91a 100644 +--- a/tests/pytests/unit/transport/test_zeromq.py ++++ b/tests/pytests/unit/transport/test_zeromq.py +@@ -2,9 +2,16 @@ + :codeauthor: Thomas Jackson + """ + ++import ctypes + import hashlib ++import logging ++import multiprocessing ++import os ++import uuid + ++import pytest + import salt.config ++import salt.crypt + import salt.exceptions + import salt.ext.tornado.gen + import salt.ext.tornado.ioloop +@@ -14,9 +21,236 @@ import salt.transport.server + import salt.utils.platform + import salt.utils.process + import salt.utils.stringutils ++from salt.master import SMaster + from salt.transport.zeromq import AsyncReqMessageClientPool + from tests.support.mock import MagicMock, patch + ++try: ++ from M2Crypto import RSA ++ ++ HAS_M2 = True ++except ImportError: ++ HAS_M2 = False ++ try: ++ from Cryptodome.Cipher import PKCS1_OAEP ++ except ImportError: ++ from Crypto.Cipher import PKCS1_OAEP # nosec ++ ++log = logging.getLogger(__name__) ++ ++MASTER_PRIV_KEY = """ ++-----BEGIN RSA PRIVATE KEY----- ++MIIEogIBAAKCAQEAoAsMPt+4kuIG6vKyw9r3+OuZrVBee/2vDdVetW+Js5dTlgrJ ++aghWWn3doGmKlEjqh7E4UTa+t2Jd6w8RSLnyHNJ/HpVhMG0M07MF6FMfILtDrrt8 ++ZX7eDVt8sx5gCEpYI+XG8Y07Ga9i3Hiczt+fu6HYwu96HggmG2pqkOrn3iGfqBvV ++YVFJzSZYe7e4c1PeEs0xYcrA4k+apyGsMtpef8vRUrNicRLc7dAcvfhtgt2DXEZ2 ++d72t/CR4ygtUvPXzisaTPW0G7OWAheCloqvTIIPQIjR8htFxGTz02STVXfnhnJ0Z ++k8KhqKF2v1SQvIYxsZU7jaDgl5i3zpeh58cYOwIDAQABAoIBABZUJEO7Y91+UnfC ++H6XKrZEZkcnH7j6/UIaOD9YhdyVKxhsnax1zh1S9vceNIgv5NltzIsfV6vrb6v2K ++Dx/F7Z0O0zR5o+MlO8ZncjoNKskex10gBEWG00Uqz/WPlddiQ/TSMJTv3uCBAzp+ ++S2Zjdb4wYPUlgzSgb2ygxrhsRahMcSMG9PoX6klxMXFKMD1JxiY8QfAHahPzQXy9 ++F7COZ0fCVo6BE+MqNuQ8tZeIxu8mOULQCCkLFwXmkz1FpfK/kNRmhIyhxwvCS+z4 ++JuErW3uXfE64RLERiLp1bSxlDdpvRO2R41HAoNELTsKXJOEt4JANRHm/CeyA5wsh ++NpscufUCgYEAxhgPfcMDy2v3nL6KtkgYjdcOyRvsAF50QRbEa8ldO+87IoMDD/Oe ++osFERJ5hhyyEO78QnaLVegnykiw5DWEF02RKMhD/4XU+1UYVhY0wJjKQIBadsufB ++2dnaKjvwzUhPh5BrBqNHl/FXwNCRDiYqXa79eWCPC9OFbZcUWWq70s8CgYEAztOI ++61zRfmXJ7f70GgYbHg+GA7IrsAcsGRITsFR82Ho0lqdFFCxz7oK8QfL6bwMCGKyk ++nzk+twh6hhj5UNp18KN8wktlo02zTgzgemHwaLa2cd6xKgmAyuPiTgcgnzt5LVNG ++FOjIWkLwSlpkDTl7ZzY2QSy7t+mq5d750fpIrtUCgYBWXZUbcpPL88WgDB7z/Bjg ++dlvW6JqLSqMK4b8/cyp4AARbNp12LfQC55o5BIhm48y/M70tzRmfvIiKnEc/gwaE ++NJx4mZrGFFURrR2i/Xx5mt/lbZbRsmN89JM+iKWjCpzJ8PgIi9Wh9DIbOZOUhKVB ++9RJEAgo70LvCnPTdS0CaVwKBgDJW3BllAvw/rBFIH4OB/vGnF5gosmdqp3oGo1Ik ++jipmPAx6895AH4tquIVYrUl9svHsezjhxvjnkGK5C115foEuWXw0u60uiTiy+6Pt ++2IS0C93VNMulenpnUrppE7CN2iWFAiaura0CY9fE/lsVpYpucHAWgi32Kok+ZxGL ++WEttAoGAN9Ehsz4LeQxEj3x8wVeEMHF6OsznpwYsI2oVh6VxpS4AjgKYqeLVcnNi ++TlZFsuQcqgod8OgzA91tdB+Rp86NygmWD5WzeKXpCOg9uA+y/YL+0sgZZHsuvbK6 ++PllUgXdYxqClk/hdBFB7v9AQoaj7K9Ga22v32msftYDQRJ94xOI= ++-----END RSA PRIVATE KEY----- ++""" ++ ++ ++MASTER_PUB_KEY = """ ++-----BEGIN PUBLIC KEY----- ++MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoAsMPt+4kuIG6vKyw9r3 +++OuZrVBee/2vDdVetW+Js5dTlgrJaghWWn3doGmKlEjqh7E4UTa+t2Jd6w8RSLny ++HNJ/HpVhMG0M07MF6FMfILtDrrt8ZX7eDVt8sx5gCEpYI+XG8Y07Ga9i3Hiczt+f ++u6HYwu96HggmG2pqkOrn3iGfqBvVYVFJzSZYe7e4c1PeEs0xYcrA4k+apyGsMtpe ++f8vRUrNicRLc7dAcvfhtgt2DXEZ2d72t/CR4ygtUvPXzisaTPW0G7OWAheCloqvT ++IIPQIjR8htFxGTz02STVXfnhnJ0Zk8KhqKF2v1SQvIYxsZU7jaDgl5i3zpeh58cY ++OwIDAQAB ++-----END PUBLIC KEY----- ++""" ++ ++MASTER2_PRIV_KEY = """ ++-----BEGIN RSA PRIVATE KEY----- ++MIIEogIBAAKCAQEAp+8cTxguO6Vg+YO92VfHgNld3Zy8aM3JbZvpJcjTnis+YFJ7 ++Zlkcc647yPRRwY9nYBNywahnt5kIeuT1rTvTsMBZWvmUoEVUj1Xg8XXQkBvb9Ozy ++Gqy/G/p8KDDpzMP/U+XCnUeHiXTZrgnqgBIc2cKeCVvWFqDi0GRFGzyaXLaX3PPm ++M7DJ0MIPL1qgmcDq6+7Ze0gJ9SrDYFAeLmbuT1OqDfufXWQl/82JXeiwU2cOpqWq ++7n5fvPOWim7l1tzQ+dSiMRRm0xa6uNexCJww3oJSwvMbAmgzvOhqqhlqv+K7u0u7 ++FrFFojESsL36Gq4GBrISnvu2tk7u4GGNTYYQbQIDAQABAoIBAADrqWDQnd5DVZEA ++lR+WINiWuHJAy/KaIC7K4kAMBgbxrz2ZbiY9Ok/zBk5fcnxIZDVtXd1sZicmPlro ++GuWodIxdPZAnWpZ3UtOXUayZK/vCP1YsH1agmEqXuKsCu6Fc+K8VzReOHxLUkmXn ++FYM+tixGahXcjEOi/aNNTWitEB6OemRM1UeLJFzRcfyXiqzHpHCIZwBpTUAsmzcG ++QiVDkMTKubwo/m+PVXburX2CGibUydctgbrYIc7EJvyx/cpRiPZXo1PhHQWdu4Y1 ++SOaC66WLsP/wqvtHo58JQ6EN/gjSsbAgGGVkZ1xMo66nR+pLpR27coS7o03xCks6 ++DY/0mukCgYEAuLIGgBnqoh7YsOBLd/Bc1UTfDMxJhNseo+hZemtkSXz2Jn51322F ++Zw/FVN4ArXgluH+XsOhvG/MFFpojwZSrb0Qq5b1MRdo9qycq8lGqNtlN1WHqosDQ ++zW29kpL0tlRrSDpww3wRESsN9rH5XIrJ1b3ZXuO7asR+KBVQMy/+NcUCgYEA6MSC ++c+fywltKPgmPl5j0DPoDe5SXE/6JQy7w/vVGrGfWGf/zEJmhzS2R+CcfTTEqaT0T ++Yw8+XbFgKAqsxwtE9MUXLTVLI3sSUyE4g7blCYscOqhZ8ItCUKDXWkSpt++rG0Um ++1+cEJP/0oCazG6MWqvBC4NpQ1nzh46QpjWqMwokCgYAKDLXJ1p8rvx3vUeUJW6zR ++dfPlEGCXuAyMwqHLxXgpf4EtSwhC5gSyPOtx2LqUtcrnpRmt6JfTH4ARYMW9TMef ++QEhNQ+WYj213mKP/l235mg1gJPnNbUxvQR9lkFV8bk+AGJ32JRQQqRUTbU+yN2MQ ++HEptnVqfTp3GtJIultfwOQKBgG+RyYmu8wBP650izg33BXu21raEeYne5oIqXN+I ++R5DZ0JjzwtkBGroTDrVoYyuH1nFNEh7YLqeQHqvyufBKKYo9cid8NQDTu+vWr5UK ++tGvHnwdKrJmM1oN5JOAiq0r7+QMAOWchVy449VNSWWV03aeftB685iR5BXkstbIQ ++EVopAoGAfcGBTAhmceK/4Q83H/FXBWy0PAa1kZGg/q8+Z0KY76AqyxOVl0/CU/rB ++3tO3sKhaMTHPME/MiQjQQGoaK1JgPY6JHYvly2KomrJ8QTugqNGyMzdVJkXAK2AM ++GAwC8ivAkHf8CHrHa1W7l8t2IqBjW1aRt7mOW92nfG88Hck0Mbo= ++-----END RSA PRIVATE KEY----- ++""" ++ ++ ++MASTER2_PUB_KEY = """ ++-----BEGIN PUBLIC KEY----- ++MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp+8cTxguO6Vg+YO92VfH ++gNld3Zy8aM3JbZvpJcjTnis+YFJ7Zlkcc647yPRRwY9nYBNywahnt5kIeuT1rTvT ++sMBZWvmUoEVUj1Xg8XXQkBvb9OzyGqy/G/p8KDDpzMP/U+XCnUeHiXTZrgnqgBIc ++2cKeCVvWFqDi0GRFGzyaXLaX3PPmM7DJ0MIPL1qgmcDq6+7Ze0gJ9SrDYFAeLmbu ++T1OqDfufXWQl/82JXeiwU2cOpqWq7n5fvPOWim7l1tzQ+dSiMRRm0xa6uNexCJww ++3oJSwvMbAmgzvOhqqhlqv+K7u0u7FrFFojESsL36Gq4GBrISnvu2tk7u4GGNTYYQ ++bQIDAQAB ++-----END PUBLIC KEY----- ++""" ++ ++ ++MASTER_SIGNING_PRIV = """ ++-----BEGIN RSA PRIVATE KEY----- ++MIIEpAIBAAKCAQEAtieqrBMTM0MSIbhPKkDcozHqyXKyL/+bXYYw+iVPsns7c7bJ ++zBqenLQlWoRVyrVyBFrrwQSrKu/0Mqn3l639iOGPlUoR3I7aZKIpyEdDkqd3xGIC ++e+BtNNDqhUai67L63hEdG+iYAchi8UZw3LZGtcGpJ3FkBH4cYFX9EOam2QjbD7WY ++EO7m1+j6XEYIOTCmAP9dGAvBbU0Jblc+wYxG3qNr+2dBWsK76QXWEqib2VSOGP+z ++gjJa8tqY7PXXdOJpalQXNphmD/4o4pHKR4Euy0yL/1oMkpacmrV61LWB8Trnx9nS ++9gdVrUteQF/cL1KAGwOsdVmiLpHfvqLLRqSAAQIDAQABAoIBABjB+HEN4Kixf4fk ++wKHKEhL+SF6b/7sFX00NXZ/KLXRhSnnWSMQ8g/1hgMg2P2DfW4FbCDsCUu9xkLvI ++HTZY+CJAIh9U42uaYPWXkt09TmJi76TZ+2Nx4/XvRUjbCm7Fs1I2ekHeUbbAUS5g +++BsPjTnL+h05zLHNoDa5yT0gVGIgFsQcX/w38arZCe8Rjp9le7PXUB5IIqASsDiw ++t8zJvdyWToeXd0WswCHTQu5coHvKo5MCjIZZ1Ink1yJcCCc3rKDc+q3jB2z9T9oW ++cUsKzJ4VuleiYj1eRxFITBmXbjKrb/GPRRUkeqCQbs68Hyj2d3UtOFDPeF4vng/3 ++jGsHPq8CgYEA0AHAbwykVC6NMa37BTvEqcKoxbjTtErxR+yczlmVDfma9vkwtZvx ++FJdbS/+WGA/ucDby5x5b2T5k1J9ueMR86xukb+HnyS0WKsZ94Ie8WnJAcbp+38M6 ++7LD0u74Cgk93oagDAzUHqdLq9cXxv/ppBpxVB1Uvu8DfVMHj+wt6ie8CgYEA4C7u ++u+6b8EmbGqEdtlPpScKG0WFstJEDGXRARDCRiVP2w6wm25v8UssCPvWcwf8U1Hoq ++lhMY+H6a5dnRRiNYql1MGQAsqMi7VeJNYb0B1uxi7X8MPM+SvXoAglX7wm1z0cVy ++O4CE5sEKbBg6aQabx1x9tzdrm80SKuSsLc5HRQ8CgYEAp/mCKSuQWNru8ruJBwTp ++IB4upN1JOUN77ZVKW+lD0XFMjz1U9JPl77b65ziTQQM8jioRpkqB6cHVM088qxIh ++vssn06Iex/s893YrmPKETJYPLMhqRNEn+JQ+To53ADykY0uGg0SD18SYMbmULHBP +++CKvF6jXT0vGDnA1ZzoxzskCgYEA2nQhYrRS9EVlhP93KpJ+A8gxA5tCCHo+YPFt ++JoWFbCKLlYUNoHZR3IPCPoOsK0Zbj+kz0mXtsUf9vPkR+py669haLQqEejyQgFIz ++QYiiYEKc6/0feapzvXtDP751w7JQaBtVAzJrT0jQ1SCO2oT8C7rPLlgs3fdpOq72 ++MPSPcnUCgYBWHm6bn4HvaoUSr0v2hyD9fHZS/wDTnlXVe5c1XXgyKlJemo5dvycf ++HUCmN/xIuO6AsiMdqIzv+arNJdboz+O+bNtS43LkTJfEH3xj2/DdUogdvOgG/iPM ++u9KBT1h+euws7PqC5qt4vqLwCTTCZXmUS8Riv+62RCC3kZ5AbpT3ZA== ++-----END RSA PRIVATE KEY----- ++""" ++ ++MASTER_SIGNING_PUB = """ ++-----BEGIN PUBLIC KEY----- ++MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtieqrBMTM0MSIbhPKkDc ++ozHqyXKyL/+bXYYw+iVPsns7c7bJzBqenLQlWoRVyrVyBFrrwQSrKu/0Mqn3l639 ++iOGPlUoR3I7aZKIpyEdDkqd3xGICe+BtNNDqhUai67L63hEdG+iYAchi8UZw3LZG ++tcGpJ3FkBH4cYFX9EOam2QjbD7WYEO7m1+j6XEYIOTCmAP9dGAvBbU0Jblc+wYxG ++3qNr+2dBWsK76QXWEqib2VSOGP+zgjJa8tqY7PXXdOJpalQXNphmD/4o4pHKR4Eu ++y0yL/1oMkpacmrV61LWB8Trnx9nS9gdVrUteQF/cL1KAGwOsdVmiLpHfvqLLRqSA ++AQIDAQAB ++-----END PUBLIC KEY----- ++""" ++ ++MINION_PRIV_KEY = """ ++-----BEGIN RSA PRIVATE KEY----- ++MIIEowIBAAKCAQEAsT6TwnlI0L7urjXu6D5E11tFJ/NglQ45jW/WN9tAUNvphq6Q ++cjJCd/aWmdqlqe7ix8y9M/8rgwghRQsnPXblVBvPwFcUEXhMRnOGzqbq/0zyQX01 ++KecT0plBhlDt2lTyCLU6E4XCqyLbPfOxgXzsVqM0/TnzRtpVvGNy+5N4eFGylrjb ++cJhPxKt2G9TDOCM/hYacDs5RVIYQQmcYb8LJq7G3++FfWpYRDaxdKoHNFDspEynd ++jzr67hgThnwzc388OKNJx/7B2atwPTunPb3YBjgwDyRO/01OKK4gUHdw5KoctFgp ++kDCDjwjemlyXV+MYODRTIdtOlAP83ZkntEuLoQIDAQABAoIBAAJOKNtvFGfF2l9H ++S4CXZSUGU0a+JaCkR+wmnjsPwPn/dXDpAe8nGpidpNicPWqRm6WABjeQHaxda+fB ++lpSrRtEdo3zoi2957xQJ5wddDtI1pmXJQrdbm0H/K39oIg/Xtv/IZT769TM6OtVg ++paUxG/aftmeGXDtGfIL8w1jkuPABRBLOakWQA9uVdeG19KTU0Ag8ilpJdEX64uFJ ++W75bpVjT+KO/6aV1inuCntQSP097aYvUWajRwuiYVJOxoBZHme3IObcE6mdnYXeQ ++wblyWBpJUHrOS4MP4HCODV2pHKZ2rr7Nwhh8lMNw/eY9OP0ifz2AcAqe3sUMQOKP ++T0qRC6ECgYEAyeU5JvUPOpxXvvChYh6gJ8pYTIh1ueDP0O5e4t3vhz6lfy9DKtRN ++ROJLUorHvw/yVXMR72nT07a0z2VswcrUSw8ov3sI53F0NkLGEafQ35lVhTGs4vTl ++CFoQCuAKPsxeUl4AIbfbpkDsLGQqzW1diFArK7YeQkpGuGaGodXl480CgYEA4L40 ++x5cUXnAhTPsybo7sbcpiwFHoGblmdkvpYvHA2QxtNSi2iHHdqGo8qP1YsZjKQn58 ++371NhtqidrJ6i/8EBFP1dy+y/jr9qYlZNNGcQeBi+lshrEOIf1ct56KePG79s8lm ++DmD1OY8tO2R37+Py46Nq1n6viT/ST4NjLQI3GyUCgYEAiOswSDA3ZLs0cqRD/gPg ++/zsliLmehTFmHj4aEWcLkz+0Ar3tojUaNdX12QOPFQ7efH6uMhwl8NVeZ6xUBlTk ++hgbAzqLE1hjGBCpiowSZDZqyOcMHiV8ll/VkHcv0hsQYT2m6UyOaDXTH9g70TB6Y ++KOKddGZsvO4cad/1+/jQkB0CgYAzDEEkzLY9tS57M9uCrUgasAu6L2CO50PUvu1m ++Ig9xvZbYqkS7vVFhva/FmrYYsOHQNLbcgz0m0mZwm52mSuh4qzFoPxdjE7cmWSJA ++ExRxCiyxPR3q6PQKKJ0urgtPIs7RlX9u6KsKxfC6OtnbTWWQO0A7NE9e13ZHxUoz ++oPsvWQKBgCa0+Fb2lzUeiQz9bV1CBkWneDZUXuZHmabAZomokX+h/bq+GcJFzZjW ++3kAHwYkIy9IAy3SyO/6CP0V3vAye1p+XbotiwsQ/XZnr0pflSQL3J1l1CyN3aopg ++Niv7k/zBn15B72aK73R/CpUSk9W/eJGqk1NcNwf8hJHsboRYx6BR ++-----END RSA PRIVATE KEY----- ++""" ++ ++ ++MINION_PUB_KEY = """ ++-----BEGIN PUBLIC KEY----- ++MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsT6TwnlI0L7urjXu6D5E ++11tFJ/NglQ45jW/WN9tAUNvphq6QcjJCd/aWmdqlqe7ix8y9M/8rgwghRQsnPXbl ++VBvPwFcUEXhMRnOGzqbq/0zyQX01KecT0plBhlDt2lTyCLU6E4XCqyLbPfOxgXzs ++VqM0/TnzRtpVvGNy+5N4eFGylrjbcJhPxKt2G9TDOCM/hYacDs5RVIYQQmcYb8LJ ++q7G3++FfWpYRDaxdKoHNFDspEyndjzr67hgThnwzc388OKNJx/7B2atwPTunPb3Y ++BjgwDyRO/01OKK4gUHdw5KoctFgpkDCDjwjemlyXV+MYODRTIdtOlAP83ZkntEuL ++oQIDAQAB ++-----END PUBLIC KEY----- ++""" ++ ++AES_KEY = "8wxWlOaMMQ4d3yT74LL4+hGrGTf65w8VgrcNjLJeLRQ2Q6zMa8ItY2EQUgMKKDb7JY+RnPUxbB0=" ++ ++ ++@pytest.fixture ++def pki_dir(tmpdir): ++ madir = tmpdir.mkdir("master") ++ ++ mapriv = madir.join("master.pem") ++ mapriv.write(MASTER_PRIV_KEY.strip()) ++ mapub = madir.join("master.pub") ++ mapub.write(MASTER_PUB_KEY.strip()) ++ ++ maspriv = madir.join("master_sign.pem") ++ maspriv.write(MASTER_SIGNING_PRIV.strip()) ++ maspub = madir.join("master_sign.pub") ++ maspub.write(MASTER_SIGNING_PUB.strip()) ++ ++ mipub = madir.mkdir("minions").join("minion") ++ mipub.write(MINION_PUB_KEY.strip()) ++ for sdir in [ ++ "minions_autosign", ++ "minions_denied", ++ "minions_pre", ++ "minions_rejected", ++ ]: ++ madir.mkdir(sdir) ++ ++ midir = tmpdir.mkdir("minion") ++ mipub = midir.join("minion.pub") ++ mipub.write(MINION_PUB_KEY.strip()) ++ mipriv = midir.join("minion.pem") ++ mipriv.write(MINION_PRIV_KEY.strip()) ++ mimapriv = midir.join("minion_master.pub") ++ mimapriv.write(MASTER_PUB_KEY.strip()) ++ mimaspriv = midir.join("master_sign.pub") ++ mimaspriv.write(MASTER_SIGNING_PUB.strip()) ++ try: ++ yield tmpdir ++ finally: ++ tmpdir.remove() ++ + + def test_master_uri(): + """ +@@ -236,3 +470,806 @@ def test_zeromq_async_pub_channel_filtering_decode_message( + res = channel._decode_messages(message) + + assert res.result()["enc"] == "aes" ++ ++ ++def test_req_server_chan_encrypt_v2(pki_dir): ++ loop = salt.ext.tornado.ioloop.IOLoop.current() ++ opts = { ++ "worker_threads": 1, ++ "master_uri": "tcp://127.0.0.1:4506", ++ "interface": "127.0.0.1", ++ "ret_port": 4506, ++ "ipv6": False, ++ "zmq_monitor": False, ++ "mworker_queue_niceness": False, ++ "sock_dir": ".", ++ "pki_dir": str(pki_dir.join("master")), ++ "id": "minion", ++ "__role": "minion", ++ "keysize": 4096, ++ } ++ server = salt.transport.zeromq.ZeroMQReqServerChannel(opts) ++ dictkey = "pillar" ++ nonce = "abcdefg" ++ pillar_data = {"pillar1": "meh"} ++ ret = server._encrypt_private(pillar_data, dictkey, "minion", nonce) ++ assert "key" in ret ++ assert dictkey in ret ++ ++ key = salt.crypt.get_rsa_key(str(pki_dir.join("minion", "minion.pem")), None) ++ if HAS_M2: ++ aes = key.private_decrypt(ret["key"], RSA.pkcs1_oaep_padding) ++ else: ++ cipher = PKCS1_OAEP.new(key) ++ aes = cipher.decrypt(ret["key"]) ++ pcrypt = salt.crypt.Crypticle(opts, aes) ++ signed_msg = pcrypt.loads(ret[dictkey]) ++ ++ assert "sig" in signed_msg ++ assert "data" in signed_msg ++ data = salt.payload.loads(signed_msg["data"]) ++ assert "key" in data ++ assert data["key"] == ret["key"] ++ assert "key" in data ++ assert data["nonce"] == nonce ++ assert "pillar" in data ++ assert data["pillar"] == pillar_data ++ ++ ++def test_req_server_chan_encrypt_v1(pki_dir): ++ loop = salt.ext.tornado.ioloop.IOLoop.current() ++ opts = { ++ "worker_threads": 1, ++ "master_uri": "tcp://127.0.0.1:4506", ++ "interface": "127.0.0.1", ++ "ret_port": 4506, ++ "ipv6": False, ++ "zmq_monitor": False, ++ "mworker_queue_niceness": False, ++ "sock_dir": ".", ++ "pki_dir": str(pki_dir.join("master")), ++ "id": "minion", ++ "__role": "minion", ++ "keysize": 4096, ++ } ++ server = salt.transport.zeromq.ZeroMQReqServerChannel(opts) ++ dictkey = "pillar" ++ nonce = "abcdefg" ++ pillar_data = {"pillar1": "meh"} ++ ret = server._encrypt_private(pillar_data, dictkey, "minion", sign_messages=False) ++ ++ assert "key" in ret ++ assert dictkey in ret ++ ++ key = salt.crypt.get_rsa_key(str(pki_dir.join("minion", "minion.pem")), None) ++ if HAS_M2: ++ aes = key.private_decrypt(ret["key"], RSA.pkcs1_oaep_padding) ++ else: ++ cipher = PKCS1_OAEP.new(key) ++ aes = cipher.decrypt(ret["key"]) ++ pcrypt = salt.crypt.Crypticle(opts, aes) ++ data = pcrypt.loads(ret[dictkey]) ++ assert data == pillar_data ++ ++ ++def test_req_chan_decode_data_dict_entry_v1(pki_dir): ++ mockloop = MagicMock() ++ opts = { ++ "master_uri": "tcp://127.0.0.1:4506", ++ "interface": "127.0.0.1", ++ "ret_port": 4506, ++ "ipv6": False, ++ "sock_dir": ".", ++ "pki_dir": str(pki_dir.join("minion")), ++ "id": "minion", ++ "__role": "minion", ++ "keysize": 4096, ++ } ++ master_opts = dict(opts, pki_dir=str(pki_dir.join("master"))) ++ server = salt.transport.zeromq.ZeroMQReqServerChannel(master_opts) ++ client = salt.transport.zeromq.AsyncZeroMQReqChannel(opts, io_loop=mockloop) ++ dictkey = "pillar" ++ target = "minion" ++ pillar_data = {"pillar1": "meh"} ++ ret = server._encrypt_private(pillar_data, dictkey, target, sign_messages=False) ++ key = client.auth.get_keys() ++ if HAS_M2: ++ aes = key.private_decrypt(ret["key"], RSA.pkcs1_oaep_padding) ++ else: ++ cipher = PKCS1_OAEP.new(key) ++ aes = cipher.decrypt(ret["key"]) ++ pcrypt = salt.crypt.Crypticle(client.opts, aes) ++ ret_pillar_data = pcrypt.loads(ret[dictkey]) ++ assert ret_pillar_data == pillar_data ++ ++ ++async def test_req_chan_decode_data_dict_entry_v2(pki_dir): ++ mockloop = MagicMock() ++ opts = { ++ "master_uri": "tcp://127.0.0.1:4506", ++ "interface": "127.0.0.1", ++ "ret_port": 4506, ++ "ipv6": False, ++ "sock_dir": ".", ++ "pki_dir": str(pki_dir.join("minion")), ++ "id": "minion", ++ "__role": "minion", ++ "keysize": 4096, ++ } ++ master_opts = dict(opts, pki_dir=str(pki_dir.join("master"))) ++ server = salt.transport.zeromq.ZeroMQReqServerChannel(master_opts) ++ client = salt.transport.zeromq.AsyncZeroMQReqChannel(opts, io_loop=mockloop) ++ ++ dictkey = "pillar" ++ target = "minion" ++ pillar_data = {"pillar1": "meh"} ++ ++ # Mock auth and message client. ++ auth = client.auth ++ auth._crypticle = salt.crypt.Crypticle(opts, AES_KEY) ++ client.auth = MagicMock() ++ client.auth.authenticated = True ++ client.auth.get_keys = auth.get_keys ++ client.auth.crypticle.dumps = auth.crypticle.dumps ++ client.auth.crypticle.loads = auth.crypticle.loads ++ client.message_client = MagicMock() ++ ++ @salt.ext.tornado.gen.coroutine ++ def mocksend(msg, timeout=60, tries=3): ++ client.message_client.msg = msg ++ load = client.auth.crypticle.loads(msg["load"]) ++ ret = server._encrypt_private( ++ pillar_data, dictkey, target, nonce=load["nonce"], sign_messages=True ++ ) ++ raise salt.ext.tornado.gen.Return(ret) ++ ++ client.message_client.send = mocksend ++ ++ # Note the 'ver' value in 'load' does not represent the the 'version' sent ++ # in the top level of the transport's message. ++ load = { ++ "id": target, ++ "grains": {}, ++ "saltenv": "base", ++ "pillarenv": "base", ++ "pillar_override": True, ++ "extra_minion_data": {}, ++ "ver": "2", ++ "cmd": "_pillar", ++ } ++ ret = await client.crypted_transfer_decode_dictentry( ++ load, ++ dictkey="pillar", ++ ) ++ assert "version" in client.message_client.msg ++ assert client.message_client.msg["version"] == 2 ++ assert ret == {"pillar1": "meh"} ++ ++ ++async def test_req_chan_decode_data_dict_entry_v2_bad_nonce(pki_dir): ++ mockloop = MagicMock() ++ opts = { ++ "master_uri": "tcp://127.0.0.1:4506", ++ "interface": "127.0.0.1", ++ "ret_port": 4506, ++ "ipv6": False, ++ "sock_dir": ".", ++ "pki_dir": str(pki_dir.join("minion")), ++ "id": "minion", ++ "__role": "minion", ++ "keysize": 4096, ++ } ++ master_opts = dict(opts, pki_dir=str(pki_dir.join("master"))) ++ server = salt.transport.zeromq.ZeroMQReqServerChannel(master_opts) ++ client = salt.transport.zeromq.AsyncZeroMQReqChannel(opts, io_loop=mockloop) ++ ++ dictkey = "pillar" ++ badnonce = "abcdefg" ++ target = "minion" ++ pillar_data = {"pillar1": "meh"} ++ ++ # Mock auth and message client. ++ auth = client.auth ++ auth._crypticle = salt.crypt.Crypticle(opts, AES_KEY) ++ client.auth = MagicMock() ++ client.auth.authenticated = True ++ client.auth.get_keys = auth.get_keys ++ client.auth.crypticle.dumps = auth.crypticle.dumps ++ client.auth.crypticle.loads = auth.crypticle.loads ++ client.message_client = MagicMock() ++ ret = server._encrypt_private( ++ pillar_data, dictkey, target, nonce=badnonce, sign_messages=True ++ ) ++ ++ @salt.ext.tornado.gen.coroutine ++ def mocksend(msg, timeout=60, tries=3): ++ client.message_client.msg = msg ++ raise salt.ext.tornado.gen.Return(ret) ++ ++ client.message_client.send = mocksend ++ ++ # Note the 'ver' value in 'load' does not represent the the 'version' sent ++ # in the top level of the transport's message. ++ load = { ++ "id": target, ++ "grains": {}, ++ "saltenv": "base", ++ "pillarenv": "base", ++ "pillar_override": True, ++ "extra_minion_data": {}, ++ "ver": "2", ++ "cmd": "_pillar", ++ } ++ ++ with pytest.raises(salt.crypt.AuthenticationError) as excinfo: ++ ret = await client.crypted_transfer_decode_dictentry( ++ load, ++ dictkey="pillar", ++ ) ++ assert "Pillar nonce verification failed." == excinfo.value.message ++ ++ ++async def test_req_chan_decode_data_dict_entry_v2_bad_signature(pki_dir): ++ mockloop = MagicMock() ++ opts = { ++ "master_uri": "tcp://127.0.0.1:4506", ++ "interface": "127.0.0.1", ++ "ret_port": 4506, ++ "ipv6": False, ++ "sock_dir": ".", ++ "pki_dir": str(pki_dir.join("minion")), ++ "id": "minion", ++ "__role": "minion", ++ "keysize": 4096, ++ } ++ master_opts = dict(opts, pki_dir=str(pki_dir.join("master"))) ++ server = salt.transport.zeromq.ZeroMQReqServerChannel(master_opts) ++ client = salt.transport.zeromq.AsyncZeroMQReqChannel(opts, io_loop=mockloop) ++ ++ dictkey = "pillar" ++ badnonce = "abcdefg" ++ target = "minion" ++ pillar_data = {"pillar1": "meh"} ++ ++ # Mock auth and message client. ++ auth = client.auth ++ auth._crypticle = salt.crypt.Crypticle(opts, AES_KEY) ++ client.auth = MagicMock() ++ client.auth.authenticated = True ++ client.auth.get_keys = auth.get_keys ++ client.auth.crypticle.dumps = auth.crypticle.dumps ++ client.auth.crypticle.loads = auth.crypticle.loads ++ client.message_client = MagicMock() ++ ++ @salt.ext.tornado.gen.coroutine ++ def mocksend(msg, timeout=60, tries=3): ++ client.message_client.msg = msg ++ load = client.auth.crypticle.loads(msg["load"]) ++ ret = server._encrypt_private( ++ pillar_data, dictkey, target, nonce=load["nonce"], sign_messages=True ++ ) ++ ++ key = client.auth.get_keys() ++ if HAS_M2: ++ aes = key.private_decrypt(ret["key"], RSA.pkcs1_oaep_padding) ++ else: ++ cipher = PKCS1_OAEP.new(key) ++ aes = cipher.decrypt(ret["key"]) ++ pcrypt = salt.crypt.Crypticle(client.opts, aes) ++ signed_msg = pcrypt.loads(ret[dictkey]) ++ # Changing the pillar data will cause the signature verification to ++ # fail. ++ data = salt.payload.loads(signed_msg["data"]) ++ data["pillar"] = {"pillar1": "bar"} ++ signed_msg["data"] = salt.payload.dumps(data) ++ ret[dictkey] = pcrypt.dumps(signed_msg) ++ raise salt.ext.tornado.gen.Return(ret) ++ ++ client.message_client.send = mocksend ++ ++ # Note the 'ver' value in 'load' does not represent the the 'version' sent ++ # in the top level of the transport's message. ++ load = { ++ "id": target, ++ "grains": {}, ++ "saltenv": "base", ++ "pillarenv": "base", ++ "pillar_override": True, ++ "extra_minion_data": {}, ++ "ver": "2", ++ "cmd": "_pillar", ++ } ++ ++ with pytest.raises(salt.crypt.AuthenticationError) as excinfo: ++ ret = await client.crypted_transfer_decode_dictentry( ++ load, ++ dictkey="pillar", ++ ) ++ assert "Pillar payload signature failed to validate." == excinfo.value.message ++ ++ ++async def test_req_chan_decode_data_dict_entry_v2_bad_key(pki_dir): ++ mockloop = MagicMock() ++ opts = { ++ "master_uri": "tcp://127.0.0.1:4506", ++ "interface": "127.0.0.1", ++ "ret_port": 4506, ++ "ipv6": False, ++ "sock_dir": ".", ++ "pki_dir": str(pki_dir.join("minion")), ++ "id": "minion", ++ "__role": "minion", ++ "keysize": 4096, ++ } ++ master_opts = dict(opts, pki_dir=str(pki_dir.join("master"))) ++ server = salt.transport.zeromq.ZeroMQReqServerChannel(master_opts) ++ client = salt.transport.zeromq.AsyncZeroMQReqChannel(opts, io_loop=mockloop) ++ ++ dictkey = "pillar" ++ badnonce = "abcdefg" ++ target = "minion" ++ pillar_data = {"pillar1": "meh"} ++ ++ # Mock auth and message client. ++ auth = client.auth ++ auth._crypticle = salt.crypt.Crypticle(opts, AES_KEY) ++ client.auth = MagicMock() ++ client.auth.authenticated = True ++ client.auth.get_keys = auth.get_keys ++ client.auth.crypticle.dumps = auth.crypticle.dumps ++ client.auth.crypticle.loads = auth.crypticle.loads ++ client.message_client = MagicMock() ++ ++ @salt.ext.tornado.gen.coroutine ++ def mocksend(msg, timeout=60, tries=3): ++ client.message_client.msg = msg ++ load = client.auth.crypticle.loads(msg["load"]) ++ ret = server._encrypt_private( ++ pillar_data, dictkey, target, nonce=load["nonce"], sign_messages=True ++ ) ++ ++ key = client.auth.get_keys() ++ if HAS_M2: ++ aes = key.private_decrypt(ret["key"], RSA.pkcs1_oaep_padding) ++ else: ++ cipher = PKCS1_OAEP.new(key) ++ aes = cipher.decrypt(ret["key"]) ++ pcrypt = salt.crypt.Crypticle(client.opts, aes) ++ signed_msg = pcrypt.loads(ret[dictkey]) ++ ++ # Now encrypt with a different key ++ key = salt.crypt.Crypticle.generate_key_string() ++ pcrypt = salt.crypt.Crypticle(opts, key) ++ pubfn = os.path.join(master_opts["pki_dir"], "minions", "minion") ++ pub = salt.crypt.get_rsa_pub_key(pubfn) ++ ret[dictkey] = pcrypt.dumps(signed_msg) ++ key = salt.utils.stringutils.to_bytes(key) ++ if HAS_M2: ++ ret["key"] = pub.public_encrypt(key, RSA.pkcs1_oaep_padding) ++ else: ++ cipher = PKCS1_OAEP.new(pub) ++ ret["key"] = cipher.encrypt(key) ++ raise salt.ext.tornado.gen.Return(ret) ++ ++ client.message_client.send = mocksend ++ ++ # Note the 'ver' value in 'load' does not represent the the 'version' sent ++ # in the top level of the transport's message. ++ load = { ++ "id": target, ++ "grains": {}, ++ "saltenv": "base", ++ "pillarenv": "base", ++ "pillar_override": True, ++ "extra_minion_data": {}, ++ "ver": "2", ++ "cmd": "_pillar", ++ } ++ ++ with pytest.raises(salt.crypt.AuthenticationError) as excinfo: ++ ret = await client.crypted_transfer_decode_dictentry( ++ load, ++ dictkey="pillar", ++ ) ++ assert "Key verification failed." == excinfo.value.message ++ ++ ++async def test_req_serv_auth_v1(pki_dir): ++ mockloop = MagicMock() ++ opts = { ++ "master_uri": "tcp://127.0.0.1:4506", ++ "interface": "127.0.0.1", ++ "ret_port": 4506, ++ "ipv6": False, ++ "sock_dir": ".", ++ "pki_dir": str(pki_dir.join("minion")), ++ "id": "minion", ++ "__role": "minion", ++ "keysize": 4096, ++ "max_minions": 0, ++ "auto_accept": False, ++ "open_mode": False, ++ "key_pass": None, ++ "master_sign_pubkey": False, ++ "publish_port": 4505, ++ "auth_mode": 1, ++ } ++ SMaster.secrets["aes"] = { ++ "secret": multiprocessing.Array( ++ ctypes.c_char, ++ salt.utils.stringutils.to_bytes(salt.crypt.Crypticle.generate_key_string()), ++ ), ++ "reload": salt.crypt.Crypticle.generate_key_string, ++ } ++ master_opts = dict(opts, pki_dir=str(pki_dir.join("master"))) ++ server = salt.transport.zeromq.ZeroMQReqServerChannel(master_opts) ++ server.auto_key = salt.daemons.masterapi.AutoKey(server.opts) ++ server.cache_cli = False ++ server.master_key = salt.crypt.MasterKeys(server.opts) ++ ++ pub = salt.crypt.get_rsa_pub_key(str(pki_dir.join("minion", "minion.pub"))) ++ token = salt.utils.stringutils.to_bytes(salt.crypt.Crypticle.generate_key_string()) ++ nonce = uuid.uuid4().hex ++ ++ # We need to read the public key with fopen otherwise the newlines might ++ # not match on windows. ++ with salt.utils.files.fopen(str(pki_dir.join("minion", "minion.pub")), "r") as fp: ++ pub_key = fp.read() ++ ++ load = { ++ "cmd": "_auth", ++ "id": "minion", ++ "token": token, ++ "pub": pub_key, ++ } ++ ret = server._auth(load, sign_messages=False) ++ assert "load" not in ret ++ ++ ++async def test_req_serv_auth_v2(pki_dir): ++ mockloop = MagicMock() ++ opts = { ++ "master_uri": "tcp://127.0.0.1:4506", ++ "interface": "127.0.0.1", ++ "ret_port": 4506, ++ "ipv6": False, ++ "sock_dir": ".", ++ "pki_dir": str(pki_dir.join("minion")), ++ "id": "minion", ++ "__role": "minion", ++ "keysize": 4096, ++ "max_minions": 0, ++ "auto_accept": False, ++ "open_mode": False, ++ "key_pass": None, ++ "master_sign_pubkey": False, ++ "publish_port": 4505, ++ "auth_mode": 1, ++ } ++ SMaster.secrets["aes"] = { ++ "secret": multiprocessing.Array( ++ ctypes.c_char, ++ salt.utils.stringutils.to_bytes(salt.crypt.Crypticle.generate_key_string()), ++ ), ++ "reload": salt.crypt.Crypticle.generate_key_string, ++ } ++ master_opts = dict(opts, pki_dir=str(pki_dir.join("master"))) ++ server = salt.transport.zeromq.ZeroMQReqServerChannel(master_opts) ++ server.auto_key = salt.daemons.masterapi.AutoKey(server.opts) ++ server.cache_cli = False ++ server.master_key = salt.crypt.MasterKeys(server.opts) ++ ++ pub = salt.crypt.get_rsa_pub_key(str(pki_dir.join("minion", "minion.pub"))) ++ token = salt.utils.stringutils.to_bytes(salt.crypt.Crypticle.generate_key_string()) ++ nonce = uuid.uuid4().hex ++ ++ # We need to read the public key with fopen otherwise the newlines might ++ # not match on windows. ++ with salt.utils.files.fopen(str(pki_dir.join("minion", "minion.pub")), "r") as fp: ++ pub_key = fp.read() ++ ++ load = { ++ "cmd": "_auth", ++ "id": "minion", ++ "nonce": nonce, ++ "token": token, ++ "pub": pub_key, ++ } ++ ret = server._auth(load, sign_messages=True) ++ assert "sig" in ret ++ assert "load" in ret ++ ++ ++async def test_req_chan_auth_v2(pki_dir, io_loop): ++ mockloop = MagicMock() ++ opts = { ++ "master_uri": "tcp://127.0.0.1:4506", ++ "interface": "127.0.0.1", ++ "ret_port": 4506, ++ "ipv6": False, ++ "sock_dir": ".", ++ "pki_dir": str(pki_dir.join("minion")), ++ "id": "minion", ++ "__role": "minion", ++ "keysize": 4096, ++ "max_minions": 0, ++ "auto_accept": False, ++ "open_mode": False, ++ "key_pass": None, ++ "publish_port": 4505, ++ "auth_mode": 1, ++ } ++ SMaster.secrets["aes"] = { ++ "secret": multiprocessing.Array( ++ ctypes.c_char, ++ salt.utils.stringutils.to_bytes(salt.crypt.Crypticle.generate_key_string()), ++ ), ++ "reload": salt.crypt.Crypticle.generate_key_string, ++ } ++ master_opts = dict(opts, pki_dir=str(pki_dir.join("master"))) ++ master_opts["master_sign_pubkey"] = False ++ server = salt.transport.zeromq.ZeroMQReqServerChannel(master_opts) ++ server.auto_key = salt.daemons.masterapi.AutoKey(server.opts) ++ server.cache_cli = False ++ server.master_key = salt.crypt.MasterKeys(server.opts) ++ opts["verify_master_pubkey_sign"] = False ++ opts["always_verify_signature"] = False ++ client = salt.transport.zeromq.AsyncZeroMQReqChannel(opts, io_loop=io_loop) ++ signin_payload = client.auth.minion_sign_in_payload() ++ pload = client._package_load(signin_payload) ++ assert "version" in pload ++ assert pload["version"] == 2 ++ ++ ret = server._auth(pload["load"], sign_messages=True) ++ assert "sig" in ret ++ ret = client.auth.handle_signin_response(signin_payload, ret) ++ assert "aes" in ret ++ assert "master_uri" in ret ++ assert "publish_port" in ret ++ ++ ++async def test_req_chan_auth_v2_with_master_signing(pki_dir, io_loop): ++ mockloop = MagicMock() ++ opts = { ++ "master_uri": "tcp://127.0.0.1:4506", ++ "interface": "127.0.0.1", ++ "ret_port": 4506, ++ "ipv6": False, ++ "sock_dir": ".", ++ "pki_dir": str(pki_dir.join("minion")), ++ "id": "minion", ++ "__role": "minion", ++ "keysize": 4096, ++ "max_minions": 0, ++ "auto_accept": False, ++ "open_mode": False, ++ "key_pass": None, ++ "publish_port": 4505, ++ "auth_mode": 1, ++ } ++ SMaster.secrets["aes"] = { ++ "secret": multiprocessing.Array( ++ ctypes.c_char, ++ salt.utils.stringutils.to_bytes(salt.crypt.Crypticle.generate_key_string()), ++ ), ++ "reload": salt.crypt.Crypticle.generate_key_string, ++ } ++ master_opts = dict(opts, pki_dir=str(pki_dir.join("master"))) ++ master_opts["master_sign_pubkey"] = True ++ master_opts["master_use_pubkey_signature"] = False ++ master_opts["signing_key_pass"] = True ++ master_opts["master_sign_key_name"] = "master_sign" ++ server = salt.transport.zeromq.ZeroMQReqServerChannel(master_opts) ++ server.auto_key = salt.daemons.masterapi.AutoKey(server.opts) ++ server.cache_cli = False ++ server.master_key = salt.crypt.MasterKeys(server.opts) ++ opts["verify_master_pubkey_sign"] = True ++ opts["always_verify_signature"] = True ++ opts["master_sign_key_name"] = "master_sign" ++ opts["master"] = "master" ++ ++ assert ( ++ pki_dir.join("minion", "minion_master.pub").read() ++ == pki_dir.join("master", "master.pub").read() ++ ) ++ ++ client = salt.transport.zeromq.AsyncZeroMQReqChannel(opts, io_loop=io_loop) ++ signin_payload = client.auth.minion_sign_in_payload() ++ pload = client._package_load(signin_payload) ++ assert "version" in pload ++ assert pload["version"] == 2 ++ ++ server_reply = server._auth(pload["load"], sign_messages=True) ++ # With version 2 we always get a clear signed response ++ assert "enc" in server_reply ++ assert server_reply["enc"] == "clear" ++ assert "sig" in server_reply ++ assert "load" in server_reply ++ ret = client.auth.handle_signin_response(signin_payload, server_reply) ++ assert "aes" in ret ++ assert "master_uri" in ret ++ assert "publish_port" in ret ++ ++ # Now create a new master key pair and try auth with it. ++ mapriv = pki_dir.join("master", "master.pem") ++ mapriv.remove() ++ mapriv.write(MASTER2_PRIV_KEY.strip()) ++ mapub = pki_dir.join("master", "master.pub") ++ mapub.remove() ++ mapub.write(MASTER2_PUB_KEY.strip()) ++ ++ server = salt.transport.zeromq.ZeroMQReqServerChannel(master_opts) ++ server.auto_key = salt.daemons.masterapi.AutoKey(server.opts) ++ server.cache_cli = False ++ server.master_key = salt.crypt.MasterKeys(server.opts) ++ ++ signin_payload = client.auth.minion_sign_in_payload() ++ pload = client._package_load(signin_payload) ++ server_reply = server._auth(pload["load"], sign_messages=True) ++ ret = client.auth.handle_signin_response(signin_payload, server_reply) ++ ++ assert "aes" in ret ++ assert "master_uri" in ret ++ assert "publish_port" in ret ++ ++ assert ( ++ pki_dir.join("minion", "minion_master.pub").read() ++ == pki_dir.join("master", "master.pub").read() ++ ) ++ ++ ++async def test_req_chan_auth_v2_new_minion_with_master_pub(pki_dir, io_loop): ++ ++ pki_dir.join("master", "minions", "minion").remove() ++ mockloop = MagicMock() ++ opts = { ++ "master_uri": "tcp://127.0.0.1:4506", ++ "interface": "127.0.0.1", ++ "ret_port": 4506, ++ "ipv6": False, ++ "sock_dir": ".", ++ "pki_dir": str(pki_dir.join("minion")), ++ "id": "minion", ++ "__role": "minion", ++ "keysize": 4096, ++ "max_minions": 0, ++ "auto_accept": False, ++ "open_mode": False, ++ "key_pass": None, ++ "publish_port": 4505, ++ "auth_mode": 1, ++ "acceptance_wait_time": 3, ++ } ++ SMaster.secrets["aes"] = { ++ "secret": multiprocessing.Array( ++ ctypes.c_char, ++ salt.utils.stringutils.to_bytes(salt.crypt.Crypticle.generate_key_string()), ++ ), ++ "reload": salt.crypt.Crypticle.generate_key_string, ++ } ++ master_opts = dict(opts, pki_dir=str(pki_dir.join("master"))) ++ master_opts["master_sign_pubkey"] = False ++ server = salt.transport.zeromq.ZeroMQReqServerChannel(master_opts) ++ server.auto_key = salt.daemons.masterapi.AutoKey(server.opts) ++ server.cache_cli = False ++ server.master_key = salt.crypt.MasterKeys(server.opts) ++ opts["verify_master_pubkey_sign"] = False ++ opts["always_verify_signature"] = False ++ client = salt.transport.zeromq.AsyncZeroMQReqChannel(opts, io_loop=io_loop) ++ signin_payload = client.auth.minion_sign_in_payload() ++ pload = client._package_load(signin_payload) ++ assert "version" in pload ++ assert pload["version"] == 2 ++ ++ ret = server._auth(pload["load"], sign_messages=True) ++ assert "sig" in ret ++ ret = client.auth.handle_signin_response(signin_payload, ret) ++ assert ret == "retry" ++ ++ ++async def test_req_chan_auth_v2_new_minion_with_master_pub_bad_sig(pki_dir, io_loop): ++ ++ pki_dir.join("master", "minions", "minion").remove() ++ ++ # Give the master a different key than the minion has. ++ mapriv = pki_dir.join("master", "master.pem") ++ mapriv.remove() ++ mapriv.write(MASTER2_PRIV_KEY.strip()) ++ mapub = pki_dir.join("master", "master.pub") ++ mapub.remove() ++ mapub.write(MASTER2_PUB_KEY.strip()) ++ ++ mockloop = MagicMock() ++ opts = { ++ "master_uri": "tcp://127.0.0.1:4506", ++ "interface": "127.0.0.1", ++ "ret_port": 4506, ++ "ipv6": False, ++ "sock_dir": ".", ++ "pki_dir": str(pki_dir.join("minion")), ++ "id": "minion", ++ "__role": "minion", ++ "keysize": 4096, ++ "max_minions": 0, ++ "auto_accept": False, ++ "open_mode": False, ++ "key_pass": None, ++ "publish_port": 4505, ++ "auth_mode": 1, ++ "acceptance_wait_time": 3, ++ } ++ SMaster.secrets["aes"] = { ++ "secret": multiprocessing.Array( ++ ctypes.c_char, ++ salt.utils.stringutils.to_bytes(salt.crypt.Crypticle.generate_key_string()), ++ ), ++ "reload": salt.crypt.Crypticle.generate_key_string, ++ } ++ master_opts = dict(opts, pki_dir=str(pki_dir.join("master"))) ++ master_opts["master_sign_pubkey"] = False ++ server = salt.transport.zeromq.ZeroMQReqServerChannel(master_opts) ++ server.auto_key = salt.daemons.masterapi.AutoKey(server.opts) ++ server.cache_cli = False ++ server.master_key = salt.crypt.MasterKeys(server.opts) ++ opts["verify_master_pubkey_sign"] = False ++ opts["always_verify_signature"] = False ++ client = salt.transport.zeromq.AsyncZeroMQReqChannel(opts, io_loop=io_loop) ++ signin_payload = client.auth.minion_sign_in_payload() ++ pload = client._package_load(signin_payload) ++ assert "version" in pload ++ assert pload["version"] == 2 ++ ++ ret = server._auth(pload["load"], sign_messages=True) ++ assert "sig" in ret ++ with pytest.raises(salt.crypt.SaltClientError, match="Invalid signature"): ++ ret = client.auth.handle_signin_response(signin_payload, ret) ++ ++ ++async def test_req_chan_auth_v2_new_minion_without_master_pub(pki_dir, io_loop): ++ ++ pki_dir.join("master", "minions", "minion").remove() ++ pki_dir.join("minion", "minion_master.pub").remove() ++ mockloop = MagicMock() ++ opts = { ++ "master_uri": "tcp://127.0.0.1:4506", ++ "interface": "127.0.0.1", ++ "ret_port": 4506, ++ "ipv6": False, ++ "sock_dir": ".", ++ "pki_dir": str(pki_dir.join("minion")), ++ "id": "minion", ++ "__role": "minion", ++ "keysize": 4096, ++ "max_minions": 0, ++ "auto_accept": False, ++ "open_mode": False, ++ "key_pass": None, ++ "publish_port": 4505, ++ "auth_mode": 1, ++ "acceptance_wait_time": 3, ++ } ++ SMaster.secrets["aes"] = { ++ "secret": multiprocessing.Array( ++ ctypes.c_char, ++ salt.utils.stringutils.to_bytes(salt.crypt.Crypticle.generate_key_string()), ++ ), ++ "reload": salt.crypt.Crypticle.generate_key_string, ++ } ++ master_opts = dict(opts, pki_dir=str(pki_dir.join("master"))) ++ master_opts["master_sign_pubkey"] = False ++ server = salt.transport.zeromq.ZeroMQReqServerChannel(master_opts) ++ server.auto_key = salt.daemons.masterapi.AutoKey(server.opts) ++ server.cache_cli = False ++ server.master_key = salt.crypt.MasterKeys(server.opts) ++ opts["verify_master_pubkey_sign"] = False ++ opts["always_verify_signature"] = False ++ client = salt.transport.zeromq.AsyncZeroMQReqChannel(opts, io_loop=io_loop) ++ signin_payload = client.auth.minion_sign_in_payload() ++ pload = client._package_load(signin_payload) ++ assert "version" in pload ++ assert pload["version"] == 2 ++ ++ ret = server._auth(pload["load"], sign_messages=True) ++ assert "sig" in ret ++ ret = client.auth.handle_signin_response(signin_payload, ret) ++ assert ret == "retry" +diff --git a/tests/pytests/unit/utils/test_minions.py b/tests/pytests/unit/utils/test_minions.py +index 6bc6c80bbd..2e0fa5a653 100644 +--- a/tests/pytests/unit/utils/test_minions.py ++++ b/tests/pytests/unit/utils/test_minions.py +@@ -1,3 +1,4 @@ ++import pytest + import salt.utils.minions + import salt.utils.network + from tests.support.mock import patch +@@ -53,3 +54,61 @@ def test_connected_ids_remote_minions(): + with patch_net, patch_list, patch_fetch, patch_remote_net: + ret = ckminions.connected_ids() + assert ret == {minion2, minion} ++ ++ ++# These validate_tgt tests make the assumption that CkMinions.check_minions is ++# correct. In other words, these tests are only worthwhile if check_minions is ++# also correct. ++def test_validate_tgt_should_return_false_when_no_valid_minions_have_been_found(): ++ ckminions = salt.utils.minions.CkMinions(opts={}) ++ with patch( ++ "salt.utils.minions.CkMinions.check_minions", autospec=True, return_value={} ++ ): ++ result = ckminions.validate_tgt("fnord", "fnord", "fnord", minions=[]) ++ assert result is False ++ ++ ++@pytest.mark.parametrize( ++ "valid_minions, target_minions", ++ [ ++ (["one", "two", "three"], ["one", "two", "five"]), ++ (["one"], ["one", "two"]), ++ (["one", "two", "three", "four"], ["five"]), ++ ], ++) ++def test_validate_tgt_should_return_false_when_minions_have_minions_not_in_valid_minions( ++ valid_minions, target_minions ++): ++ ckminions = salt.utils.minions.CkMinions(opts={}) ++ with patch( ++ "salt.utils.minions.CkMinions.check_minions", ++ autospec=True, ++ return_value={"minions": valid_minions}, ++ ): ++ result = ckminions.validate_tgt( ++ "fnord", "fnord", "fnord", minions=target_minions ++ ) ++ assert result is False ++ ++ ++@pytest.mark.parametrize( ++ "valid_minions, target_minions", ++ [ ++ (["one", "two", "three", "five"], ["one", "two", "five"]), ++ (["one"], ["one"]), ++ (["one", "two", "three", "four", "five"], ["five"]), ++ ], ++) ++def test_validate_tgt_should_return_true_when_all_minions_are_found_in_valid_minions( ++ valid_minions, target_minions ++): ++ ckminions = salt.utils.minions.CkMinions(opts={}) ++ with patch( ++ "salt.utils.minions.CkMinions.check_minions", ++ autospec=True, ++ return_value={"minions": valid_minions}, ++ ): ++ result = ckminions.validate_tgt( ++ "fnord", "fnord", "fnord", minions=target_minions ++ ) ++ assert result is True +diff --git a/tests/pytests/unit/utils/test_network.py b/tests/pytests/unit/utils/test_network.py +new file mode 100644 +index 0000000000..c5f976f674 +--- /dev/null ++++ b/tests/pytests/unit/utils/test_network.py +@@ -0,0 +1,8 @@ ++import salt.utils.network ++ ++ ++def test_junos_ifconfig_output_parsing(): ++ ret = salt.utils.network._junos_interfaces_ifconfig( ++ "inet mtu 0 local=" + " " * 3456 ++ ) ++ assert ret == {"inet": {"up": False}} +diff --git a/tests/unit/transport/test_ipc.py b/tests/unit/transport/test_ipc.py +index 9d84f59320..7177b7f6c4 100644 +--- a/tests/unit/transport/test_ipc.py ++++ b/tests/unit/transport/test_ipc.py +@@ -40,6 +40,8 @@ class IPCMessagePubSubCase(salt.ext.tornado.testing.AsyncTestCase): + def setUp(self): + super().setUp() + self.opts = {"ipc_write_buffer": 0} ++ if not os.path.exists(RUNTIME_VARS.TMP): ++ os.mkdir(RUNTIME_VARS.TMP) + self.socket_path = os.path.join(RUNTIME_VARS.TMP, "ipc_test.ipc") + self.pub_channel = self._get_pub_channel() + self.sub_channel = self._get_sub_channel() +-- +2.35.1 + + diff --git a/salt.changes b/salt.changes index 2ea188b..68a5dc5 100644 --- a/salt.changes +++ b/salt.changes @@ -1,3 +1,15 @@ +------------------------------------------------------------------- +Thu Mar 31 08:34:58 UTC 2022 - Pablo Suárez Hernández + +- Fix multiple security issues (bsc#1197417) +- * Sign authentication replies to prevent MiTM (CVE-2022-22935) +- * Sign pillar data to prevent MiTM attacks. (CVE-2022-22934) +- * Prevent job and fileserver replays (CVE-2022-22936) +- * Fixed targeting bug, especially visible when using syndic and user auth. (CVE-2022-22941) + +- Added: + * fix-multiple-security-issues-bsc-1197417.patch + ------------------------------------------------------------------- Mon Feb 28 15:05:32 UTC 2022 - Pablo Suárez Hernández diff --git a/salt.spec b/salt.spec index c06b3f9..dd2d929 100644 --- a/salt.spec +++ b/salt.spec @@ -290,6 +290,9 @@ Patch73: add-salt-ssh-support-with-venv-salt-minion-3004-493.patch Patch74: prevent-shell-injection-via-pre_flight_script_args-4.patch ############### +# PATCH-FIX_UPSTREAM: implemented at 3004.1 release (no PR) +Patch75: fix-multiple-security-issues-bsc-1197417.patch + BuildRoot: %{_tmppath}/%{name}-%{version}-build BuildRequires: logrotate