Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
openSUSE:Leap:15.5:Update
salt.23534
fix-multiple-security-issues-bsc-1197417.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File fix-multiple-security-issues-bsc-1197417.patch of Package salt.23534
From 0afba71c57c72a95d11cc2fea84aac20a4861324 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] Fix multiple security issues (bsc#1197417) * Sign authentication replies to prevent MiTM (CVE-2020-22935) * Sign pillar data to prevent MiTM attacks. (CVE-2022-22934) * Prevent job and fileserver replays (CVE-2022-22936) * Fixed targeting bug, especially visible when using syndic and user auth. (CVE-2022-22941) --- .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
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor