From aa24e28ae42a794036df4334bfa6b6ff79019c61 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?=
 <psuarezhernandez@suse.com>
Date: Wed, 23 Mar 2022 11:07:00 +0000
Subject: [PATCH] CVE-2020-22934 CVE-2020-22935 CVE-2020-22936 CVE-2020-22941

---
 .pre-commit-config.yaml                     |    5 +
 requirements/static/ci/linux.in             |    6 +-
 requirements/static/ci/py3.5/linux.txt      |   20 +-
 requirements/static/ci/py3.5/windows.txt    |   15 +-
 requirements/static/ci/py3.6/linux.txt      |    6 +-
 requirements/static/ci/py3.6/windows.txt    |   15 +-
 requirements/static/ci/py3.7/linux.txt      |    6 +-
 requirements/static/ci/py3.7/windows.txt    |   16 +-
 requirements/static/ci/py3.8/linux.txt      |    6 +-
 requirements/static/ci/py3.9/linux.txt      |    6 +-
 requirements/static/ci/windows.in           |    2 +-
 requirements/static/pkg/py3.5/windows.txt   |    8 +-
 requirements/static/pkg/py3.6/windows.txt   |    8 +-
 requirements/static/pkg/py3.7/windows.txt   |    8 +-
 requirements/windows.txt                    |    4 +-
 salt/crypt.py                               |  278 +++--
 salt/master.py                              |   64 +-
 salt/minion.py                              |    1 +
 salt/pillar/__init__.py                     |    4 +
 salt/transport/mixins/auth.py               |  117 ++-
 salt/transport/tcp.py                       |  106 +-
 salt/transport/zeromq.py                    |   94 +-
 salt/utils/minions.py                       |   19 +-
 tests/pytests/conftest.py                   |   77 ++
 tests/pytests/unit/test_crypt.py            |  149 +++
 tests/pytests/unit/transport/test_zeromq.py | 1042 +++++++++++++++++++
 tests/pytests/unit/utils/test_minions.py    |   59 ++
 tests/unit/transport/mixins.py              |   18 +-
 tests/unit/transport/test_tcp.py            |   52 +-
 tests/unit/transport/test_zeromq.py         |   89 +-
 30 files changed, 1931 insertions(+), 369 deletions(-)
 create mode 100644 tests/pytests/unit/transport/test_zeromq.py

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 65b4fff1ff..38d148ffc1 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -285,6 +285,7 @@ repos:
           - --platform=linux
           - --include=requirements/static/pkg/py{py_version}/linux.txt
           - --include=requirements/pytest.txt
+          - --passthrough-line-from-input=^git\+https(.*)$
           - requirements/static/ci/linux.in
 
       - id: pip-tools-compile
@@ -298,6 +299,7 @@ repos:
           - --platform=linux
           - --include=requirements/static/pkg/py{py_version}/linux.txt
           - --include=requirements/pytest.txt
+          - --passthrough-line-from-input=^git\+https(.*)$
           - requirements/static/ci/linux.in
 
       - id: pip-tools-compile
@@ -311,6 +313,7 @@ repos:
           - --platform=linux
           - --include=requirements/static/pkg/py{py_version}/linux.txt
           - --include=requirements/pytest.txt
+          - --passthrough-line-from-input=^git\+https(.*)$
           - requirements/static/ci/linux.in
 
       - id: pip-tools-compile
@@ -324,6 +327,7 @@ repos:
           - --platform=linux
           - --include=requirements/static/pkg/py{py_version}/linux.txt
           - --include=requirements/pytest.txt
+          - --passthrough-line-from-input=^git\+https(.*)$
           - requirements/static/ci/linux.in
 
       - id: pip-tools-compile
@@ -337,6 +341,7 @@ repos:
           - --platform=linux
           - --include=requirements/static/pkg/py{py_version}/linux.txt
           - --include=requirements/pytest.txt
+          - --passthrough-line-from-input=^git\+https(.*)$
           - requirements/static/ci/linux.in
 
       - id: pip-tools-compile
diff --git a/requirements/static/ci/linux.in b/requirements/static/ci/linux.in
index 99e10fbc0b..fc54661b90 100644
--- a/requirements/static/ci/linux.in
+++ b/requirements/static/ci/linux.in
@@ -1,5 +1,5 @@
 apache-libcloud==2.0.0
-azure>=4.0.0
+azure==4.0.0; sys_platform != "win32"
 boto3>=1.13.5
 boto>=2.46.0
 certifi
@@ -19,10 +19,10 @@ keyring==5.7.1
 kubernetes<4.0
 libnacl>=1.7.1
 mock>=3.0.5
-moto
+moto; python_version >= '3.6'
 napalm; python_version >= '3.6'
 passlib
-paramiko>=2.1.6
+paramiko>=2.1.6; python_version >= '3.6'
 pycurl
 pygit2<=0.28.2; python_version < '3.8'
 pygit2>=1.2.0; python_version >= '3.8'
diff --git a/requirements/static/ci/py3.5/linux.txt b/requirements/static/ci/py3.5/linux.txt
index 82450fa2be..4ce3cf2d50 100644
--- a/requirements/static/ci/py3.5/linux.txt
+++ b/requirements/static/ci/py3.5/linux.txt
@@ -10,7 +10,6 @@ appdirs==1.4.4            # via virtualenv
 argh==0.26.2              # via watchdog
 asn1crypto==1.3.0         # via certvalidator, oscrypto
 attrs==19.1.0             # via pytest
-aws-xray-sdk==0.95        # via moto
 azure-applicationinsights==0.1.0  # via azure
 azure-batch==4.1.3        # via azure
 azure-common==1.1.18      # via azure-applicationinsights, azure-batch, azure-cosmosdb-table, azure-eventgrid, azure-graphrbac, azure-keyvault, azure-loganalytics, azure-mgmt-advisor, azure-mgmt-applicationinsights, azure-mgmt-authorization, azure-mgmt-batch, azure-mgmt-batchai, azure-mgmt-billing, azure-mgmt-cdn, azure-mgmt-cognitiveservices, azure-mgmt-commerce, azure-mgmt-compute, azure-mgmt-consumption, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-cosmosdb, azure-mgmt-datafactory, azure-mgmt-datalake-analytics, azure-mgmt-datalake-store, azure-mgmt-datamigration, azure-mgmt-devspaces, azure-mgmt-devtestlabs, azure-mgmt-dns, azure-mgmt-eventgrid, azure-mgmt-eventhub, azure-mgmt-hanaonazure, azure-mgmt-iotcentral, azure-mgmt-iothub, azure-mgmt-iothubprovisioningservices, azure-mgmt-keyvault, azure-mgmt-loganalytics, azure-mgmt-logic, azure-mgmt-machinelearningcompute, azure-mgmt-managementgroups, azure-mgmt-managementpartner, azure-mgmt-maps, azure-mgmt-marketplaceordering, azure-mgmt-media, azure-mgmt-monitor, azure-mgmt-msi, azure-mgmt-network, azure-mgmt-notificationhubs, azure-mgmt-policyinsights, azure-mgmt-powerbiembedded, azure-mgmt-rdbms, azure-mgmt-recoveryservices, azure-mgmt-recoveryservicesbackup, azure-mgmt-redis, azure-mgmt-relay, azure-mgmt-reservations, azure-mgmt-resource, azure-mgmt-scheduler, azure-mgmt-search, azure-mgmt-servicebus, azure-mgmt-servicefabric, azure-mgmt-signalr, azure-mgmt-sql, azure-mgmt-storage, azure-mgmt-subscription, azure-mgmt-trafficmanager, azure-mgmt-web, azure-servicebus, azure-servicefabric, azure-servicemanagement-legacy, azure-storage-blob, azure-storage-common, azure-storage-file, azure-storage-queue
@@ -92,13 +91,13 @@ azure-storage-blob==1.5.0  # via azure
 azure-storage-common==1.4.0  # via azure-cosmosdb-table, azure-storage-blob, azure-storage-file, azure-storage-queue
 azure-storage-file==1.4.0  # via azure
 azure-storage-queue==1.4.0  # via azure
-azure==4.0.0
+azure==4.0.0 ; sys_platform != "win32"
 backports.functools-lru-cache==1.5
 backports.ssl-match-hostname==3.7.0.1 ; python_version < "3.7"
 bcrypt==3.1.6             # via paramiko
 boto3==1.13.5
 boto==2.49.0
-botocore==1.16.26         # via boto3, moto, s3transfer
+botocore==1.16.26         # via boto3, s3transfer
 cachetools==3.1.0         # via google-auth
 cassandra-driver==3.23.0
 certifi==2020.6.20
@@ -118,9 +117,8 @@ dnspython==1.16.0
 docker-pycreds==0.4.0     # via docker
 docker==3.7.2
 docutils==0.15.2          # via botocore
-ecdsa==0.13.3             # via python-jose
 filelock==3.0.12          # via virtualenv
-future==0.17.1            # via python-jose, textfsm
+future==0.17.1            # via textfsm
 genshi==0.7.3
 geomet==0.1.2             # via cassandra-driver
 gitdb2==2.0.5             # via gitpython
@@ -136,8 +134,6 @@ isodate==0.6.0            # via msrest
 jaraco.functools==2.0
 jinja2==2.10.1
 jmespath==0.9.4
-jsondiff==1.1.1           # via moto
-jsonpickle==1.1           # via aws-xray-sdk
 jsonschema==2.6.0
 junos-eznc==2.4.0
 jxmlease==1.0.1
@@ -150,7 +146,6 @@ mako==1.1.0
 markupsafe==1.1.1
 mock==3.0.5
 more-itertools==5.0.0
-moto==1.3.7
 msgpack==1.0.0
 msrest==0.6.14            # via azure-applicationinsights, azure-eventgrid, azure-keyvault, azure-loganalytics, azure-mgmt-cdn, azure-mgmt-compute, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-dns, azure-mgmt-eventhub, azure-mgmt-keyvault, azure-mgmt-media, azure-mgmt-network, azure-mgmt-rdbms, azure-mgmt-resource, azure-mgmt-servicebus, azure-mgmt-servicefabric, azure-mgmt-signalr, azure-servicefabric, msrestazure
 msrestazure==0.6.3        # via azure-batch, azure-eventgrid, azure-graphrbac, azure-keyvault, azure-mgmt-advisor, azure-mgmt-applicationinsights, azure-mgmt-authorization, azure-mgmt-batch, azure-mgmt-batchai, azure-mgmt-billing, azure-mgmt-cdn, azure-mgmt-cognitiveservices, azure-mgmt-commerce, azure-mgmt-compute, azure-mgmt-consumption, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-cosmosdb, azure-mgmt-datafactory, azure-mgmt-datalake-analytics, azure-mgmt-datalake-store, azure-mgmt-datamigration, azure-mgmt-devspaces, azure-mgmt-devtestlabs, azure-mgmt-dns, azure-mgmt-eventgrid, azure-mgmt-eventhub, azure-mgmt-hanaonazure, azure-mgmt-iotcentral, azure-mgmt-iothub, azure-mgmt-iothubprovisioningservices, azure-mgmt-keyvault, azure-mgmt-loganalytics, azure-mgmt-logic, azure-mgmt-machinelearningcompute, azure-mgmt-managementgroups, azure-mgmt-managementpartner, azure-mgmt-maps, azure-mgmt-marketplaceordering, azure-mgmt-media, azure-mgmt-monitor, azure-mgmt-msi, azure-mgmt-network, azure-mgmt-notificationhubs, azure-mgmt-policyinsights, azure-mgmt-powerbiembedded, azure-mgmt-rdbms, azure-mgmt-recoveryservices, azure-mgmt-recoveryservicesbackup, azure-mgmt-redis, azure-mgmt-relay, azure-mgmt-reservations, azure-mgmt-resource, azure-mgmt-scheduler, azure-mgmt-search, azure-mgmt-servicebus, azure-mgmt-servicefabric, azure-mgmt-signalr, azure-mgmt-sql, azure-mgmt-storage, azure-mgmt-subscription, azure-mgmt-trafficmanager, azure-mgmt-web
@@ -160,7 +155,7 @@ ntc-templates==1.4.0      # via junos-eznc
 oauthlib==3.1.0           # via requests-oauthlib
 oscrypto==1.2.0           # via certvalidator
 packaging==19.2           # via pytest
-paramiko==2.7.1
+paramiko==2.7.1           # via junos-eznc, ncclient, scp
 passlib==1.7.2
 pathlib2==2.3.3           # via pytest
 pathtools==0.1.2          # via watchdog
@@ -168,11 +163,9 @@ pluggy==0.13.0            # via pytest
 portend==2.4
 psutil==5.6.7
 py==1.9.0                 # via pytest
-pyaml==19.4.1             # via moto
 pyasn1-modules==0.2.4     # via google-auth
 pyasn1==0.4.5             # via pyasn1-modules, rsa
 pycparser==2.17
-pycryptodome==3.8.1       # via python-jose
 pycryptodomex==3.9.8
 pycurl==7.43.0.6
 pygit2==0.28.2 ; python_version < "3.8"
@@ -191,14 +184,12 @@ pytest==6.1.1
 python-dateutil==2.8.1
 python-etcd==0.4.5
 python-gnupg==0.4.4
-python-jose==2.0.2        # via moto
 pytz==2020.1
 pyvmomi==6.7.1.2018.12
 pyyaml==5.3.1
 pyzmq==18.0.1 ; python_version < "3.9"
 requests-oauthlib==1.3.0  # via msrest
 requests==2.21.0
-responses==0.10.6         # via moto
 rfc3987==1.3.8
 rsa==4.0                  # via google-auth
 s3transfer==0.3.3         # via boto3
@@ -220,9 +211,6 @@ vcert==0.7.3
 virtualenv==20.0.20
 watchdog==0.9.0
 websocket-client==0.40.0  # via docker, kubernetes
-werkzeug==0.15.6          # via moto
-wrapt==1.11.1             # via aws-xray-sdk
-xmltodict==0.12.0         # via moto
 yamlordereddictloader==0.4.0  # via junos-eznc
 zc.lockfile==1.4
 zipp==0.6.0               # via importlib-metadata, importlib-resources
diff --git a/requirements/static/ci/py3.5/windows.txt b/requirements/static/ci/py3.5/windows.txt
index 3de8e54de0..203490f8a6 100644
--- a/requirements/static/ci/py3.5/windows.txt
+++ b/requirements/static/ci/py3.5/windows.txt
@@ -9,14 +9,14 @@ atomicwrites==1.3.0       # via pytest
 attrs==19.1.0             # via pytest
 aws-xray-sdk==0.95        # via moto
 backports.ssl-match-hostname==3.7.0.1 ; python_version < "3.7"
-boto3==1.13.5
+boto3==1.18.64
 boto==2.49.0
-botocore==1.16.26         # via boto3, moto, s3transfer
+botocore==1.21.64         # via boto3, moto, s3transfer
 cachetools==3.1.0         # via google-auth
 cassandra-driver==3.23.0
 certifi==2020.6.20
 cffi==1.12.2
-chardet==3.0.4
+charset-normalizer==2.0.7
 cheetah3==3.1.0
 cheroot==8.3.0
 cherrypy==17.4.1
@@ -30,7 +30,6 @@ dmidecode==0.9.0
 dnspython==1.16.0
 docker-pycreds==0.4.0     # via docker
 docker==2.7.0
-docutils==0.15.2          # via botocore
 ecdsa==0.13.3             # via python-jose
 filelock==3.0.12          # via virtualenv
 future==0.17.1            # via python-jose
@@ -93,14 +92,14 @@ python-jose==2.0.2        # via moto
 pythonnet==2.4.0
 pytz==2020.1
 pyvmomi==6.7.1.2018.12
-pywin32==227
+pywin32==301
 pyyaml==5.3.1
 pyzmq==18.0.1 ; python_version < "3.8"
-requests==2.21.0
+requests==2.26.0
 responses==0.10.6         # via moto
 rfc3987==1.3.8
 rsa==4.0                  # via google-auth
-s3transfer==0.3.3         # via boto3
+s3transfer==0.5.0         # via boto3
 sed==0.3.1
 setproctitle==1.1.10
 six==1.15.0
@@ -109,7 +108,7 @@ strict-rfc3339==0.7
 tempora==1.14.1
 timelib==0.2.5
 toml==0.10.0
-urllib3==1.24.3
+urllib3==1.26.7
 virtualenv==20.0.20
 watchdog==0.10.3
 websocket-client==0.40.0  # via docker, kubernetes
diff --git a/requirements/static/ci/py3.6/linux.txt b/requirements/static/ci/py3.6/linux.txt
index 8733ac1602..3e4286307e 100644
--- a/requirements/static/ci/py3.6/linux.txt
+++ b/requirements/static/ci/py3.6/linux.txt
@@ -92,7 +92,7 @@ azure-storage-blob==1.5.0  # via azure
 azure-storage-common==1.4.0  # via azure-cosmosdb-table, azure-storage-blob, azure-storage-file, azure-storage-queue
 azure-storage-file==1.4.0  # via azure
 azure-storage-queue==1.4.0  # via azure
-azure==4.0.0
+azure==4.0.0 ; sys_platform != "win32"
 backports.functools-lru-cache==1.5
 backports.ssl-match-hostname==3.7.0.1 ; python_version < "3.7"
 bcrypt==3.1.6             # via paramiko
@@ -152,7 +152,7 @@ mako==1.1.0
 markupsafe==1.1.1
 mock==3.0.5
 more-itertools==5.0.0
-moto==1.3.7
+moto==1.3.7 ; python_version >= "3.6"
 msgpack==1.0.0
 msrest==0.6.14            # via azure-applicationinsights, azure-eventgrid, azure-keyvault, azure-loganalytics, azure-mgmt-cdn, azure-mgmt-compute, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-dns, azure-mgmt-eventhub, azure-mgmt-keyvault, azure-mgmt-media, azure-mgmt-network, azure-mgmt-rdbms, azure-mgmt-resource, azure-mgmt-servicebus, azure-mgmt-servicefabric, azure-mgmt-signalr, azure-servicefabric, msrestazure
 msrestazure==0.6.3        # via azure-batch, azure-eventgrid, azure-graphrbac, azure-keyvault, azure-mgmt-advisor, azure-mgmt-applicationinsights, azure-mgmt-authorization, azure-mgmt-batch, azure-mgmt-batchai, azure-mgmt-billing, azure-mgmt-cdn, azure-mgmt-cognitiveservices, azure-mgmt-commerce, azure-mgmt-compute, azure-mgmt-consumption, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-cosmosdb, azure-mgmt-datafactory, azure-mgmt-datalake-analytics, azure-mgmt-datalake-store, azure-mgmt-datamigration, azure-mgmt-devspaces, azure-mgmt-devtestlabs, azure-mgmt-dns, azure-mgmt-eventgrid, azure-mgmt-eventhub, azure-mgmt-hanaonazure, azure-mgmt-iotcentral, azure-mgmt-iothub, azure-mgmt-iothubprovisioningservices, azure-mgmt-keyvault, azure-mgmt-loganalytics, azure-mgmt-logic, azure-mgmt-machinelearningcompute, azure-mgmt-managementgroups, azure-mgmt-managementpartner, azure-mgmt-maps, azure-mgmt-marketplaceordering, azure-mgmt-media, azure-mgmt-monitor, azure-mgmt-msi, azure-mgmt-network, azure-mgmt-notificationhubs, azure-mgmt-policyinsights, azure-mgmt-powerbiembedded, azure-mgmt-rdbms, azure-mgmt-recoveryservices, azure-mgmt-recoveryservicesbackup, azure-mgmt-redis, azure-mgmt-relay, azure-mgmt-reservations, azure-mgmt-resource, azure-mgmt-scheduler, azure-mgmt-search, azure-mgmt-servicebus, azure-mgmt-servicefabric, azure-mgmt-signalr, azure-mgmt-sql, azure-mgmt-storage, azure-mgmt-subscription, azure-mgmt-trafficmanager, azure-mgmt-web
@@ -164,7 +164,7 @@ ntc-templates==1.4.0      # via junos-eznc
 oauthlib==3.1.0           # via requests-oauthlib
 oscrypto==1.2.0           # via certvalidator
 packaging==19.2           # via pytest
-paramiko==2.7.1
+paramiko==2.7.1 ; python_version >= "3.6"
 passlib==1.7.2
 pathtools==0.1.2          # via watchdog
 pluggy==0.13.0            # via pytest
diff --git a/requirements/static/ci/py3.6/windows.txt b/requirements/static/ci/py3.6/windows.txt
index 325e6ec969..54a651d4a0 100644
--- a/requirements/static/ci/py3.6/windows.txt
+++ b/requirements/static/ci/py3.6/windows.txt
@@ -9,14 +9,14 @@ atomicwrites==1.3.0       # via pytest
 attrs==19.1.0             # via pytest
 aws-xray-sdk==0.95        # via moto
 backports.ssl-match-hostname==3.7.0.1 ; python_version < "3.7"
-boto3==1.13.5
+boto3==1.18.64
 boto==2.49.0
-botocore==1.16.26         # via boto3, moto, s3transfer
+botocore==1.21.64         # via boto3, moto, s3transfer
 cachetools==3.1.0         # via google-auth
 cassandra-driver==3.23.0
 certifi==2020.6.20
 cffi==1.12.2
-chardet==3.0.4
+charset-normalizer==2.0.7
 cheetah3==3.1.0
 cheroot==8.3.0
 cherrypy==17.4.1
@@ -30,7 +30,6 @@ dmidecode==0.9.0
 dnspython==1.16.0
 docker-pycreds==0.4.0     # via docker
 docker==2.7.0
-docutils==0.15.2          # via botocore
 ecdsa==0.13.3             # via python-jose
 filelock==3.0.12          # via virtualenv
 future==0.17.1            # via python-jose
@@ -92,14 +91,14 @@ python-jose==2.0.2        # via moto
 pythonnet==2.4.0
 pytz==2020.1
 pyvmomi==6.7.1.2018.12
-pywin32==227
+pywin32==302
 pyyaml==5.3.1
 pyzmq==18.0.1 ; python_version < "3.8"
-requests==2.21.0
+requests==2.26.0
 responses==0.10.6         # via moto
 rfc3987==1.3.8
 rsa==4.0                  # via google-auth
-s3transfer==0.3.3         # via boto3
+s3transfer==0.5.0         # via boto3
 sed==0.3.1
 setproctitle==1.1.10
 six==1.15.0
@@ -108,7 +107,7 @@ strict-rfc3339==0.7
 tempora==1.14.1
 timelib==0.2.5
 toml==0.10.0
-urllib3==1.24.3
+urllib3==1.26.7
 virtualenv==20.0.20
 watchdog==0.10.3
 websocket-client==0.40.0  # via docker, kubernetes
diff --git a/requirements/static/ci/py3.7/linux.txt b/requirements/static/ci/py3.7/linux.txt
index e8b0cdd804..783149b2eb 100644
--- a/requirements/static/ci/py3.7/linux.txt
+++ b/requirements/static/ci/py3.7/linux.txt
@@ -92,7 +92,7 @@ azure-storage-blob==1.5.0  # via azure
 azure-storage-common==1.4.0  # via azure-cosmosdb-table, azure-storage-blob, azure-storage-file, azure-storage-queue
 azure-storage-file==1.4.0  # via azure
 azure-storage-queue==1.4.0  # via azure
-azure==4.0.0
+azure==4.0.0 ; sys_platform != "win32"
 backports.functools-lru-cache==1.5
 bcrypt==3.1.6             # via paramiko
 boto3==1.13.5
@@ -150,7 +150,7 @@ mako==1.1.0
 markupsafe==1.1.1
 mock==3.0.5
 more-itertools==5.0.0
-moto==1.3.7
+moto==1.3.7 ; python_version >= "3.6"
 msgpack==1.0.0
 msrest==0.6.14            # via azure-applicationinsights, azure-eventgrid, azure-keyvault, azure-loganalytics, azure-mgmt-cdn, azure-mgmt-compute, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-dns, azure-mgmt-eventhub, azure-mgmt-keyvault, azure-mgmt-media, azure-mgmt-network, azure-mgmt-rdbms, azure-mgmt-resource, azure-mgmt-servicebus, azure-mgmt-servicefabric, azure-mgmt-signalr, azure-servicefabric, msrestazure
 msrestazure==0.6.3        # via azure-batch, azure-eventgrid, azure-graphrbac, azure-keyvault, azure-mgmt-advisor, azure-mgmt-applicationinsights, azure-mgmt-authorization, azure-mgmt-batch, azure-mgmt-batchai, azure-mgmt-billing, azure-mgmt-cdn, azure-mgmt-cognitiveservices, azure-mgmt-commerce, azure-mgmt-compute, azure-mgmt-consumption, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-cosmosdb, azure-mgmt-datafactory, azure-mgmt-datalake-analytics, azure-mgmt-datalake-store, azure-mgmt-datamigration, azure-mgmt-devspaces, azure-mgmt-devtestlabs, azure-mgmt-dns, azure-mgmt-eventgrid, azure-mgmt-eventhub, azure-mgmt-hanaonazure, azure-mgmt-iotcentral, azure-mgmt-iothub, azure-mgmt-iothubprovisioningservices, azure-mgmt-keyvault, azure-mgmt-loganalytics, azure-mgmt-logic, azure-mgmt-machinelearningcompute, azure-mgmt-managementgroups, azure-mgmt-managementpartner, azure-mgmt-maps, azure-mgmt-marketplaceordering, azure-mgmt-media, azure-mgmt-monitor, azure-mgmt-msi, azure-mgmt-network, azure-mgmt-notificationhubs, azure-mgmt-policyinsights, azure-mgmt-powerbiembedded, azure-mgmt-rdbms, azure-mgmt-recoveryservices, azure-mgmt-recoveryservicesbackup, azure-mgmt-redis, azure-mgmt-relay, azure-mgmt-reservations, azure-mgmt-resource, azure-mgmt-scheduler, azure-mgmt-search, azure-mgmt-servicebus, azure-mgmt-servicefabric, azure-mgmt-signalr, azure-mgmt-sql, azure-mgmt-storage, azure-mgmt-subscription, azure-mgmt-trafficmanager, azure-mgmt-web
@@ -162,7 +162,7 @@ ntc-templates==1.4.0      # via junos-eznc
 oauthlib==3.1.0           # via requests-oauthlib
 oscrypto==1.2.0           # via certvalidator
 packaging==19.2           # via pytest
-paramiko==2.7.1
+paramiko==2.7.1 ; python_version >= "3.6"
 passlib==1.7.2
 pathtools==0.1.2          # via watchdog
 pluggy==0.13.0            # via pytest
diff --git a/requirements/static/ci/py3.7/windows.txt b/requirements/static/ci/py3.7/windows.txt
index 53b5db2734..0b3736657d 100644
--- a/requirements/static/ci/py3.7/windows.txt
+++ b/requirements/static/ci/py3.7/windows.txt
@@ -8,14 +8,15 @@ appdirs==1.4.4            # via virtualenv
 atomicwrites==1.3.0       # via pytest
 attrs==19.1.0             # via pytest
 aws-xray-sdk==0.95        # via moto
-boto3==1.13.5
+backports.ssl-match-hostname==3.7.0.1  # via websocket-client
+boto3==1.18.64
 boto==2.49.0
-botocore==1.16.26         # via boto3, moto, s3transfer
+botocore==1.21.64         # via boto3, moto, s3transfer
 cachetools==3.1.0         # via google-auth
 cassandra-driver==3.23.0
 certifi==2020.6.20
 cffi==1.12.2
-chardet==3.0.4
+charset-normalizer==2.0.7
 cheetah3==3.1.0
 cheroot==8.3.0
 cherrypy==17.4.1
@@ -29,7 +30,6 @@ dmidecode==0.9.0
 dnspython==1.16.0
 docker-pycreds==0.4.0     # via docker
 docker==2.7.0
-docutils==0.15.2          # via botocore
 ecdsa==0.13.3             # via python-jose
 filelock==3.0.12          # via virtualenv
 future==0.17.1            # via python-jose
@@ -90,14 +90,14 @@ python-jose==2.0.2        # via moto
 pythonnet==2.4.0
 pytz==2020.1
 pyvmomi==6.7.1.2018.12
-pywin32==227
+pywin32==302
 pyyaml==5.3.1
 pyzmq==18.0.1 ; python_version < "3.8"
-requests==2.21.0
+requests==2.26.0
 responses==0.10.6         # via moto
 rfc3987==1.3.8
 rsa==4.0                  # via google-auth
-s3transfer==0.3.3         # via boto3
+s3transfer==0.5.0         # via boto3
 sed==0.3.1
 setproctitle==1.1.10
 six==1.15.0
@@ -106,7 +106,7 @@ strict-rfc3339==0.7
 tempora==1.14.1
 timelib==0.2.5
 toml==0.10.0
-urllib3==1.24.3
+urllib3==1.26.7
 virtualenv==20.0.20
 watchdog==0.10.3
 websocket-client==0.40.0  # via docker, kubernetes
diff --git a/requirements/static/ci/py3.8/linux.txt b/requirements/static/ci/py3.8/linux.txt
index 75e3d72b36..f5c9f62293 100644
--- a/requirements/static/ci/py3.8/linux.txt
+++ b/requirements/static/ci/py3.8/linux.txt
@@ -92,7 +92,7 @@ azure-storage-blob==1.5.0  # via azure
 azure-storage-common==1.4.0  # via azure-cosmosdb-table, azure-storage-blob, azure-storage-file, azure-storage-queue
 azure-storage-file==1.4.0  # via azure
 azure-storage-queue==1.4.0  # via azure
-azure==4.0.0
+azure==4.0.0 ; sys_platform != "win32"
 backports.functools-lru-cache==1.5
 bcrypt==3.1.6             # via paramiko
 boto3==1.13.5
@@ -150,7 +150,7 @@ mako==1.1.0
 markupsafe==1.1.1
 mock==3.0.5
 more-itertools==5.0.0
-moto==1.3.7
+moto==1.3.7 ; python_version >= "3.6"
 msgpack==1.0.0
 msrest==0.6.14            # via azure-applicationinsights, azure-eventgrid, azure-keyvault, azure-loganalytics, azure-mgmt-cdn, azure-mgmt-compute, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-dns, azure-mgmt-eventhub, azure-mgmt-keyvault, azure-mgmt-media, azure-mgmt-network, azure-mgmt-rdbms, azure-mgmt-resource, azure-mgmt-servicebus, azure-mgmt-servicefabric, azure-mgmt-signalr, azure-servicefabric, msrestazure
 msrestazure==0.6.3        # via azure-batch, azure-eventgrid, azure-graphrbac, azure-keyvault, azure-mgmt-advisor, azure-mgmt-applicationinsights, azure-mgmt-authorization, azure-mgmt-batch, azure-mgmt-batchai, azure-mgmt-billing, azure-mgmt-cdn, azure-mgmt-cognitiveservices, azure-mgmt-commerce, azure-mgmt-compute, azure-mgmt-consumption, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-cosmosdb, azure-mgmt-datafactory, azure-mgmt-datalake-analytics, azure-mgmt-datalake-store, azure-mgmt-datamigration, azure-mgmt-devspaces, azure-mgmt-devtestlabs, azure-mgmt-dns, azure-mgmt-eventgrid, azure-mgmt-eventhub, azure-mgmt-hanaonazure, azure-mgmt-iotcentral, azure-mgmt-iothub, azure-mgmt-iothubprovisioningservices, azure-mgmt-keyvault, azure-mgmt-loganalytics, azure-mgmt-logic, azure-mgmt-machinelearningcompute, azure-mgmt-managementgroups, azure-mgmt-managementpartner, azure-mgmt-maps, azure-mgmt-marketplaceordering, azure-mgmt-media, azure-mgmt-monitor, azure-mgmt-msi, azure-mgmt-network, azure-mgmt-notificationhubs, azure-mgmt-policyinsights, azure-mgmt-powerbiembedded, azure-mgmt-rdbms, azure-mgmt-recoveryservices, azure-mgmt-recoveryservicesbackup, azure-mgmt-redis, azure-mgmt-relay, azure-mgmt-reservations, azure-mgmt-resource, azure-mgmt-scheduler, azure-mgmt-search, azure-mgmt-servicebus, azure-mgmt-servicefabric, azure-mgmt-signalr, azure-mgmt-sql, azure-mgmt-storage, azure-mgmt-subscription, azure-mgmt-trafficmanager, azure-mgmt-web
@@ -162,7 +162,7 @@ ntc-templates==1.4.1      # via junos-eznc
 oauthlib==3.1.0           # via requests-oauthlib
 oscrypto==1.2.0           # via certvalidator
 packaging==19.2           # via pytest
-paramiko==2.7.1
+paramiko==2.7.1 ; python_version >= "3.6"
 passlib==1.7.2
 pathtools==0.1.2          # via watchdog
 pluggy==0.13.0            # via pytest
diff --git a/requirements/static/ci/py3.9/linux.txt b/requirements/static/ci/py3.9/linux.txt
index fe95d02273..2dc81874e1 100644
--- a/requirements/static/ci/py3.9/linux.txt
+++ b/requirements/static/ci/py3.9/linux.txt
@@ -92,7 +92,7 @@ azure-storage-blob==1.5.0  # via azure
 azure-storage-common==1.4.0  # via azure-cosmosdb-table, azure-storage-blob, azure-storage-file, azure-storage-queue
 azure-storage-file==1.4.0  # via azure
 azure-storage-queue==1.4.0  # via azure
-azure==4.0.0
+azure==4.0.0 ; sys_platform != "win32"
 backports.functools-lru-cache==1.5
 bcrypt==3.1.6             # via paramiko
 boto3==1.13.5
@@ -150,7 +150,7 @@ mako==1.1.0
 markupsafe==1.1.1
 mock==3.0.5
 more-itertools==5.0.0
-moto==1.3.7
+moto==1.3.7 ; python_version >= "3.6"
 msgpack==1.0.0
 msrest==0.6.14            # via azure-applicationinsights, azure-eventgrid, azure-keyvault, azure-loganalytics, azure-mgmt-cdn, azure-mgmt-compute, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-dns, azure-mgmt-eventhub, azure-mgmt-keyvault, azure-mgmt-media, azure-mgmt-network, azure-mgmt-rdbms, azure-mgmt-resource, azure-mgmt-servicebus, azure-mgmt-servicefabric, azure-mgmt-signalr, azure-servicefabric, msrestazure
 msrestazure==0.6.3        # via azure-batch, azure-eventgrid, azure-graphrbac, azure-keyvault, azure-mgmt-advisor, azure-mgmt-applicationinsights, azure-mgmt-authorization, azure-mgmt-batch, azure-mgmt-batchai, azure-mgmt-billing, azure-mgmt-cdn, azure-mgmt-cognitiveservices, azure-mgmt-commerce, azure-mgmt-compute, azure-mgmt-consumption, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-cosmosdb, azure-mgmt-datafactory, azure-mgmt-datalake-analytics, azure-mgmt-datalake-store, azure-mgmt-datamigration, azure-mgmt-devspaces, azure-mgmt-devtestlabs, azure-mgmt-dns, azure-mgmt-eventgrid, azure-mgmt-eventhub, azure-mgmt-hanaonazure, azure-mgmt-iotcentral, azure-mgmt-iothub, azure-mgmt-iothubprovisioningservices, azure-mgmt-keyvault, azure-mgmt-loganalytics, azure-mgmt-logic, azure-mgmt-machinelearningcompute, azure-mgmt-managementgroups, azure-mgmt-managementpartner, azure-mgmt-maps, azure-mgmt-marketplaceordering, azure-mgmt-media, azure-mgmt-monitor, azure-mgmt-msi, azure-mgmt-network, azure-mgmt-notificationhubs, azure-mgmt-policyinsights, azure-mgmt-powerbiembedded, azure-mgmt-rdbms, azure-mgmt-recoveryservices, azure-mgmt-recoveryservicesbackup, azure-mgmt-redis, azure-mgmt-relay, azure-mgmt-reservations, azure-mgmt-resource, azure-mgmt-scheduler, azure-mgmt-search, azure-mgmt-servicebus, azure-mgmt-servicefabric, azure-mgmt-signalr, azure-mgmt-sql, azure-mgmt-storage, azure-mgmt-subscription, azure-mgmt-trafficmanager, azure-mgmt-web
@@ -162,7 +162,7 @@ ntc-templates==1.4.1      # via junos-eznc
 oauthlib==3.1.0           # via requests-oauthlib
 oscrypto==1.2.0           # via certvalidator
 packaging==19.2           # via pytest
-paramiko==2.7.1
+paramiko==2.7.1 ; python_version >= "3.6"
 passlib==1.7.2
 pathtools==0.1.2          # via watchdog
 pluggy==0.13.0            # via pytest
diff --git a/requirements/static/ci/windows.in b/requirements/static/ci/windows.in
index 6a4eadd093..4c2e67e435 100644
--- a/requirements/static/ci/windows.in
+++ b/requirements/static/ci/windows.in
@@ -1,6 +1,6 @@
 # This is a compilation of requirements installed on salt-jenkins git.salt state run
 #apache-libcloud==2.0.0
-boto3>=1.13.5
+boto3>=1.15.0
 boto>=2.46.0
 dmidecode
 dnspython
diff --git a/requirements/static/pkg/py3.5/windows.txt b/requirements/static/pkg/py3.5/windows.txt
index 5bc3f8aa2a..2234851fae 100644
--- a/requirements/static/pkg/py3.5/windows.txt
+++ b/requirements/static/pkg/py3.5/windows.txt
@@ -7,7 +7,7 @@
 backports.ssl-match-hostname==3.7.0.1 ; python_version < "3.7"
 certifi==2020.6.20
 cffi==1.12.2
-chardet==3.0.4            # via requests
+charset-normalizer==2.0.7  # via requests
 cheroot==8.3.0            # via cherrypy
 cherrypy==17.4.1
 contextlib2==0.6.0.post1  # via cherrypy
@@ -39,16 +39,16 @@ python-dateutil==2.8.0
 python-gnupg==0.4.4
 pythonnet==2.4.0
 pytz==2020.1              # via tempora
-pywin32==227
+pywin32==301
 pyyaml==5.3.1
 pyzmq==18.0.1 ; python_version < "3.8"
-requests==2.21.0
+requests==2.26.0
 setproctitle==1.1.10
 six==1.15.0               # via cheroot, cherrypy, cryptography, pyopenssl, python-dateutil, tempora
 smmap2==2.0.5             # via gitdb2
 tempora==1.14.1           # via portend
 timelib==0.2.5
-urllib3==1.24.3           # via requests
+urllib3==1.26.7
 wheel==0.33.4
 wmi==1.4.9
 zc.lockfile==2.0          # via cherrypy
diff --git a/requirements/static/pkg/py3.6/windows.txt b/requirements/static/pkg/py3.6/windows.txt
index c72607f9c5..725b72d27e 100644
--- a/requirements/static/pkg/py3.6/windows.txt
+++ b/requirements/static/pkg/py3.6/windows.txt
@@ -7,7 +7,7 @@
 backports.ssl-match-hostname==3.7.0.1 ; python_version < "3.7"
 certifi==2020.6.20
 cffi==1.12.2
-chardet==3.0.4            # via requests
+charset-normalizer==2.0.7  # via requests
 cheroot==8.3.0            # via cherrypy
 cherrypy==17.4.1
 contextlib2==0.6.0.post1  # via cherrypy
@@ -39,16 +39,16 @@ python-dateutil==2.8.0
 python-gnupg==0.4.4
 pythonnet==2.4.0
 pytz==2020.1              # via tempora
-pywin32==227
+pywin32==302
 pyyaml==5.3.1
 pyzmq==18.0.1 ; python_version < "3.8"
-requests==2.21.0
+requests==2.26.0
 setproctitle==1.1.10
 six==1.15.0               # via cheroot, cherrypy, cryptography, pyopenssl, python-dateutil, tempora
 smmap2==2.0.5             # via gitdb2
 tempora==1.14.1           # via portend
 timelib==0.2.5
-urllib3==1.24.3           # via requests
+urllib3==1.26.7
 wheel==0.33.4
 wmi==1.4.9
 zc.lockfile==2.0          # via cherrypy
diff --git a/requirements/static/pkg/py3.7/windows.txt b/requirements/static/pkg/py3.7/windows.txt
index de6e075617..515f9734a3 100644
--- a/requirements/static/pkg/py3.7/windows.txt
+++ b/requirements/static/pkg/py3.7/windows.txt
@@ -6,7 +6,7 @@
 #
 certifi==2020.6.20
 cffi==1.12.2
-chardet==3.0.4            # via requests
+charset-normalizer==2.0.7  # via requests
 cheroot==8.3.0            # via cherrypy
 cherrypy==17.4.1
 contextlib2==0.6.0.post1  # via cherrypy
@@ -38,16 +38,16 @@ python-dateutil==2.8.0
 python-gnupg==0.4.4
 pythonnet==2.4.0
 pytz==2020.1              # via tempora
-pywin32==227
+pywin32==302
 pyyaml==5.3.1
 pyzmq==18.0.1 ; python_version < "3.8"
-requests==2.21.0
+requests==2.26.0
 setproctitle==1.1.10
 six==1.15.0               # via cheroot, cherrypy, cryptography, pyopenssl, python-dateutil, tempora
 smmap2==2.0.5             # via gitdb2
 tempora==1.14.1           # via portend
 timelib==0.2.5
-urllib3==1.24.3           # via requests
+urllib3==1.26.7
 wheel==0.33.4
 wmi==1.4.9
 zc.lockfile==2.0          # via cherrypy
diff --git a/requirements/windows.txt b/requirements/windows.txt
index 1e31ad2b88..3186140677 100644
--- a/requirements/windows.txt
+++ b/requirements/windows.txt
@@ -2,7 +2,7 @@
 
 -r zeromq.txt
 
-pywin32>=227
+pywin32>=301
 wmi>=1.4.9
 pythonnet>=2.4.0
 
@@ -30,8 +30,10 @@ python-dateutil>=2.8.0
 python-gnupg>=0.4.4
 pyzmq==18.0.1 ; python_version < "3.8"
 pyzmq==19.0.0 ; python_version >= "3.8"
+requests>=2.25.1
 setproctitle
 timelib>=0.2.4
+urllib3>=1.26.5
 # Watchdog pulls in a GPL-3 package, argh, which cannot be shipped on the
 # windows distribution package.
 #
diff --git a/salt/crypt.py b/salt/crypt.py
index e6e4f3181e..c666f166e1 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
@@ -259,7 +260,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(
@@ -694,9 +699,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
@@ -727,7 +740,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:
@@ -739,10 +751,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
@@ -766,59 +774,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 = self.serial.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)
@@ -840,8 +874,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):
         """
@@ -889,6 +924,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"]:
@@ -1252,6 +1288,7 @@ class SAuth(AsyncAuth):
         self.serial = salt.payload.Serial(self.opts)
         self.pub_path = os.path.join(self.opts["pki_dir"], "minion.pub")
         self.rsa_path = os.path.join(self.opts["pki_dir"], "minion.pem")
+        self._creds = None
         if "syndic_master" in self.opts:
             self.mpub = "syndic_master.pub"
         elif "alert_master" in self.opts:
@@ -1321,8 +1358,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):
         """
@@ -1375,78 +1418,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:
@@ -1461,11 +1433,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 = salt.payload.Serial(opts)
+        self.serial = serial
 
     @classmethod
     def generate_key_string(cls, key_size=192):
@@ -1535,13 +1507,19 @@ 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 + self.serial.dumps(obj))
+        if nonce:
+            toencrypt = (
+                self.PICKLE_PAD + nonce.encode() + salt.payload.Serial({}).dumps(obj)
+            )
+        else:
+            toencrypt = self.PICKLE_PAD + salt.payload.Serial({}).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
         """
@@ -1549,5 +1527,25 @@ class Crypticle:
         # simple integrity check to verify that we got meaningful data
         if not data.startswith(self.PICKLE_PAD):
             return {}
-        load = self.serial.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.Serial({}).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 fc103ac489..9c2030c712 100644
--- a/salt/master.py
+++ b/salt/master.py
@@ -143,6 +143,51 @@ class SMaster:
         """
         return salt.daemons.masterapi.access_keys(self.opts)
 
+    @classmethod
+    def get_serial(cls, opts=None, event=None, lock=True):
+        if lock:
+            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
+        else:
+            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):
     """
@@ -313,21 +358,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):
         """
@@ -713,8 +745,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 6bfac076eb..41d42c5ce1 100644
--- a/salt/minion.py
+++ b/salt/minion.py
@@ -1664,6 +1664,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 six.PY2:
             data = salt.utils.data.decode(data, keep=True)
diff --git a/salt/pillar/__init__.py b/salt/pillar/__init__.py
index 57f4b4d367..826461d295 100644
--- a/salt/pillar/__init__.py
+++ b/salt/pillar/__init__.py
@@ -11,6 +11,7 @@ import logging
 import os
 import sys
 import traceback
+import uuid
 
 import salt.ext.tornado.gen
 import salt.fileclient
@@ -242,6 +243,9 @@ class AsyncRemotePillar(RemotePillarMixin):
             ret_pillar = yield self.channel.crypted_transfer_decode_dictentry(
                 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 0f0c615408..c84d8a4b94 100644
--- a/salt/transport/mixins/auth.py
+++ b/salt/transport/mixins/auth.py
@@ -122,7 +122,7 @@ class AESReqServerMixin(object):
 
         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
         """
@@ -137,7 +137,6 @@ class AESReqServerMixin(object):
         except IOError:
             log.error("AES key not found")
             return {"error": "AES key not found"}
-
         pret = {}
         if not six.PY2:
             key = salt.utils.stringutils.to_bytes(key)
@@ -146,9 +145,33 @@ class AESReqServerMixin(object):
         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.Serial({}).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.Serial({}).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
@@ -175,7 +198,7 @@ class AESReqServerMixin(object):
                 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.
@@ -193,7 +216,10 @@ class AESReqServerMixin(object):
 
         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'
@@ -231,7 +257,12 @@ class AESReqServerMixin(object):
                         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"])
@@ -258,8 +289,10 @@ class AESReqServerMixin(object):
             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:
@@ -283,7 +316,12 @@ class AESReqServerMixin(object):
                         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
@@ -293,7 +331,10 @@ class AESReqServerMixin(object):
                 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
@@ -316,7 +357,6 @@ class AESReqServerMixin(object):
                 # 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,
@@ -325,7 +365,12 @@ class AESReqServerMixin(object):
                 }
                 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
@@ -341,7 +386,6 @@ class AESReqServerMixin(object):
                     "Pending public key for %s rejected via " "autoreject_file",
                     load["id"],
                 )
-                ret = {"enc": "clear", "load": {"ret": False}}
                 eload = {
                     "result": False,
                     "act": "reject",
@@ -350,7 +394,10 @@ class AESReqServerMixin(object):
                 }
                 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.
@@ -378,7 +425,12 @@ class AESReqServerMixin(object):
                             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 "
@@ -397,7 +449,12 @@ class AESReqServerMixin(object):
                             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
@@ -419,7 +476,12 @@ class AESReqServerMixin(object):
                             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)
 
@@ -429,7 +491,10 @@ class AESReqServerMixin(object):
             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,
@@ -447,8 +512,11 @@ class AESReqServerMixin(object):
                 with salt.utils.files.fopen(pubfn, "w+") as fp_:
                     fp_.write(load["pub"])
             elif not load["pub"]:
-                log.error("Public key is empty: {0}".format(load["id"]))
-                return {"enc": "clear", "load": {"ret": False}}
+                log.error("Public key is empty: %s", load["id"])
+                if sign_messages:
+                    return self._clear_signed({"ret": False, "nonce": load["nonce"]})
+                else:
+                    return {"enc": "clear", "load": {"ret": False}}
 
         pub = None
 
@@ -462,7 +530,10 @@ class AESReqServerMixin(object):
             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)
@@ -543,10 +614,14 @@ class AESReqServerMixin(object):
                 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 80e6141c33..06280aa20c 100644
--- a/salt/transport/tcp.py
+++ b/salt/transport/tcp.py
@@ -17,6 +17,7 @@ import sys
 import threading
 import time
 import traceback
+import uuid
 import weakref
 
 # Import Salt Libs
@@ -373,12 +374,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(
@@ -390,11 +394,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])
-        if six.PY3:
-            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.Serial({}).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):
@@ -404,6 +426,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():
@@ -415,9 +440,8 @@ 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)
-                if six.PY3:
-                    data = salt.transport.frame.decode_embedded_strs(data)
+                data = self.auth.crypticle.loads(data, nonce=nonce)
+                data = salt.transport.frame.decode_embedded_strs(data)
             raise salt.ext.tornado.gen.Return(data)
 
         if not self.auth.authenticated:
@@ -493,6 +517,7 @@ class AsyncTCPPubChannel(
         return {
             "enc": self.crypt,
             "load": load,
+            "version": 2,
         }
 
     @salt.ext.tornado.gen.coroutine
@@ -790,6 +815,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 (
@@ -798,11 +831,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)
@@ -821,13 +858,15 @@ 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":
                 stream.write(
                     salt.transport.frame.frame_msg(
-                        self._encrypt_private(ret, req_opts["key"], req_opts["tgt"],),
+                        self._encrypt_private(
+                            ret, req_opts["key"], req_opts["tgt"], nonce, sign_messages,
+                        ),
                         header=header,
                     )
                 )
@@ -1418,8 +1457,8 @@ class PubServer(salt.ext.tornado.tcpserver.TCPServer, object):
     TCP publisher
     """
 
-    def __init__(self, opts, io_loop=None):
-        super(PubServer, self).__init__(ssl_options=opts.get("ssl"))
+    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
         self._closing = False
@@ -1442,6 +1481,10 @@ class PubServer(salt.ext.tornado.tcpserver.TCPServer, object):
             self.event = salt.utils.event.get_event(
                 "master", opts=self.opts, listen=False
             )
+        self._pack_publish = pack_publish
+
+    def pack_publish(self, payload):
+        return self._pack_publish(payload)
 
     def close(self):
         if self._closing:
@@ -1548,6 +1591,7 @@ class PubServer(salt.ext.tornado.tcpserver.TCPServer, object):
     @salt.ext.tornado.gen.coroutine
     def publish_payload(self, package, _):
         log.debug("TCP PubServer sending payload: %s", package)
+        package = self.pack_publish(package)
         payload = salt.transport.frame.frame_msg(package["payload"])
 
         to_remove = []
@@ -1624,7 +1668,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)
@@ -1663,12 +1709,12 @@ class TCPPubServerChannel(salt.transport.server.PubServerChannel):
         """
         process_manager.add_process(self._publish_daemon, kwargs=kwargs)
 
-    def publish(self, load):
+    def pack_publish(self, load):
         """
         Publish "load" to minions
         """
         payload = {"enc": "aes"}
-
+        load["serial"] = salt.master.SMaster.get_serial()
         crypticle = salt.crypt.Crypticle(
             self.opts, salt.master.SMaster.secrets["aes"]["secret"].value
         )
@@ -1677,17 +1723,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": self.serial.dumps(payload)}
 
@@ -1705,5 +1740,22 @@ 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)
+        # Use the Salt IPC server
+        # TODO: switch to the actual asynchronous interface
+        # pub_sock = salt.transport.ipc.IPCMessageClient(self.opts, io_loop=self.io_loop)
+        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 22a5e754c2..7d116be31b 100644
--- a/salt/transport/zeromq.py
+++ b/salt/transport/zeromq.py
@@ -11,6 +11,7 @@ import os
 import signal
 import sys
 import threading
+import uuid
 import weakref
 from random import randint
 
@@ -66,6 +67,7 @@ except ImportError:
     except ImportError:
         from Crypto.Cipher import PKCS1_OAEP
 
+
 log = logging.getLogger(__name__)
 
 
@@ -77,12 +79,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
@@ -340,22 +342,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()
@@ -364,15 +371,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.Serial({}).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):
@@ -389,6 +417,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():
@@ -403,7 +434,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 six.PY3 and not raw:
                 data = salt.transport.frame.decode_embedded_strs(data)
             raise salt.ext.tornado.gen.Return(data)
@@ -859,12 +890,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(self.serial.dumps(self._auth(payload["load"])))
+            stream.send(self.serial.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
@@ -880,11 +923,13 @@ class ZeroMQReqServerChannel(
         if req_fun == "send_clear":
             stream.send(self.serial.dumps(ret))
         elif req_fun == "send":
-            stream.send(self.serial.dumps(self.crypticle.dumps(ret)))
+            stream.send(self.serial.dumps(self.crypticle.dumps(ret, nonce)))
         elif req_fun == "send_private":
             stream.send(
                 self.serial.dumps(
-                    self._encrypt_private(ret, req_opts["key"], req_opts["tgt"],)
+                    self._encrypt_private(
+                        ret, req_opts["key"], req_opts["tgt"], nonce, sign_messages,
+                    )
                 )
             )
         else:
@@ -1016,6 +1061,8 @@ class ZeroMQPubServerChannel(salt.transport.server.PubServerChannel):
                     log.debug("Publish daemon getting data from puller %s", pull_uri)
                     package = pull_sock.recv()
                     log.debug("Publish daemon received payload. size=%d", len(package))
+                    load = salt.payload.Serial({}).loads(package)
+                    package = self.pack_publish(load)
 
                     unpacked_package = salt.payload.unpackage(package)
                     unpacked_package = salt.transport.frame.decode_embedded_strs(
@@ -1109,7 +1156,10 @@ class ZeroMQPubServerChannel(salt.transport.server.PubServerChannel):
             self.pub_close()
         ctx = zmq.Context.instance()
         self._sock_data.sock = ctx.socket(zmq.PUSH)
-        self.pub_sock.setsockopt(zmq.LINGER, -1)
+        self._sock_data.sock.setsockopt(zmq.LINGER, -1)
+        self._sock_data.sock.setsockopt(zmq.SNDHWM, self.opts.get("pub_hwm", 1000))
+        self._sock_data.sock.setsockopt(zmq.RCVHWM, self.opts.get("pub_hwm", 1000))
+        self._sock_data.sock.setsockopt(zmq.BACKLOG, self.opts.get("zmq_backlog", 1000))
         if self.opts.get("ipc_mode", "") == "tcp":
             pull_uri = "tcp://127.0.0.1:{}".format(
                 self.opts.get("tcp_master_publish_pull", 4514)
@@ -1119,7 +1169,7 @@ class ZeroMQPubServerChannel(salt.transport.server.PubServerChannel):
                 os.path.join(self.opts["sock_dir"], "publish_pull.ipc")
             )
         log.debug("Connecting to pub server: %s", pull_uri)
-        self.pub_sock.connect(pull_uri)
+        self._sock_data.sock.connect(pull_uri)
         return self._sock_data.sock
 
     def pub_close(self):
@@ -1129,16 +1179,17 @@ class ZeroMQPubServerChannel(salt.transport.server.PubServerChannel):
         """
         if hasattr(self._sock_data, "sock"):
             self._sock_data.sock.close()
-            delattr(self._sock_data, "sock")
+            self._sock_data.sock = None
 
-    def publish(self, load):
+    def pack_publish(self, load):
         """
-        Publish "load" to minions. This send the load to the publisher daemon
-        process with does the actual sending to minions.
+        Package the "load" for a publish 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
         """
         payload = {"enc": "aes"}
+        load["serial"] = salt.master.SMaster.get_serial()
         crypticle = salt.crypt.Crypticle(
             self.opts, salt.master.SMaster.secrets["aes"]["secret"].value
         )
@@ -1169,9 +1220,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(self.serial.dumps(load))
         log.debug("Sent payload to publish daemon.")
 
 
diff --git a/salt/utils/minions.py b/salt/utils/minions.py
index 216baf5b6b..e7486ab704 100644
--- a/salt/utils/minions.py
+++ b/salt/utils/minions.py
@@ -733,20 +733,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/tests/pytests/conftest.py b/tests/pytests/conftest.py
index 6a70545e08..94ff0a50fa 100644
--- a/tests/pytests/conftest.py
+++ b/tests/pytests/conftest.py
@@ -2,6 +2,8 @@
     tests.pytests.conftest
     ~~~~~~~~~~~~~~~~~~~~~~
 """
+import functools
+import inspect
 import logging
 import os
 import shutil
@@ -225,3 +227,78 @@ def bridge_pytest_and_runtests():
     """
     We're basically overriding the same fixture defined in tests/conftest.py
     """
+
+
+# ----- Async Test Fixtures ----------------------------------------------------------------------------------------->
+# This is based on https://github.com/eukaryote/pytest-tornasync
+# The reason why we don't use that pytest plugin instead is because it has
+# tornado as a dependency, and we need to use the tornado we ship with salt
+
+
+def get_test_timeout(pyfuncitem):
+    default_timeout = 30
+    marker = pyfuncitem.get_closest_marker("timeout")
+    if marker:
+        return marker.kwargs.get("seconds") or default_timeout
+    return default_timeout
+
+
+@pytest.mark.tryfirst
+def pytest_pycollect_makeitem(collector, name, obj):
+    if collector.funcnamefilter(name) and inspect.iscoroutinefunction(obj):
+        return list(collector._genfunctions(name, obj))
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_runtest_setup(item):
+    if inspect.iscoroutinefunction(item.obj):
+        if "io_loop" not in item.fixturenames:
+            # Append the io_loop fixture for the async functions
+            item.fixturenames.append("io_loop")
+
+
+class CoroTestFunction:
+    def __init__(self, func, kwargs):
+        self.func = func
+        self.kwargs = kwargs
+        functools.update_wrapper(self, func)
+
+    async def __call__(self):
+        ret = await self.func(**self.kwargs)
+        return ret
+
+
+@pytest.mark.tryfirst
+def pytest_pyfunc_call(pyfuncitem):
+    if not inspect.iscoroutinefunction(pyfuncitem.obj):
+        return
+
+    funcargs = pyfuncitem.funcargs
+    testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
+
+    try:
+        loop = funcargs["io_loop"]
+    except KeyError:
+        loop = salt.ext.tornado.ioloop.IOLoop.current()
+
+    loop.run_sync(
+        CoroTestFunction(pyfuncitem.obj, testargs), timeout=get_test_timeout(pyfuncitem)
+    )
+    return True
+
+
+@pytest.fixture
+def io_loop():
+    """
+    Create new io loop for each test, and tear it down after.
+    """
+    loop = salt.ext.tornado.ioloop.IOLoop()
+    loop.make_current()
+    try:
+        yield loop
+    finally:
+        loop.clear_current()
+        loop.close(all_fds=True)
+
+
+# <---- Async Test Fixtures ------------------------------------------------------------------------------------------
diff --git a/tests/pytests/unit/test_crypt.py b/tests/pytests/unit/test_crypt.py
index aa8f439b8c..6ffd912166 100644
--- a/tests/pytests/unit/test_crypt.py
+++ b/tests/pytests/unit/test_crypt.py
@@ -4,10 +4,159 @@ 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_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.Serial({}).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.Serial({}).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)
 
 def test_get_rsa_pub_key_bad_key(tmp_path):
     """
diff --git a/tests/pytests/unit/transport/test_zeromq.py b/tests/pytests/unit/transport/test_zeromq.py
new file mode 100644
index 0000000000..16c8f1aa49
--- /dev/null
+++ b/tests/pytests/unit/transport/test_zeromq.py
@@ -0,0 +1,1042 @@
+"""
+    :codeauthor: Thomas Jackson <jacksontj.89@gmail.com>
+"""
+
+import ctypes
+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
+import salt.log.setup
+import salt.transport.client
+import salt.transport.server
+import salt.transport.zeromq
+import salt.utils.platform
+import salt.utils.process
+import salt.utils.stringutils
+from salt.master import SMaster
+from tests.support.mock import MagicMock
+
+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())
+
+    for sdir in [
+        "minions_autosign",
+        "minions_denied",
+        "minions_pre",
+        "minions_rejected",
+    ]:
+        madir.mkdir(sdir)
+
+    mipub = madir.mkdir("minions").join("minion")
+    mipub.write(MINION_PUB_KEY.strip())
+
+    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_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.Serial({}).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.Serial({}).loads(signed_msg["data"])
+        data["pillar"] = {"pillar1": "bar"}
+        signed_msg["data"] = salt.payload.Serial({}).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 5b0cd77216..23ea8712da 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
@@ -21,3 +22,61 @@ def test_connected_ids():
     with patch_net, patch_ip_addrs, patch_list, patch_fetch:
         ret = ckminions.connected_ids()
         assert ret == {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/unit/transport/mixins.py b/tests/unit/transport/mixins.py
index 4e41dd9422..53b22efc81 100644
--- a/tests/unit/transport/mixins.py
+++ b/tests/unit/transport/mixins.py
@@ -1,16 +1,6 @@
-# -*- coding: utf-8 -*-
-
-# Import Python libs
-from __future__ import absolute_import, print_function, unicode_literals
-
 import salt.ext.tornado.gen
-
-# Import Salt Libs
 import salt.transport.client
 
-# Import 3rd-party libs
-from salt.ext import six
-
 
 def run_loop_in_thread(loop, evt):
     """
@@ -33,7 +23,7 @@ def run_loop_in_thread(loop, evt):
         loop.close()
 
 
-class ReqChannelMixin(object):
+class ReqChannelMixin:
     def test_basic(self):
         """
         Test a variety of messages, make sure we get the expected responses
@@ -44,7 +34,7 @@ class ReqChannelMixin(object):
             {"baz": "qux", "list": [1, 2, 3]},
         ]
         for msg in msgs:
-            ret = self.channel.send(msg, timeout=2, tries=1)
+            ret = self.channel.send(dict(msg), timeout=2, tries=1)
             self.assertEqual(ret["load"], msg)
 
     def test_normalization(self):
@@ -59,7 +49,7 @@ class ReqChannelMixin(object):
         ]
         for msg in msgs:
             ret = self.channel.send(msg, timeout=2, tries=1)
-            for k, v in six.iteritems(ret["load"]):
+            for k, v in ret["load"].items():
                 self.assertEqual(types[k], type(v))
 
     def test_badload(self):
@@ -72,7 +62,7 @@ class ReqChannelMixin(object):
             self.assertEqual(ret, "payload and load must be a dict")
 
 
-class PubChannelMixin(object):
+class PubChannelMixin:
     def test_basic(self):
         self.pub = None
 
diff --git a/tests/unit/transport/test_tcp.py b/tests/unit/transport/test_tcp.py
index 5611d13d4f..94e62a06c3 100644
--- a/tests/unit/transport/test_tcp.py
+++ b/tests/unit/transport/test_tcp.py
@@ -1,8 +1,6 @@
-# -*- coding: utf-8 -*-
 """
     :codeauthor: Thomas Jackson <jacksontj.89@gmail.com>
 """
-from __future__ import absolute_import, print_function, unicode_literals
 
 import logging
 import socket
@@ -17,8 +15,6 @@ import salt.transport.client
 import salt.transport.server
 import salt.utils.platform
 import salt.utils.process
-from salt.ext import six
-from salt.ext.six.moves import range
 from salt.ext.tornado.testing import AsyncTestCase, gen_test
 from salt.transport.tcp import (
     SaltMessageClient,
@@ -74,7 +70,7 @@ class BaseTCPReqCase(TestCase, AdaptedConfigurationTestCaseMixin):
                 "transport": "tcp",
                 "master_ip": "127.0.0.1",
                 "master_port": ret_port,
-                "master_uri": "tcp://127.0.0.1:{0}".format(ret_port),
+                "master_uri": "tcp://127.0.0.1:{}".format(ret_port),
             }
         )
 
@@ -200,7 +196,7 @@ class BaseTCPPubCase(AsyncTestCase, AdaptedConfigurationTestCaseMixin):
                 "master_ip": "127.0.0.1",
                 "auth_timeout": 1,
                 "master_port": ret_port,
-                "master_uri": "tcp://127.0.0.1:{0}".format(ret_port),
+                "master_uri": "tcp://127.0.0.1:{}".format(ret_port),
             }
         )
 
@@ -243,17 +239,17 @@ class BaseTCPPubCase(AsyncTestCase, AdaptedConfigurationTestCaseMixin):
         del cls.req_server_channel
 
     def setUp(self):
-        super(BaseTCPPubCase, self).setUp()
+        super().setUp()
         self._start_handlers = dict(self.io_loop._handlers)
 
     def tearDown(self):
-        super(BaseTCPPubCase, self).tearDown()
+        super().tearDown()
         failures = []
-        for k, v in six.iteritems(self.io_loop._handlers):
+        for k, v in self.io_loop._handlers.items():
             if self._start_handlers.get(k) != v:
                 failures.append((k, v))
         if failures:
-            raise Exception("FDs still attached to the IOLoop: {0}".format(failures))
+            raise Exception("FDs still attached to the IOLoop: {}".format(failures))
         del self.channel
         del self._start_handlers
 
@@ -287,7 +283,7 @@ class AsyncPubChannelTest(BaseTCPPubCase, PubChannelMixin):
 
 class SaltMessageClientPoolTest(AsyncTestCase):
     def setUp(self):
-        super(SaltMessageClientPoolTest, self).setUp()
+        super().setUp()
         sock_pool_size = 5
         with patch(
             "salt.transport.tcp.SaltMessageClient.__init__",
@@ -306,7 +302,7 @@ class SaltMessageClientPoolTest(AsyncTestCase):
             "salt.transport.tcp.SaltMessageClient.close", MagicMock(return_value=None)
         ):
             del self.original_message_clients
-        super(SaltMessageClientPoolTest, self).tearDown()
+        super().tearDown()
 
     def test_send(self):
         for message_client_mock in self.message_client_pool.message_clients:
@@ -422,30 +418,29 @@ class SaltMessageClientCleanupTest(TestCase, AdaptedConfigurationTestCaseMixin):
 class TCPPubServerChannelTest(TestCase, AdaptedConfigurationTestCaseMixin):
     @patch("salt.master.SMaster.secrets")
     @patch("salt.crypt.Crypticle")
-    @patch("salt.utils.asynchronous.SyncWrapper")
-    def test_publish_filtering(self, sync_wrapper, crypticle, secrets):
+    def test_publish_filtering(self, crypticle, secrets):
         opts = self.get_temp_config("master")
         opts["sign_pub_messages"] = False
         channel = TCPPubServerChannel(opts)
 
-        wrap = MagicMock()
         crypt = MagicMock()
         crypt.dumps.return_value = {"test": "value"}
 
         secrets.return_value = {"aes": {"secret": None}}
         crypticle.return_value = crypt
-        sync_wrapper.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
@@ -453,8 +448,9 @@ class TCPPubServerChannelTest(TestCase, AdaptedConfigurationTestCaseMixin):
 
         # 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
@@ -462,26 +458,22 @@ class TCPPubServerChannelTest(TestCase, AdaptedConfigurationTestCaseMixin):
     @patch("salt.utils.minions.CkMinions.check_minions")
     @patch("salt.master.SMaster.secrets")
     @patch("salt.crypt.Crypticle")
-    @patch("salt.utils.asynchronous.SyncWrapper")
-    def test_publish_filtering_str_list(
-        self, sync_wrapper, crypticle, secrets, check_minions
-    ):
+    def test_publish_filtering_str_list(self, crypticle, secrets, check_minions):
         opts = self.get_temp_config("master")
         opts["sign_pub_messages"] = False
         channel = TCPPubServerChannel(opts)
 
-        wrap = MagicMock()
         crypt = MagicMock()
         crypt.dumps.return_value = {"test": "value"}
 
         secrets.return_value = {"aes": {"secret": None}}
         crypticle.return_value = crypt
-        sync_wrapper.return_value = wrap
         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/unit/transport/test_zeromq.py b/tests/unit/transport/test_zeromq.py
index c78649ff9f..81652c80f0 100644
--- a/tests/unit/transport/test_zeromq.py
+++ b/tests/unit/transport/test_zeromq.py
@@ -437,6 +437,7 @@ class PubServerChannel(TestCase, AdaptedConfigurationTestCaseMixin):
             "secret": multiprocessing.Array(
                 ctypes.c_char, six.b(salt.crypt.Crypticle.generate_key_string()),
             ),
+            "serial": multiprocessing.Value(ctypes.c_longlong, lock=False),
         }
         cls.minion_config = cls.get_temp_config(
             "minion",
@@ -492,9 +493,11 @@ class PubServerChannel(TestCase, AdaptedConfigurationTestCaseMixin):
         crypticle = salt.crypt.Crypticle(
             opts, salt.master.SMaster.secrets["aes"]["secret"].value
         )
+        unpacker = salt.utils.msgpack.Unpacker()
+        stop = False
         while time.time() - last_msg < timeout:
             try:
-                payload = sock.recv(zmq.NOBLOCK)
+                wire_bytes = sock.recv(zmq.NOBLOCK)
             except zmq.ZMQError:
                 time.sleep(0.01)
             else:
@@ -502,13 +505,20 @@ class PubServerChannel(TestCase, AdaptedConfigurationTestCaseMixin):
                     if messages != 1:
                         messages -= 1
                         continue
-                payload = crypticle.loads(serial.loads(payload)["load"])
-                if "stop" in payload:
-                    break
-                last_msg = time.time()
-                results.append(payload["jid"])
 
-    @skipIf(salt.utils.platform.is_windows(), "Skip on Windows OS")
+                unpacker.feed(wire_bytes)
+                for w_payload in unpacker:
+                    payload = crypticle.loads(w_payload[b"load"])
+                    if not payload:
+                        continue
+                    if "stop" in payload:
+                        stop = True
+                        break
+                    last_msg = time.time()
+                    results.append(payload["jid"])
+            if stop:
+                break
+
     @slowTest
     def test_publish_to_pubserv_ipc(self):
         """
@@ -541,7 +551,6 @@ class PubServerChannel(TestCase, AdaptedConfigurationTestCaseMixin):
         server_channel.pub_close()
         assert len(results) == send_num, (len(results), set(expect).difference(results))
 
-    @skipIf(salt.utils.platform.is_linux(), "Skip on Linux")
     @slowTest
     def test_zeromq_publish_port(self):
         """
@@ -572,7 +581,6 @@ class PubServerChannel(TestCase, AdaptedConfigurationTestCaseMixin):
             channel.connect()
         assert str(opts["publish_port"]) in patch_socket.mock_calls[0][1][0]
 
-    @skipIf(salt.utils.platform.is_linux(), "Skip on Linux")
     def test_zeromq_zeromq_filtering_decode_message_no_match(self):
         """
         test AsyncZeroMQPubChannel _decode_messages when
@@ -610,7 +618,6 @@ class PubServerChannel(TestCase, AdaptedConfigurationTestCaseMixin):
             res = server_channel._decode_messages(message)
         assert res.result() is None
 
-    @skipIf(salt.utils.platform.is_linux(), "Skip on Linux")
     def test_zeromq_zeromq_filtering_decode_message(self):
         """
         test AsyncZeroMQPubChannel _decode_messages
@@ -663,12 +670,7 @@ class PubServerChannel(TestCase, AdaptedConfigurationTestCaseMixin):
             zmq_filtering=True,
             acceptance_wait_time=5,
         )
-        server_channel = salt.transport.zeromq.ZeroMQPubServerChannel(opts)
-        server_channel.pre_fork(
-            self.process_manager,
-            kwargs={"log_queue": salt.log.setup.get_multiprocessing_logging_queue()},
-        )
-        pub_uri = "tcp://{interface}:{publish_port}".format(**server_channel.opts)
+        pub_uri = "tcp://{interface}:{publish_port}".format(**opts)
         send_num = 1
         expect = []
         results = []
@@ -678,10 +680,6 @@ class PubServerChannel(TestCase, AdaptedConfigurationTestCaseMixin):
             kwargs={"messages": 2},
         )
         gather.start()
-        # Allow time for server channel to start, especially on windows
-        time.sleep(2)
-        expect.append(send_num)
-        load = {"tgt_type": "glob", "tgt": "*", "jid": send_num}
         with patch(
             "salt.utils.minions.CkMinions.check_minions",
             MagicMock(
@@ -692,11 +690,37 @@ class PubServerChannel(TestCase, AdaptedConfigurationTestCaseMixin):
                 }
             ),
         ):
-            server_channel.publish(load)
-        server_channel.publish({"tgt_type": "glob", "tgt": "*", "stop": True})
-        gather.join()
-        server_channel.pub_close()
-        assert len(results) == send_num, (len(results), set(expect).difference(results))
+            # Allow time for server channel to start, especially on windows
+            time.sleep(2)
+            server_channel = salt.transport.zeromq.ZeroMQPubServerChannel(opts)
+            server_channel.pre_fork(
+                self.process_manager,
+                kwargs={
+                    "log_queue": salt.log.setup.get_multiprocessing_logging_queue()
+                },
+            )
+            time.sleep(2)
+            expect.append(send_num)
+            load = {"tgt_type": "glob", "tgt": "*", "jid": send_num}
+            with patch(
+                "salt.utils.minions.CkMinions.check_minions",
+                MagicMock(
+                    return_value={
+                        "minions": ["minion"],
+                        "missing": [],
+                        "ssh_minions": False,
+                    }
+                ),
+            ):
+                server_channel.publish(load)
+            server_channel.publish({"tgt_type": "glob", "tgt": "*", "stop": True})
+            time.sleep(0.3)
+            server_channel.pub_close()
+            gather.join()
+            assert len(results) == send_num, (
+                len(results),
+                set(expect).difference(results),
+            )
 
     @slowTest
     def test_publish_to_pubserv_tcp(self):
@@ -733,6 +757,7 @@ class PubServerChannel(TestCase, AdaptedConfigurationTestCaseMixin):
         for i in range(num):
             load = {"tgt_type": "glob", "tgt": "*", "jid": "{}-{}".format(sid, i)}
             server_channel.publish(load)
+        time.sleep(0.3)
         server_channel.pub_close()
 
     @staticmethod
@@ -746,6 +771,7 @@ class PubServerChannel(TestCase, AdaptedConfigurationTestCaseMixin):
                 "xdata": "0" * size,
             }
             server_channel.publish(load)
+        time.sleep(0.3)
         server_channel.pub_close()
 
     @skipIf(salt.utils.platform.is_freebsd(), "Skip on FreeBSD")
@@ -757,21 +783,22 @@ class PubServerChannel(TestCase, AdaptedConfigurationTestCaseMixin):
         https://github.com/saltstack/salt/issues/36469
         """
         opts = dict(self.master_config, ipc_mode="tcp", pub_hwm=0)
-        server_channel = salt.transport.zeromq.ZeroMQPubServerChannel(opts)
-        server_channel.pre_fork(
-            self.process_manager,
-            kwargs={"log_queue": salt.log.setup.get_multiprocessing_logging_queue()},
-        )
         send_num = 10 * 4
         expect = []
         results = []
         pub_uri = "tcp://{interface}:{publish_port}".format(**opts)
         # Allow time for server channel to start, especially on windows
-        time.sleep(2)
         gather = threading.Thread(
             target=self._gather_results, args=(self.minion_config, pub_uri, results,)
         )
         gather.start()
+        time.sleep(2)
+        server_channel = salt.transport.zeromq.ZeroMQPubServerChannel(opts)
+        server_channel.pre_fork(
+            self.process_manager,
+            kwargs={"log_queue": salt.log.setup.get_multiprocessing_logging_queue()},
+        )
+        time.sleep(2)
         with ThreadPoolExecutor(max_workers=4) as executor:
             executor.submit(self._send_small, opts, 1)
             executor.submit(self._send_small, opts, 2)
-- 
2.35.1

