├── .coveragerc ├── .github └── workflows │ └── run_tests.yml ├── .gitignore ├── .readthedocs.yaml ├── CONTRIBUTING.rst ├── COPYRIGHT ├── LICENSE ├── MANIFEST.in ├── README.rst ├── asyncssh ├── __init__.py ├── agent.py ├── agent_unix.py ├── agent_win32.py ├── asn1.py ├── auth.py ├── auth_keys.py ├── channel.py ├── client.py ├── compression.py ├── config.py ├── connection.py ├── constants.py ├── crypto │ ├── __init__.py │ ├── chacha.py │ ├── cipher.py │ ├── dh.py │ ├── dsa.py │ ├── ec.py │ ├── ec_params.py │ ├── ed.py │ ├── kdf.py │ ├── misc.py │ ├── pq.py │ ├── rsa.py │ ├── umac.py │ └── x509.py ├── dsa.py ├── ecdsa.py ├── eddsa.py ├── editor.py ├── encryption.py ├── forward.py ├── gss.py ├── gss_unix.py ├── gss_win32.py ├── kex.py ├── kex_dh.py ├── kex_rsa.py ├── keysign.py ├── known_hosts.py ├── listener.py ├── logging.py ├── mac.py ├── misc.py ├── packet.py ├── pattern.py ├── pbe.py ├── pkcs11.py ├── process.py ├── public_key.py ├── py.typed ├── rsa.py ├── saslprep.py ├── scp.py ├── server.py ├── session.py ├── sftp.py ├── sk.py ├── sk_ecdsa.py ├── sk_eddsa.py ├── socks.py ├── stream.py ├── subprocess.py ├── tuntap.py ├── version.py └── x11.py ├── docs ├── _templates │ ├── sidebarbottom.html │ └── sidebartop.html ├── api.rst ├── changes.rst ├── conf.py ├── contributing.rst ├── index.rst ├── requirements.txt ├── rftheme │ ├── layout.html │ ├── static │ │ └── rftheme.css_t │ └── theme.conf └── rtd-req.txt ├── examples ├── callback_client.py ├── callback_client2.py ├── callback_client3.py ├── callback_math_server.py ├── chat_server.py ├── check_exit_status.py ├── chroot_sftp_server.py ├── direct_client.py ├── direct_server.py ├── editor.py ├── gather_results.py ├── listening_client.py ├── local_forwarding_client.py ├── local_forwarding_client2.py ├── local_forwarding_server.py ├── math_client.py ├── math_server.py ├── redirect_input.py ├── redirect_local_pipe.py ├── redirect_remote_pipe.py ├── redirect_server.py ├── remote_forwarding_client.py ├── remote_forwarding_client2.py ├── remote_forwarding_server.py ├── reverse_client.py ├── reverse_server.py ├── scp_client.py ├── set_environment.py ├── set_terminal.py ├── sftp_client.py ├── show_environment.py ├── show_terminal.py ├── simple_cert_server.py ├── simple_client.py ├── simple_keyed_server.py ├── simple_scp_server.py ├── simple_server.py ├── simple_sftp_server.py ├── stream_direct_client.py ├── stream_direct_server.py └── stream_listening_client.py ├── mypy.ini ├── pylintrc ├── pyproject.toml ├── tests ├── __init__.py ├── gss_stub.py ├── gssapi_stub.py ├── keysign_stub.py ├── pkcs11_stub.py ├── server.py ├── sk_stub.py ├── sspi_stub.py ├── test_agent.py ├── test_asn1.py ├── test_auth.py ├── test_auth_keys.py ├── test_channel.py ├── test_compression.py ├── test_config.py ├── test_connection.py ├── test_connection_auth.py ├── test_editor.py ├── test_encryption.py ├── test_forward.py ├── test_kex.py ├── test_known_hosts.py ├── test_logging.py ├── test_mac.py ├── test_packet.py ├── test_pkcs11.py ├── test_process.py ├── test_public_key.py ├── test_saslprep.py ├── test_sftp.py ├── test_sk.py ├── test_stream.py ├── test_subprocess.py ├── test_tuntap.py ├── test_x11.py ├── test_x509.py └── util.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | relative_files = True 4 | source = 5 | asyncssh 6 | tests 7 | 8 | [report] 9 | exclude_lines = 10 | if TYPE_CHECKING: 11 | pragma: no cover 12 | raise NotImplementedError 13 | partial_branches = 14 | pragma: no branch 15 | for .* 16 | -------------------------------------------------------------------------------- /.github/workflows/run_tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | run-tests: 6 | name: Run tests 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | os: [ubuntu-latest, macos-latest, windows-latest] 11 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 12 | include: 13 | - os: macos-latest 14 | python-version: "3.10" 15 | openssl-version: "3" 16 | - os: macos-latest 17 | python-version: "3.11" 18 | openssl-version: "3" 19 | - os: macos-latest 20 | python-version: "3.12" 21 | openssl-version: "3" 22 | - os: macos-latest 23 | python-version: "3.13" 24 | openssl-version: "3" 25 | 26 | runs-on: ${{ matrix.os }} 27 | env: 28 | liboqs_version: '0.10.1' 29 | nettle_version: nettle_3.8.1_release_20220727 30 | 31 | steps: 32 | - name: Checkout asyncssh 33 | uses: actions/checkout@v4 34 | with: 35 | path: asyncssh 36 | 37 | - name: Checkout liboqs 38 | if: ${{ runner.os != 'macOS' }} 39 | uses: actions/checkout@v4 40 | with: 41 | repository: open-quantum-safe/liboqs 42 | ref: ${{ env.liboqs_version }} 43 | path: liboqs 44 | 45 | - uses: actions/setup-python@v5 46 | with: 47 | python-version: ${{ matrix.python-version }} 48 | cache: pip 49 | cache-dependency-path: | 50 | asyncssh/setup.py 51 | asyncssh/tox.ini 52 | 53 | - name: Set up ccache for liboqs (Linux) 54 | uses: hendrikmuhs/ccache-action@v1.2 55 | if: ${{ runner.os == 'Linux' }} 56 | with: 57 | key: liboqs-cache-${{ matrix.os }} 58 | 59 | - name: Install Linux dependencies 60 | if: ${{ runner.os == 'Linux' }} 61 | run: | 62 | sudo apt update 63 | sudo apt install -y --no-install-recommends libnettle8 libsodium-dev libssl-dev libkrb5-dev ssh cmake ninja-build 64 | 65 | - name: Install macOS dependencies 66 | if: ${{ runner.os == 'macOS' }} 67 | run: brew install nettle liboqs libsodium openssl 68 | 69 | - name: Provide OpenSSL 3 70 | if: ${{ runner.os == 'macOS' && matrix.openssl-version == '3' }} 71 | run: echo "/usr/local/opt/openssl@3/bin" >> $GITHUB_PATH 72 | 73 | - name: Install nettle (Windows) 74 | if: ${{ runner.os == 'Windows' }} 75 | shell: pwsh 76 | run: | 77 | curl -fLO https://github.com/ShiftMediaProject/nettle/releases/download/${{ env.nettle_version }}/libnettle_${{ env.nettle_version }}_msvc17.zip 78 | Expand-Archive libnettle_${{ env.nettle_version }}_msvc17.zip nettle 79 | cp nettle\bin\x64\*.dll "$env:Python_ROOT_DIR" 80 | 81 | - name: Install liboqs (Linux) 82 | if: ${{ runner.os == 'Linux' }} 83 | working-directory: liboqs 84 | run: | 85 | cmake -GNinja -Bbuild . -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_SHARED_LIBS=ON -DOQS_BUILD_ONLY_LIB=ON -DOQS_DIST_BUILD=ON -DCMAKE_C_COMPILER_LAUNCHER=ccache 86 | cmake --build build 87 | sudo cmake --install build 88 | 89 | - name: Initialize MSVC environment 90 | uses: ilammy/msvc-dev-cmd@v1 91 | 92 | - name: Install liboqs (Windows) 93 | if: ${{ runner.os == 'Windows' }} 94 | shell: pwsh 95 | working-directory: liboqs 96 | run: | 97 | cmake -GNinja -Bbuild . -DBUILD_SHARED_LIBS=ON -DOQS_BUILD_ONLY_LIB=ON -DOQS_DIST_BUILD=ON 98 | cmake --build build 99 | cp build\bin\oqs.dll "$env:Python_ROOT_DIR" 100 | 101 | - name: Install Python dependencies 102 | run: pip install tox 103 | 104 | - name: Run tests 105 | shell: python 106 | working-directory: asyncssh 107 | run: | 108 | import os, sys, platform, subprocess 109 | V = sys.version_info 110 | p = platform.system().lower() 111 | subprocess.run( 112 | ['tox', 'run', '-e', f'py{V.major}{V.minor}-{p}', '--', '-ra'], 113 | check=True) 114 | 115 | - name: Upload coverage data 116 | uses: actions/upload-artifact@v4 117 | with: 118 | name: coverage-${{ matrix.os }}-${{ matrix.python-version }} 119 | path: asyncssh/.coverage.* 120 | include-hidden-files: true 121 | retention-days: 1 122 | 123 | merge-coverage: 124 | runs-on: ubuntu-latest 125 | needs: run-tests 126 | if: ${{ always() }} 127 | steps: 128 | - name: Merge coverage 129 | uses: actions/upload-artifact/merge@v4 130 | with: 131 | name: coverage 132 | pattern: coverage-* 133 | include-hidden-files: true 134 | 135 | report-coverage: 136 | name: Report coverage 137 | runs-on: ubuntu-latest 138 | needs: merge-coverage 139 | if: ${{ always() }} 140 | steps: 141 | - uses: actions/checkout@v4 142 | - uses: actions/setup-python@v5 143 | - uses: actions/download-artifact@v4 144 | with: 145 | name: coverage 146 | - name: Install dependencies 147 | run: | 148 | sudo apt install -y sqlite3 149 | pip install tox 150 | - name: Report coverage 151 | run: | 152 | shopt -s nullglob 153 | for f in .coverage.*-windows; do 154 | sqlite3 "$f" "update file set path = replace(path, '\\', '/');" 155 | done 156 | tox -e report 157 | - uses: codecov/codecov-action@v4 158 | with: 159 | files: coverage.xml 160 | token: ${{ secrets.CODECOV_TOKEN }} 161 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | MANIFEST 3 | __pycache__/ 4 | *.py[cod] 5 | 6 | asyncssh.egg-info 7 | build/ 8 | dist/ 9 | docs/Makefile 10 | docs/_build/ 11 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | 8 | python: 9 | install: 10 | - requirements: docs/requirements.txt 11 | 12 | sphinx: 13 | configuration: docs/conf.py 14 | 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing to AsyncSSH 2 | ======================== 3 | 4 | Input on AsyncSSH is extremely welcome. Below are some recommendations of 5 | the best ways to contribute. 6 | 7 | Asking questions 8 | ---------------- 9 | 10 | If you have a general question about how to use AsyncSSH, you are welcome 11 | to post it to the end-user mailing list at `asyncssh-users@googlegroups.com 12 | `_. If you have a question 13 | related to the development of AsyncSSH, you can post it to the development 14 | mailing list at `asyncssh-dev@googlegroups.com 15 | `_. 16 | 17 | You are also welcome to use the AsyncSSH `issue tracker 18 | `_ to ask questions. 19 | 20 | Reporting bugs 21 | -------------- 22 | 23 | Please use the `issue tracker `_ 24 | to report any bugs you find. Before creating a new issue, please check the 25 | currently open issues to see if your problem has already been reported. 26 | 27 | If you create a new issue, please include the version of AsyncSSH you are 28 | using, information about the OS you are running on and the installed 29 | version of Python and any other libraries that are involved. Please also 30 | include detailed information about how to reproduce the problem, including 31 | any traceback information you were able to collect or other relevant output. 32 | If you have sample code which exhibits the problem, feel free to include 33 | that as well. 34 | 35 | If possible, please test against the latest version of AsyncSSH. Also, if 36 | you are testing code in something other than the master branch, it would 37 | be helpful to know if you also see the problem in master. 38 | 39 | Requesting feature enhancements 40 | ------------------------------- 41 | 42 | The `issue tracker `_ 43 | should also be used to post feature enhancement requests. While I can't 44 | make any promises about what features will be added in the future, 45 | suggestions are always welcome! 46 | 47 | Contributing code 48 | ----------------- 49 | 50 | Before submitting a pull request, please create an issue on the `issue 51 | tracker `_ explaining what 52 | functionality you'd like to contribute and how it could be used. 53 | Discussing the approach you'd like to take up front will make it far 54 | more likely I'll be able to accept your changes, or explain what issues 55 | might prevent that before you spend a lot of effort. 56 | 57 | If you find a typo or other small bug in the code, you're welcome to 58 | submit a patch without filing an issue first, but for anything larger than 59 | a few lines I strongly recommend coordinating up front. 60 | 61 | Any code you submit will need to be provided with a compatible license. 62 | AsyncSSH code is currently released under the `Eclipse Public License 63 | v2.0 `_. Before submitting 64 | a pull request, make sure to indicate that you are ok with releasing 65 | your code under this license and how you'd like to be listed in the 66 | contributors list. 67 | 68 | Branches 69 | -------- 70 | 71 | There are two long-lived branches in AsyncSSH: 72 | 73 | * The master branch is intended to contain the latest stable version 74 | of the code. All official versions of AsyncSSH are released from 75 | this branch, and each release has a corresponding tag added 76 | matching its release number. 77 | 78 | * The develop branch is intended to contain new features and bug fixes 79 | ready to be tested before being added to an official release. APIs 80 | in the develop branch may be subject to change until they are 81 | migrated back to master, and there's no guarantee of backward 82 | compatibility in this branch. However, pulling from this branch 83 | will provide early access to new functionality and a chance to 84 | influence this functionality before it is released. Also, all 85 | pull requests should be submitted against this branch. 86 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2018 by Ron Frederick and others. 2 | 3 | This program and the accompanying materials are made available under 4 | the terms of the Eclipse Public License v2.0 which accompanies this 5 | distribution and is available at: 6 | 7 | http://www.eclipse.org/legal/epl-2.0/ 8 | 9 | This program may also be made available under the following secondary 10 | licenses when the conditions for such availability set forth in the 11 | Eclipse Public License v2.0 are satisfied: 12 | 13 | GNU General Public License, Version 2.0, or any later versions of 14 | that license 15 | 16 | SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CONTRIBUTING.rst COPYRIGHT LICENSE README.rst pylintrc tox.ini 2 | include examples/*.py tests/*.py 3 | -------------------------------------------------------------------------------- /asyncssh/agent_unix.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016-2024 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """SSH agent support code for UNIX""" 22 | 23 | import asyncio 24 | import errno 25 | from typing import TYPE_CHECKING, Tuple 26 | 27 | 28 | if TYPE_CHECKING: 29 | # pylint: disable=cyclic-import 30 | from .agent import AgentReader, AgentWriter 31 | 32 | 33 | async def open_agent(agent_path: str) -> Tuple['AgentReader', 'AgentWriter']: 34 | """Open a connection to ssh-agent""" 35 | 36 | if not agent_path: 37 | raise OSError(errno.ENOENT, 'Agent not found') 38 | 39 | return await asyncio.open_unix_connection(agent_path) 40 | -------------------------------------------------------------------------------- /asyncssh/agent_win32.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016-2024 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """SSH agent support code for Windows""" 22 | 23 | # Some of the imports below won't be found when running pylint on UNIX 24 | # pylint: disable=import-error 25 | 26 | import asyncio 27 | import ctypes 28 | import ctypes.wintypes 29 | import errno 30 | from typing import TYPE_CHECKING, Tuple, Union, cast 31 | 32 | from .misc import open_file 33 | 34 | 35 | if TYPE_CHECKING: 36 | # pylint: disable=cyclic-import 37 | from .agent import AgentReader, AgentWriter 38 | 39 | 40 | try: 41 | import mmapfile 42 | import win32api 43 | import win32con 44 | import win32ui 45 | _pywin32_available = True 46 | except ImportError: 47 | _pywin32_available = False 48 | 49 | 50 | _AGENT_COPYDATA_ID = 0x804e50ba 51 | _AGENT_MAX_MSGLEN = 8192 52 | _AGENT_NAME = 'Pageant' 53 | 54 | _DEFAULT_OPENSSH_PATH = r'\\.\pipe\openssh-ssh-agent' 55 | 56 | 57 | def _find_agent_window() -> 'win32ui.PyCWnd': 58 | """Find and return the Pageant window""" 59 | 60 | if _pywin32_available: 61 | try: 62 | return win32ui.FindWindow(_AGENT_NAME, _AGENT_NAME) 63 | except win32ui.error: 64 | raise OSError(errno.ENOENT, 'Agent not found') from None 65 | else: 66 | raise OSError(errno.ENOENT, 'PyWin32 not installed') from None 67 | 68 | 69 | class _CopyDataStruct(ctypes.Structure): 70 | """Windows COPYDATASTRUCT argument for WM_COPYDATA message""" 71 | 72 | _fields_ = (('dwData', ctypes.wintypes.LPARAM), 73 | ('cbData', ctypes.wintypes.DWORD), 74 | ('lpData', ctypes.c_char_p)) 75 | 76 | 77 | class _PageantTransport: 78 | """Transport to connect to Pageant agent on Windows""" 79 | 80 | def __init__(self) -> None: 81 | self._mapname = f'{_AGENT_NAME}{win32api.GetCurrentThreadId():08x}' 82 | 83 | try: 84 | self._mapfile = mmapfile.mmapfile('', self._mapname, 85 | _AGENT_MAX_MSGLEN, 0, 0) 86 | except mmapfile.error as exc: 87 | raise OSError(errno.EIO, str(exc)) from None 88 | 89 | self._cds = _CopyDataStruct(_AGENT_COPYDATA_ID, len(self._mapname) + 1, 90 | self._mapname.encode()) 91 | 92 | self._writing = False 93 | 94 | def write(self, data: bytes) -> None: 95 | """Write request data to Pageant agent""" 96 | 97 | if not self._writing: 98 | self._mapfile.seek(0) 99 | self._writing = True 100 | 101 | try: 102 | self._mapfile.write(data) 103 | except ValueError as exc: 104 | raise OSError(errno.EIO, str(exc)) from None 105 | 106 | async def readexactly(self, n: int) -> bytes: 107 | """Read response data from Pageant agent""" 108 | 109 | if self._writing: 110 | cwnd = _find_agent_window() 111 | 112 | if not cwnd.SendMessage(win32con.WM_COPYDATA, 0, 113 | cast(int, self._cds)): 114 | raise OSError(errno.EIO, 'Unable to send agent request') 115 | 116 | self._writing = False 117 | self._mapfile.seek(0) 118 | 119 | result = self._mapfile.read(n) 120 | 121 | if len(result) != n: 122 | raise asyncio.IncompleteReadError(result, n) 123 | 124 | return result 125 | 126 | def close(self) -> None: 127 | """Close the connection to Pageant""" 128 | 129 | if self._mapfile: 130 | self._mapfile.close() 131 | 132 | async def wait_closed(self) -> None: 133 | """Wait for the transport to close""" 134 | 135 | 136 | class _W10OpenSSHTransport: 137 | """Transport to connect to OpenSSH agent on Windows 10""" 138 | 139 | def __init__(self, agent_path: str): 140 | self._agentfile = open_file(agent_path, 'r+b') 141 | 142 | async def readexactly(self, n: int) -> bytes: 143 | """Read response data from OpenSSH agent""" 144 | 145 | result = self._agentfile.read(n) 146 | 147 | if len(result) != n: 148 | raise asyncio.IncompleteReadError(result, n) 149 | 150 | return result 151 | 152 | def write(self, data: bytes) -> None: 153 | """Write request data to OpenSSH agent""" 154 | 155 | self._agentfile.write(data) 156 | 157 | def close(self) -> None: 158 | """Close the connection to OpenSSH""" 159 | 160 | if self._agentfile: 161 | self._agentfile.close() 162 | 163 | async def wait_closed(self) -> None: 164 | """Wait for the transport to close""" 165 | 166 | 167 | async def open_agent(agent_path: str) -> Tuple['AgentReader', 'AgentWriter']: 168 | """Open a connection to the Pageant or Windows 10 OpenSSH agent""" 169 | 170 | transport: Union[None, _PageantTransport, _W10OpenSSHTransport] = None 171 | 172 | if not agent_path: 173 | try: 174 | _find_agent_window() 175 | transport = _PageantTransport() 176 | except OSError: 177 | agent_path = _DEFAULT_OPENSSH_PATH 178 | 179 | if not transport: 180 | transport = _W10OpenSSHTransport(agent_path) 181 | 182 | return transport, transport 183 | -------------------------------------------------------------------------------- /asyncssh/compression.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2024 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """SSH compression handlers""" 22 | 23 | from typing import Callable, List, Optional 24 | import zlib 25 | 26 | _cmp_algs: List[bytes] = [] 27 | _default_cmp_algs: List[bytes] = [] 28 | 29 | _cmp_params = {} 30 | 31 | _cmp_compressors = {} 32 | _cmp_decompressors = {} 33 | 34 | 35 | class Compressor: 36 | """Base class for data compressor""" 37 | 38 | def compress(self, data: bytes) -> Optional[bytes]: 39 | """Compress data""" 40 | 41 | raise NotImplementedError 42 | 43 | 44 | class Decompressor: 45 | """Base class for data decompressor""" 46 | 47 | def decompress(self, data: bytes) -> Optional[bytes]: 48 | """Decompress data""" 49 | 50 | raise NotImplementedError 51 | 52 | 53 | _CompressorType = Callable[[], Optional[Compressor]] 54 | _DecompressorType = Callable[[], Optional[Decompressor]] 55 | 56 | 57 | def _none() -> None: 58 | """Compressor/decompressor for no compression""" 59 | 60 | return None 61 | 62 | 63 | class _ZLibCompress(Compressor): 64 | """Wrapper class to force a sync flush and handle exceptions""" 65 | 66 | def __init__(self) -> None: 67 | self._comp = zlib.compressobj() 68 | 69 | def compress(self, data: bytes) -> Optional[bytes]: 70 | """Compress data using zlib compression with sync flush""" 71 | 72 | try: 73 | return self._comp.compress(data) + \ 74 | self._comp.flush(zlib.Z_SYNC_FLUSH) 75 | except zlib.error: # pragma: no cover 76 | return None 77 | 78 | 79 | class _ZLibDecompress(Decompressor): 80 | """Wrapper class to handle exceptions""" 81 | 82 | def __init__(self) -> None: 83 | self._decomp = zlib.decompressobj() 84 | 85 | def decompress(self, data: bytes) -> Optional[bytes]: 86 | """Decompress data using zlib compression""" 87 | 88 | try: 89 | return self._decomp.decompress(data) 90 | except zlib.error: # pragma: no cover 91 | return None 92 | 93 | 94 | def register_compression_alg(alg: bytes, compressor: _CompressorType, 95 | decompressor: _DecompressorType, 96 | after_auth: bool, default: bool) -> None: 97 | """Register a compression algorithm""" 98 | 99 | _cmp_algs.append(alg) 100 | 101 | if default: 102 | _default_cmp_algs.append(alg) 103 | 104 | _cmp_params[alg] = after_auth 105 | 106 | _cmp_compressors[alg] = compressor 107 | _cmp_decompressors[alg] = decompressor 108 | 109 | 110 | def get_compression_algs() -> List[bytes]: 111 | """Return supported compression algorithms""" 112 | 113 | return _cmp_algs 114 | 115 | 116 | def get_default_compression_algs() -> List[bytes]: 117 | """Return default compression algorithms""" 118 | 119 | return _default_cmp_algs 120 | 121 | 122 | def get_compression_params(alg: bytes) -> bool: 123 | """Get parameters of a compression algorithm 124 | 125 | This function returns whether or not a compression algorithm should 126 | be delayed until after authentication completes. 127 | 128 | """ 129 | 130 | return _cmp_params[alg] 131 | 132 | 133 | def get_compressor(alg: bytes) -> Optional[Compressor]: 134 | """Return an instance of a compressor 135 | 136 | This function returns an object that can be used for data compression. 137 | 138 | """ 139 | 140 | return _cmp_compressors[alg]() 141 | 142 | 143 | def get_decompressor(alg: bytes) -> Optional[Decompressor]: 144 | """Return an instance of a decompressor 145 | 146 | This function returns an object that can be used for data decompression. 147 | 148 | """ 149 | 150 | return _cmp_decompressors[alg]() 151 | 152 | register_compression_alg(b'none', 153 | _none, _none, False, True) 154 | register_compression_alg(b'zlib@openssh.com', 155 | _ZLibCompress, _ZLibDecompress, True, True) 156 | register_compression_alg(b'zlib', 157 | _ZLibCompress, _ZLibDecompress, False, False) 158 | -------------------------------------------------------------------------------- /asyncssh/crypto/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-2021 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """A shim for accessing cryptographic primitives needed by asyncssh""" 22 | 23 | from .cipher import BasicCipher, GCMCipher, register_cipher, get_cipher_params 24 | 25 | from .dsa import DSAPrivateKey, DSAPublicKey 26 | 27 | from .dh import DH 28 | 29 | from .ec import ECDSAPrivateKey, ECDSAPublicKey, ECDH 30 | 31 | from .ed import ed25519_available, ed448_available 32 | from .ed import curve25519_available, curve448_available 33 | from .ed import EdDSAPrivateKey, EdDSAPublicKey, Curve25519DH, Curve448DH 34 | 35 | from .ec_params import lookup_ec_curve_by_params 36 | 37 | from .kdf import pbkdf2_hmac 38 | 39 | from .misc import CryptoKey, PyCAKey 40 | 41 | from .rsa import RSAPrivateKey, RSAPublicKey 42 | 43 | from .pq import mlkem_available, sntrup_available, PQDH 44 | 45 | # Import chacha20-poly1305 cipher if available 46 | from .chacha import ChachaCipher, chacha_available 47 | 48 | # Import umac cryptographic hash if available 49 | try: 50 | from .umac import umac32, umac64, umac96, umac128 51 | except (ImportError, AttributeError, OSError): # pragma: no cover 52 | pass 53 | 54 | # Import X.509 certificate support if available 55 | try: 56 | from .x509 import X509Certificate, X509Name, X509NamePattern 57 | from .x509 import generate_x509_certificate, import_x509_certificate 58 | except (ImportError, AttributeError): # pragma: no cover 59 | pass 60 | 61 | __all__ = [ 62 | 'BasicCipher', 'ChachaCipher', 'CryptoKey', 'Curve25519DH', 'Curve448DH', 63 | 'DH', 'DSAPrivateKey', 'DSAPublicKey', 'ECDH', 'ECDSAPrivateKey', 64 | 'ECDSAPublicKey', 'EdDSAPrivateKey', 'EdDSAPublicKey', 'GCMCipher', 'PQDH', 65 | 'PyCAKey', 'RSAPrivateKey', 'RSAPublicKey', 'chacha_available', 66 | 'curve25519_available', 'curve448_available', 'X509Certificate', 67 | 'X509Name', 'X509NamePattern', 'ed25519_available', 'ed448_available', 68 | 'generate_x509_certificate', 'get_cipher_params', 'import_x509_certificate', 69 | 'lookup_ec_curve_by_params', 'mlkem_available', 'pbkdf2_hmac', 70 | 'register_cipher', 'sntrup_available', 'umac32', 'umac64', 'umac96', 71 | 'umac128' 72 | ] 73 | -------------------------------------------------------------------------------- /asyncssh/crypto/chacha.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2021 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """Chacha20-Poly1305 symmetric encryption handler""" 22 | 23 | from ctypes import c_ulonglong, create_string_buffer 24 | from typing import Optional, Tuple 25 | 26 | from cryptography.exceptions import InvalidSignature 27 | from cryptography.hazmat.backends.openssl import backend 28 | from cryptography.hazmat.primitives.ciphers import Cipher 29 | from cryptography.hazmat.primitives.ciphers.algorithms import ChaCha20 30 | from cryptography.hazmat.primitives.poly1305 import Poly1305 31 | 32 | from .cipher import register_cipher 33 | 34 | 35 | if backend.poly1305_supported(): 36 | _CTR_0 = (0).to_bytes(8, 'little') 37 | _CTR_1 = (1).to_bytes(8, 'little') 38 | 39 | _POLY1305_KEYBYTES = 32 40 | 41 | def chacha20(key: bytes, data: bytes, nonce: bytes, ctr: int) -> bytes: 42 | """Encrypt/decrypt a block of data with the ChaCha20 cipher""" 43 | 44 | return Cipher(ChaCha20(key, (_CTR_1 if ctr else _CTR_0) + nonce), 45 | mode=None).encryptor().update(data) 46 | 47 | def poly1305_key(key: bytes, nonce: bytes) -> bytes: 48 | """Derive a Poly1305 key""" 49 | 50 | return chacha20(key, _POLY1305_KEYBYTES * b'\0', nonce, 0) 51 | 52 | def poly1305(key: bytes, data: bytes, nonce: bytes) -> bytes: 53 | """Compute a Poly1305 tag for a block of data""" 54 | 55 | return Poly1305.generate_tag(poly1305_key(key, nonce), data) 56 | 57 | def poly1305_verify(key: bytes, data: bytes, 58 | nonce: bytes, tag: bytes) -> bool: 59 | """Verify a Poly1305 tag for a block of data""" 60 | 61 | try: 62 | Poly1305.verify_tag(poly1305_key(key, nonce), data, tag) 63 | return True 64 | except InvalidSignature: 65 | return False 66 | 67 | chacha_available = True 68 | else: # pragma: no cover 69 | try: 70 | from libnacl import nacl 71 | 72 | _chacha20 = nacl.crypto_stream_chacha20 73 | _chacha20_xor_ic = nacl.crypto_stream_chacha20_xor_ic 74 | 75 | _POLY1305_BYTES = nacl.crypto_onetimeauth_poly1305_bytes() 76 | _POLY1305_KEYBYTES = nacl.crypto_onetimeauth_poly1305_keybytes() 77 | 78 | _poly1305 = nacl.crypto_onetimeauth_poly1305 79 | _poly1305_verify = nacl.crypto_onetimeauth_poly1305_verify 80 | 81 | def chacha20(key: bytes, data: bytes, nonce: bytes, ctr: int) -> bytes: 82 | """Encrypt/decrypt a block of data with the ChaCha20 cipher""" 83 | 84 | datalen = len(data) 85 | result = create_string_buffer(datalen) 86 | ull_datalen = c_ulonglong(datalen) 87 | ull_ctr = c_ulonglong(ctr) 88 | 89 | _chacha20_xor_ic(result, data, ull_datalen, nonce, ull_ctr, key) 90 | 91 | return result.raw 92 | 93 | def poly1305_key(key: bytes, nonce: bytes) -> bytes: 94 | """Derive a Poly1305 key""" 95 | 96 | polykey = create_string_buffer(_POLY1305_KEYBYTES) 97 | ull_polykeylen = c_ulonglong(_POLY1305_KEYBYTES) 98 | 99 | _chacha20(polykey, ull_polykeylen, nonce, key) 100 | 101 | return polykey.raw 102 | 103 | def poly1305(key: bytes, data: bytes, nonce: bytes) -> bytes: 104 | """Compute a Poly1305 tag for a block of data""" 105 | 106 | tag = create_string_buffer(_POLY1305_BYTES) 107 | ull_datalen = c_ulonglong(len(data)) 108 | polykey = poly1305_key(key, nonce) 109 | 110 | _poly1305(tag, data, ull_datalen, polykey) 111 | 112 | return tag.raw 113 | 114 | def poly1305_verify(key: bytes, data: bytes, 115 | nonce: bytes, tag: bytes) -> bool: 116 | """Verify a Poly1305 tag for a block of data""" 117 | 118 | ull_datalen = c_ulonglong(len(data)) 119 | polykey = poly1305_key(key, nonce) 120 | 121 | return _poly1305_verify(tag, data, ull_datalen, polykey) == 0 122 | 123 | chacha_available = True 124 | except (ImportError, OSError, AttributeError): 125 | chacha_available = False 126 | 127 | 128 | class ChachaCipher: 129 | """Shim for Chacha20-Poly1305 symmetric encryption""" 130 | 131 | def __init__(self, key: bytes): 132 | keylen = len(key) // 2 133 | self._key = key[:keylen] 134 | self._adkey = key[keylen:] 135 | 136 | def encrypt_and_sign(self, header: bytes, data: bytes, 137 | nonce: bytes) -> Tuple[bytes, bytes]: 138 | """Encrypt and sign a block of data""" 139 | 140 | header = chacha20(self._adkey, header, nonce, 0) 141 | data = chacha20(self._key, data, nonce, 1) 142 | tag = poly1305(self._key, header + data, nonce) 143 | 144 | return header + data, tag 145 | 146 | def decrypt_header(self, header: bytes, nonce: bytes) -> bytes: 147 | """Decrypt header data""" 148 | 149 | return chacha20(self._adkey, header, nonce, 0) 150 | 151 | def verify_and_decrypt(self, header: bytes, data: bytes, 152 | nonce: bytes, tag: bytes) -> Optional[bytes]: 153 | """Verify the signature of and decrypt a block of data""" 154 | 155 | if poly1305_verify(self._key, header + data, nonce, tag): 156 | return chacha20(self._key, data, nonce, 1) 157 | else: 158 | return None 159 | 160 | 161 | if chacha_available: # pragma: no branch 162 | register_cipher('chacha20-poly1305', 64, 0, 1) 163 | -------------------------------------------------------------------------------- /asyncssh/crypto/cipher.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-2024 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """A shim around PyCA for accessing symmetric ciphers needed by AsyncSSH""" 22 | 23 | from types import ModuleType 24 | from typing import Any, MutableMapping, Optional, Tuple 25 | import warnings 26 | 27 | from cryptography.exceptions import InvalidTag 28 | from cryptography.hazmat.primitives.ciphers import Cipher, CipherContext 29 | from cryptography.hazmat.primitives.ciphers.aead import AESGCM 30 | from cryptography.hazmat.primitives.ciphers.modes import CBC, CTR 31 | 32 | import cryptography.hazmat.primitives.ciphers.algorithms as _algs 33 | 34 | _decrepit_algs: Optional[ModuleType] 35 | 36 | try: 37 | import cryptography.hazmat.decrepit.ciphers.algorithms as _decrepit_algs 38 | except ImportError: # pragma: no cover 39 | _decrepit_algs = None 40 | 41 | 42 | _CipherAlgs = Tuple[Any, Any, int] 43 | _CipherParams = Tuple[int, int, int] 44 | 45 | 46 | _GCM_MAC_SIZE = 16 47 | 48 | _cipher_algs: MutableMapping[str, _CipherAlgs] = {} 49 | _cipher_params: MutableMapping[str, _CipherParams] = {} 50 | 51 | 52 | class BasicCipher: 53 | """Shim for basic ciphers""" 54 | 55 | def __init__(self, cipher_name: str, key: bytes, iv: bytes): 56 | cipher, mode, initial_bytes = _cipher_algs[cipher_name] 57 | 58 | self._cipher = Cipher(cipher(key), mode(iv) if mode else None) 59 | self._initial_bytes = initial_bytes 60 | self._encryptor: Optional[CipherContext] = None 61 | self._decryptor: Optional[CipherContext] = None 62 | 63 | def encrypt(self, data: bytes) -> bytes: 64 | """Encrypt a block of data""" 65 | 66 | if not self._encryptor: 67 | self._encryptor = self._cipher.encryptor() 68 | 69 | if self._initial_bytes: 70 | assert self._encryptor is not None 71 | self._encryptor.update(self._initial_bytes * b'\0') 72 | 73 | assert self._encryptor is not None 74 | return self._encryptor.update(data) 75 | 76 | def decrypt(self, data: bytes) -> bytes: 77 | """Decrypt a block of data""" 78 | 79 | if not self._decryptor: 80 | self._decryptor = self._cipher.decryptor() 81 | 82 | if self._initial_bytes: 83 | assert self._decryptor is not None 84 | self._decryptor.update(self._initial_bytes * b'\0') 85 | 86 | assert self._decryptor is not None 87 | return self._decryptor.update(data) 88 | 89 | 90 | class GCMCipher: 91 | """Shim for GCM ciphers""" 92 | 93 | def __init__(self, cipher_name: str, key: bytes, iv: bytes): 94 | self._cipher = _cipher_algs[cipher_name][0] 95 | self._key = key 96 | self._iv = iv 97 | 98 | def _update_iv(self) -> None: 99 | """Update the IV after each encrypt/decrypt operation""" 100 | 101 | invocation = int.from_bytes(self._iv[4:], 'big') 102 | invocation = (invocation + 1) & 0xffffffffffffffff 103 | self._iv = self._iv[:4] + invocation.to_bytes(8, 'big') 104 | 105 | def encrypt_and_sign(self, header: bytes, 106 | data: bytes) -> Tuple[bytes, bytes]: 107 | """Encrypt and sign a block of data""" 108 | 109 | data = AESGCM(self._key).encrypt(self._iv, data, header) 110 | 111 | self._update_iv() 112 | 113 | return header + data[:-_GCM_MAC_SIZE], data[-_GCM_MAC_SIZE:] 114 | 115 | def verify_and_decrypt(self, header: bytes, data: bytes, 116 | mac: bytes) -> Optional[bytes]: 117 | """Verify the signature of and decrypt a block of data""" 118 | 119 | try: 120 | decrypted_data: Optional[bytes] = \ 121 | AESGCM(self._key).decrypt(self._iv, data + mac, header) 122 | except InvalidTag: 123 | decrypted_data = None 124 | 125 | self._update_iv() 126 | 127 | return decrypted_data 128 | 129 | 130 | def register_cipher(cipher_name: str, key_size: int, 131 | iv_size: int, block_size: int) -> None: 132 | """Register a symmetric cipher""" 133 | 134 | _cipher_params[cipher_name] = (key_size, iv_size, block_size) 135 | 136 | 137 | def get_cipher_params(cipher_name: str) -> _CipherParams: 138 | """Get parameters of a symmetric cipher""" 139 | 140 | return _cipher_params[cipher_name] 141 | 142 | 143 | _cipher_alg_list = ( 144 | ('aes128-cbc', 'AES', CBC, 0, 16, 16, 16), 145 | ('aes192-cbc', 'AES', CBC, 0, 24, 16, 16), 146 | ('aes256-cbc', 'AES', CBC, 0, 32, 16, 16), 147 | ('aes128-ctr', 'AES', CTR, 0, 16, 16, 16), 148 | ('aes192-ctr', 'AES', CTR, 0, 24, 16, 16), 149 | ('aes256-ctr', 'AES', CTR, 0, 32, 16, 16), 150 | ('aes128-gcm', None, None, 0, 16, 12, 16), 151 | ('aes256-gcm', None, None, 0, 32, 12, 16), 152 | ('arcfour', 'ARC4', None, 0, 16, 1, 1), 153 | ('arcfour40', 'ARC4', None, 0, 5, 1, 1), 154 | ('arcfour128', 'ARC4', None, 1536, 16, 1, 1), 155 | ('arcfour256', 'ARC4', None, 1536, 32, 1, 1), 156 | ('blowfish-cbc', 'Blowfish', CBC, 0, 16, 8, 8), 157 | ('cast128-cbc', 'CAST5', CBC, 0, 16, 8, 8), 158 | ('des-cbc', 'TripleDES', CBC, 0, 8, 8, 8), 159 | ('des2-cbc', 'TripleDES', CBC, 0, 16, 8, 8), 160 | ('des3-cbc', 'TripleDES', CBC, 0, 24, 8, 8), 161 | ('seed-cbc', 'SEED', CBC, 0, 16, 16, 16) 162 | ) 163 | 164 | with warnings.catch_warnings(): 165 | warnings.simplefilter('ignore') 166 | 167 | for _cipher_name, _alg, _mode, _initial_bytes, \ 168 | _key_size, _iv_size, _block_size in _cipher_alg_list: 169 | if _alg: 170 | try: 171 | _cipher = getattr(_algs, _alg) 172 | except AttributeError as exc: # pragma: no cover 173 | if _decrepit_algs: 174 | try: 175 | _cipher = getattr(_decrepit_algs, _alg) 176 | except AttributeError: 177 | raise exc from None 178 | else: 179 | raise 180 | else: 181 | _cipher = None 182 | 183 | _cipher_algs[_cipher_name] = (_cipher, _mode, _initial_bytes) 184 | register_cipher(_cipher_name, _key_size, _iv_size, _block_size) 185 | -------------------------------------------------------------------------------- /asyncssh/crypto/dh.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """A shim around PyCA for Diffie Hellman key exchange""" 22 | 23 | from cryptography.hazmat.primitives.asymmetric import dh 24 | 25 | 26 | class DH: 27 | """A shim around PyCA for Diffie Hellman key exchange""" 28 | 29 | def __init__(self, g: int, p: int): 30 | self._pn = dh.DHParameterNumbers(p, g) 31 | self._priv_key = self._pn.parameters().generate_private_key() 32 | 33 | def get_public(self) -> int: 34 | """Return the public key to send in the handshake""" 35 | 36 | pub_key = self._priv_key.public_key() 37 | 38 | return pub_key.public_numbers().y 39 | 40 | def get_shared(self, peer_public: int) -> int: 41 | """Return the shared key from the peer's public key""" 42 | 43 | peer_key = dh.DHPublicNumbers(peer_public, self._pn).public_key() 44 | shared_key = self._priv_key.exchange(peer_key) 45 | 46 | return int.from_bytes(shared_key, 'big') 47 | -------------------------------------------------------------------------------- /asyncssh/crypto/dsa.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-2023 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """A shim around PyCA for DSA public and private keys""" 22 | 23 | from typing import Optional, cast 24 | 25 | from cryptography.exceptions import InvalidSignature 26 | from cryptography.hazmat.primitives.asymmetric import dsa 27 | 28 | from .misc import CryptoKey, PyCAKey, hashes 29 | 30 | 31 | # Short variable names are used here, matching names in the spec 32 | # pylint: disable=invalid-name 33 | 34 | 35 | class _DSAKey(CryptoKey): 36 | """Base class for shim around PyCA for DSA keys""" 37 | 38 | def __init__(self, pyca_key: PyCAKey, params: dsa.DSAParameterNumbers, 39 | pub: dsa.DSAPublicNumbers, 40 | priv: Optional[dsa.DSAPrivateNumbers] = None): 41 | super().__init__(pyca_key) 42 | 43 | self._params = params 44 | self._pub = pub 45 | self._priv = priv 46 | 47 | @property 48 | def p(self) -> int: 49 | """Return the DSA public modulus""" 50 | 51 | return self._params.p 52 | 53 | @property 54 | def q(self) -> int: 55 | """Return the DSA sub-group order""" 56 | 57 | return self._params.q 58 | 59 | @property 60 | def g(self) -> int: 61 | """Return the DSA generator""" 62 | 63 | return self._params.g 64 | 65 | @property 66 | def y(self) -> int: 67 | """Return the DSA public value""" 68 | 69 | return self._pub.y 70 | 71 | @property 72 | def x(self) -> Optional[int]: 73 | """Return the DSA private value""" 74 | 75 | return self._priv.x if self._priv else None 76 | 77 | 78 | class DSAPrivateKey(_DSAKey): 79 | """A shim around PyCA for DSA private keys""" 80 | 81 | @classmethod 82 | def construct(cls, p: int, q: int, g: int, 83 | y: int, x: int) -> 'DSAPrivateKey': 84 | """Construct a DSA private key""" 85 | 86 | params = dsa.DSAParameterNumbers(p, q, g) 87 | pub = dsa.DSAPublicNumbers(y, params) 88 | priv = dsa.DSAPrivateNumbers(x, pub) 89 | priv_key = priv.private_key() 90 | 91 | return cls(priv_key, params, pub, priv) 92 | 93 | @classmethod 94 | def generate(cls, key_size: int) -> 'DSAPrivateKey': 95 | """Generate a new DSA private key""" 96 | 97 | priv_key = dsa.generate_private_key(key_size) 98 | priv = priv_key.private_numbers() 99 | pub = priv.public_numbers 100 | params = pub.parameter_numbers 101 | 102 | return cls(priv_key, params, pub, priv) 103 | 104 | def sign(self, data: bytes, hash_name: str = '') -> bytes: 105 | """Sign a block of data""" 106 | 107 | priv_key = cast('dsa.DSAPrivateKey', self.pyca_key) 108 | return priv_key.sign(data, hashes[hash_name]()) 109 | 110 | 111 | class DSAPublicKey(_DSAKey): 112 | """A shim around PyCA for DSA public keys""" 113 | 114 | @classmethod 115 | def construct(cls, p: int, q: int, g: int, y: int) -> 'DSAPublicKey': 116 | """Construct a DSA public key""" 117 | 118 | params = dsa.DSAParameterNumbers(p, q, g) 119 | pub = dsa.DSAPublicNumbers(y, params) 120 | pub_key = pub.public_key() 121 | 122 | return cls(pub_key, params, pub) 123 | 124 | def verify(self, data: bytes, sig: bytes, hash_name: str = '') -> bool: 125 | """Verify the signature on a block of data""" 126 | 127 | try: 128 | pub_key = cast('dsa.DSAPublicKey', self.pyca_key) 129 | pub_key.verify(sig, data, hashes[hash_name]()) 130 | return True 131 | except InvalidSignature: 132 | return False 133 | -------------------------------------------------------------------------------- /asyncssh/crypto/ec_params.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2021 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """Functions for looking up named elliptic curves by their parameters""" 22 | 23 | _curve_param_map = {} 24 | 25 | # Short variable names are used here, matching names in the spec 26 | # pylint: disable=invalid-name 27 | 28 | 29 | def register_prime_curve(curve_id: bytes, p: int, a: int, b: int, 30 | point: bytes, n: int) -> None: 31 | """Register an elliptic curve prime domain 32 | 33 | This function registers an elliptic curve prime domain by 34 | specifying the SSH identifier for the curve and the set of 35 | parameters describing the curve, generator point, and order. 36 | This allows EC keys encoded with explicit parameters to be 37 | mapped back into their SSH curve IDs. 38 | 39 | """ 40 | 41 | _curve_param_map[p, a % p, b % p, point, n] = curve_id 42 | 43 | 44 | def lookup_ec_curve_by_params(p: int, a: int, b: int, 45 | point: bytes, n: int) -> bytes: 46 | """Look up an elliptic curve by its parameters 47 | 48 | This function looks up an elliptic curve by its parameters 49 | and returns the curve's name. 50 | 51 | """ 52 | 53 | try: 54 | return _curve_param_map[p, a % p, b % p, point, n] 55 | except (KeyError, ValueError): 56 | raise ValueError('Unknown elliptic curve parameters') from None 57 | 58 | 59 | # pylint: disable=line-too-long 60 | 61 | register_prime_curve(b'nistp521', 62 | 6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057151, 63 | -3, 64 | 1093849038073734274511112390766805569936207598951683748994586394495953116150735016013708737573759623248592132296706313309438452531591012912142327488478985984, 65 | b'\x04\x00\xc6\x85\x8e\x06\xb7\x04\x04\xe9\xcd\x9e>\xcbf#\x95\xb4B\x9cd\x819\x05?\xb5!\xf8(\xaf`kM=\xba\xa1K^w\xef\xe7Y(\xfe\x1d\xc1\'\xa2\xff\xa8\xde3H\xb3\xc1\x85jB\x9b\xf9~~1\xc2\xe5\xbdf\x01\x189)jx\x9a;\xc0\x04\\\x8a_\xb4,}\x1b\xd9\x98\xf5DIW\x9bDh\x17\xaf\xbd\x17\'>f,\x97\xeer\x99^\xf4&@\xc5P\xb9\x01?\xad\x07a5 and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """A shim around PyCA for key derivation functions""" 22 | 23 | from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC 24 | 25 | from .misc import hashes 26 | 27 | 28 | def pbkdf2_hmac(hash_name: str, passphrase: bytes, salt: bytes, 29 | count: int, key_size: int) -> bytes: 30 | """A shim around PyCA for PBKDF2 HMAC key derivation""" 31 | 32 | return PBKDF2HMAC(hashes[hash_name](), key_size, salt, 33 | count).derive(passphrase) 34 | -------------------------------------------------------------------------------- /asyncssh/crypto/misc.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2023 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """Miscellaneous PyCA utility classes and functions""" 22 | 23 | from typing import Callable, Mapping, Union 24 | 25 | from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa 26 | from cryptography.hazmat.primitives.asymmetric import ed25519, ed448 27 | from cryptography.hazmat.primitives.hashes import HashAlgorithm 28 | from cryptography.hazmat.primitives.hashes import MD5, SHA1, SHA224 29 | from cryptography.hazmat.primitives.hashes import SHA256, SHA384, SHA512 30 | 31 | 32 | PyCAPrivateKey = Union[dsa.DSAPrivateKey, rsa.RSAPrivateKey, 33 | ec.EllipticCurvePrivateKey, 34 | ed25519.Ed25519PrivateKey, ed448.Ed448PrivateKey] 35 | 36 | PyCAPublicKey = Union[dsa.DSAPublicKey, rsa.RSAPublicKey, 37 | ec.EllipticCurvePublicKey, 38 | ed25519.Ed25519PublicKey, ed448.Ed448PublicKey] 39 | 40 | PyCAKey = Union[PyCAPrivateKey, PyCAPublicKey] 41 | 42 | 43 | hashes: Mapping[str, Callable[[], HashAlgorithm]] = { 44 | str(h.name): h for h in (MD5, SHA1, SHA224, SHA256, SHA384, SHA512) 45 | } 46 | 47 | 48 | class CryptoKey: 49 | """Base class for PyCA private/public keys""" 50 | 51 | def __init__(self, pyca_key: PyCAKey): 52 | self._pyca_key = pyca_key 53 | 54 | @property 55 | def pyca_key(self) -> PyCAKey: 56 | """Return the PyCA object associated with this key""" 57 | 58 | return self._pyca_key 59 | 60 | def sign(self, data: bytes, hash_name: str = '') -> bytes: 61 | """Sign a block of data""" 62 | 63 | # pylint: disable=no-self-use 64 | raise RuntimeError # pragma: no cover 65 | 66 | def verify(self, data: bytes, sig: bytes, hash_name: str = '') -> bool: 67 | """Verify the signature on a block of data""" 68 | 69 | # pylint: disable=no-self-use 70 | raise RuntimeError # pragma: no cover 71 | -------------------------------------------------------------------------------- /asyncssh/crypto/pq.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2024 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """A shim around liboqs for Streamlined NTRU Prime post-quantum encryption""" 22 | 23 | import ctypes 24 | import ctypes.util 25 | from typing import Mapping, Tuple 26 | 27 | 28 | _pq_algs: Mapping[bytes, Tuple[int, int, int, int, str]] = { 29 | b'mlkem768': (1184, 2400, 1088, 32, 'KEM_ml_kem_768'), 30 | b'mlkem1024': (1568, 3168, 1568, 32, 'KEM_ml_kem_1024'), 31 | b'sntrup761': (1158, 1763, 1039, 32, 'KEM_ntruprime_sntrup761') 32 | } 33 | 34 | mlkem_available = False 35 | sntrup_available = False 36 | 37 | for lib in ('oqs', 'liboqs'): 38 | _oqs_lib = ctypes.util.find_library(lib) 39 | 40 | if _oqs_lib: # pragma: no branch 41 | break 42 | else: # pragma: no cover 43 | _oqs_lib = None 44 | 45 | if _oqs_lib: # pragma: no branch 46 | _oqs = ctypes.cdll.LoadLibrary(_oqs_lib) 47 | 48 | mlkem_available = (hasattr(_oqs, 'OQS_KEM_ml_kem_768_keypair') or 49 | hasattr(_oqs, 'OQS_KEM_ml_kem_768_ipd_keypair')) 50 | sntrup_available = hasattr(_oqs, 'OQS_KEM_ntruprime_sntrup761_keypair') 51 | 52 | 53 | class PQDH: 54 | """A shim around liboqs for post-quantum key exchange algorithms""" 55 | 56 | def __init__(self, alg_name: bytes): 57 | try: 58 | self.pubkey_bytes, self.privkey_bytes, \ 59 | self.ciphertext_bytes, self.secret_bytes, \ 60 | oqs_name = _pq_algs[alg_name] 61 | except KeyError: # pragma: no cover, other algs not registered 62 | raise ValueError(f'Unknown PQ algorithm {oqs_name}') from None 63 | 64 | if not hasattr(_oqs, 'OQS_' + oqs_name + '_keypair'): # pragma: no cover 65 | oqs_name += '_ipd' 66 | 67 | self._keypair = getattr(_oqs, 'OQS_' + oqs_name + '_keypair') 68 | self._encaps = getattr(_oqs, 'OQS_' + oqs_name + '_encaps') 69 | self._decaps = getattr(_oqs, 'OQS_' + oqs_name + '_decaps') 70 | 71 | def keypair(self) -> Tuple[bytes, bytes]: 72 | """Make a new key pair""" 73 | 74 | pubkey = ctypes.create_string_buffer(self.pubkey_bytes) 75 | privkey = ctypes.create_string_buffer(self.privkey_bytes) 76 | self._keypair(pubkey, privkey) 77 | 78 | return pubkey.raw, privkey.raw 79 | 80 | def encaps(self, pubkey: bytes) -> Tuple[bytes, bytes]: 81 | """Generate a random secret and encrypt it with a public key""" 82 | 83 | if len(pubkey) != self.pubkey_bytes: 84 | raise ValueError('Invalid public key') 85 | 86 | ciphertext = ctypes.create_string_buffer(self.ciphertext_bytes) 87 | secret = ctypes.create_string_buffer(self.secret_bytes) 88 | 89 | self._encaps(ciphertext, secret, pubkey) 90 | 91 | return secret.raw, ciphertext.raw 92 | 93 | def decaps(self, ciphertext: bytes, privkey: bytes) -> bytes: 94 | """Decrypt an encrypted secret using a private key""" 95 | 96 | if len(ciphertext) != self.ciphertext_bytes: 97 | raise ValueError('Invalid ciphertext') 98 | 99 | secret = ctypes.create_string_buffer(self.secret_bytes) 100 | 101 | self._decaps(secret, ciphertext, privkey) 102 | 103 | return secret.raw 104 | -------------------------------------------------------------------------------- /asyncssh/crypto/rsa.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-2023 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """A shim around PyCA for RSA public and private keys""" 22 | 23 | from typing import Optional, cast 24 | 25 | from cryptography.exceptions import InvalidSignature 26 | from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP 27 | from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 28 | from cryptography.hazmat.primitives.asymmetric import rsa 29 | 30 | from .misc import CryptoKey, PyCAKey, hashes 31 | 32 | 33 | # Short variable names are used here, matching names in the spec 34 | # pylint: disable=invalid-name 35 | 36 | 37 | class _RSAKey(CryptoKey): 38 | """Base class for shim around PyCA for RSA keys""" 39 | 40 | def __init__(self, pyca_key: PyCAKey, pub: rsa.RSAPublicNumbers, 41 | priv: Optional[rsa.RSAPrivateNumbers] = None): 42 | super().__init__(pyca_key) 43 | 44 | self._pub = pub 45 | self._priv = priv 46 | 47 | @property 48 | def n(self) -> int: 49 | """Return the RSA public modulus""" 50 | 51 | return self._pub.n 52 | 53 | @property 54 | def e(self) -> int: 55 | """Return the RSA public exponent""" 56 | 57 | return self._pub.e 58 | 59 | @property 60 | def d(self) -> Optional[int]: 61 | """Return the RSA private exponent""" 62 | 63 | return self._priv.d if self._priv else None 64 | 65 | @property 66 | def p(self) -> Optional[int]: 67 | """Return the RSA first private prime""" 68 | 69 | return self._priv.p if self._priv else None 70 | 71 | @property 72 | def q(self) -> Optional[int]: 73 | """Return the RSA second private prime""" 74 | 75 | return self._priv.q if self._priv else None 76 | 77 | @property 78 | def dmp1(self) -> Optional[int]: 79 | """Return d modulo p-1""" 80 | 81 | return self._priv.dmp1 if self._priv else None 82 | 83 | @property 84 | def dmq1(self) -> Optional[int]: 85 | """Return q modulo p-1""" 86 | 87 | return self._priv.dmq1 if self._priv else None 88 | 89 | @property 90 | def iqmp(self) -> Optional[int]: 91 | """Return the inverse of q modulo p""" 92 | 93 | return self._priv.iqmp if self._priv else None 94 | 95 | 96 | class RSAPrivateKey(_RSAKey): 97 | """A shim around PyCA for RSA private keys""" 98 | 99 | @classmethod 100 | def construct(cls, n: int, e: int, d: int, p: int, q: int, 101 | dmp1: int, dmq1: int, iqmp: int, 102 | skip_validation: bool) -> 'RSAPrivateKey': 103 | """Construct an RSA private key""" 104 | 105 | pub = rsa.RSAPublicNumbers(e, n) 106 | priv = rsa.RSAPrivateNumbers(p, q, d, dmp1, dmq1, iqmp, pub) 107 | priv_key = priv.private_key( 108 | unsafe_skip_rsa_key_validation=skip_validation) 109 | 110 | return cls(priv_key, pub, priv) 111 | 112 | @classmethod 113 | def generate(cls, key_size: int, exponent: int) -> 'RSAPrivateKey': 114 | """Generate a new RSA private key""" 115 | 116 | priv_key = rsa.generate_private_key(exponent, key_size) 117 | priv = priv_key.private_numbers() 118 | pub = priv.public_numbers 119 | 120 | return cls(priv_key, pub, priv) 121 | 122 | def decrypt(self, data: bytes, hash_name: str) -> Optional[bytes]: 123 | """Decrypt a block of data""" 124 | 125 | try: 126 | hash_alg = hashes[hash_name]() 127 | priv_key = cast('rsa.RSAPrivateKey', self.pyca_key) 128 | return priv_key.decrypt(data, OAEP(MGF1(hash_alg), hash_alg, None)) 129 | except ValueError: 130 | return None 131 | 132 | def sign(self, data: bytes, hash_name: str = '') -> bytes: 133 | """Sign a block of data""" 134 | 135 | priv_key = cast('rsa.RSAPrivateKey', self.pyca_key) 136 | return priv_key.sign(data, PKCS1v15(), hashes[hash_name]()) 137 | 138 | 139 | class RSAPublicKey(_RSAKey): 140 | """A shim around PyCA for RSA public keys""" 141 | 142 | @classmethod 143 | def construct(cls, n: int, e: int) -> 'RSAPublicKey': 144 | """Construct an RSA public key""" 145 | 146 | pub = rsa.RSAPublicNumbers(e, n) 147 | pub_key = pub.public_key() 148 | 149 | return cls(pub_key, pub) 150 | 151 | def encrypt(self, data: bytes, hash_name: str) -> Optional[bytes]: 152 | """Encrypt a block of data""" 153 | 154 | try: 155 | hash_alg = hashes[hash_name]() 156 | pub_key = cast('rsa.RSAPublicKey', self.pyca_key) 157 | return pub_key.encrypt(data, OAEP(MGF1(hash_alg), hash_alg, None)) 158 | except ValueError: 159 | return None 160 | 161 | def verify(self, data: bytes, sig: bytes, hash_name: str = '') -> bool: 162 | """Verify the signature on a block of data""" 163 | 164 | try: 165 | pub_key = cast('rsa.RSAPublicKey', self.pyca_key) 166 | pub_key.verify(sig, data, PKCS1v15(), hashes[hash_name]()) 167 | return True 168 | except InvalidSignature: 169 | return False 170 | -------------------------------------------------------------------------------- /asyncssh/crypto/umac.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016-2024 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """UMAC cryptographic hash (RFC 4418) wrapper for Nettle library""" 22 | 23 | import binascii 24 | import ctypes 25 | import ctypes.util 26 | from typing import TYPE_CHECKING, Callable, Optional 27 | 28 | 29 | if TYPE_CHECKING: 30 | _ByteArray = ctypes.Array[ctypes.c_char] 31 | _SetKey = Callable[[_ByteArray, bytes], None] 32 | _SetNonce = Callable[[_ByteArray, ctypes.c_size_t, bytes], None] 33 | _Update = Callable[[_ByteArray, ctypes.c_size_t, bytes], None] 34 | _Digest = Callable[[_ByteArray, ctypes.c_size_t, _ByteArray], None] 35 | _New = Callable[[bytes, Optional[bytes], Optional[bytes]], object] 36 | 37 | 38 | _UMAC_BLOCK_SIZE = 1024 39 | _UMAC_DEFAULT_CTX_SIZE = 4096 40 | 41 | 42 | def _build_umac(size: int) -> '_New': 43 | """Function to build UMAC wrapper for a specific digest size""" 44 | 45 | _name = f'umac{size}' 46 | _prefix = f'nettle_{_name}_' 47 | 48 | try: 49 | _context_size: int = getattr(_nettle, _prefix + '_ctx_size')() 50 | except AttributeError: 51 | _context_size = _UMAC_DEFAULT_CTX_SIZE 52 | 53 | _set_key: _SetKey = getattr(_nettle, _prefix + 'set_key') 54 | _set_nonce: _SetNonce = getattr(_nettle, _prefix + 'set_nonce') 55 | _update: _Update = getattr(_nettle, _prefix + 'update') 56 | _digest: _Digest = getattr(_nettle, _prefix + 'digest') 57 | 58 | 59 | class _UMAC: 60 | """Wrapper for UMAC cryptographic hash 61 | 62 | This class supports the cryptographic hash API defined in PEP 452. 63 | 64 | """ 65 | 66 | name = _name 67 | block_size = _UMAC_BLOCK_SIZE 68 | digest_size = size // 8 69 | 70 | def __init__(self, ctx: '_ByteArray', nonce: Optional[bytes] = None, 71 | msg: Optional[bytes] = None): 72 | self._ctx = ctx 73 | 74 | if nonce: 75 | self.set_nonce(nonce) 76 | 77 | if msg: 78 | self.update(msg) 79 | 80 | @classmethod 81 | def new(cls, key: bytes, msg: Optional[bytes] = None, 82 | nonce: Optional[bytes] = None) -> '_UMAC': 83 | """Construct a new UMAC hash object""" 84 | 85 | ctx = ctypes.create_string_buffer(_context_size) 86 | _set_key(ctx, key) 87 | 88 | return cls(ctx, nonce, msg) 89 | 90 | def copy(self) -> '_UMAC': 91 | """Return a new hash object with this object's state""" 92 | 93 | ctx = ctypes.create_string_buffer(self._ctx.raw) 94 | return self.__class__(ctx) 95 | 96 | def set_nonce(self, nonce: bytes) -> None: 97 | """Reset the nonce associated with this object""" 98 | 99 | _set_nonce(self._ctx, ctypes.c_size_t(len(nonce)), nonce) 100 | 101 | def update(self, msg: bytes) -> None: 102 | """Add the data in msg to the hash""" 103 | 104 | _update(self._ctx, ctypes.c_size_t(len(msg)), msg) 105 | 106 | def digest(self) -> bytes: 107 | """Return the hash and increment nonce to begin a new message 108 | 109 | .. note:: The hash is reset and the nonce is incremented 110 | when this function is called. This doesn't match 111 | the behavior defined in PEP 452. 112 | 113 | """ 114 | 115 | result = ctypes.create_string_buffer(self.digest_size) 116 | _digest(self._ctx, ctypes.c_size_t(self.digest_size), result) 117 | return result.raw 118 | 119 | def hexdigest(self) -> str: 120 | """Return the digest as a string of hexadecimal digits""" 121 | 122 | return binascii.b2a_hex(self.digest()).decode('ascii') 123 | 124 | 125 | return _UMAC.new 126 | 127 | 128 | for lib in ('nettle', 'libnettle', 'libnettle-6'): 129 | _nettle_lib = ctypes.util.find_library(lib) 130 | 131 | if _nettle_lib: # pragma: no branch 132 | break 133 | else: # pragma: no cover 134 | _nettle_lib = None 135 | 136 | if _nettle_lib: # pragma: no branch 137 | _nettle = ctypes.cdll.LoadLibrary(_nettle_lib) 138 | 139 | umac32, umac64, umac96, umac128 = map(_build_umac, (32, 64, 96, 128)) 140 | -------------------------------------------------------------------------------- /asyncssh/gss.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2024 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """GSSAPI wrapper""" 22 | 23 | import sys 24 | 25 | from typing import Optional 26 | 27 | from .misc import BytesOrStrDict 28 | 29 | 30 | try: 31 | # pylint: disable=unused-import 32 | 33 | if sys.platform == 'win32': # pragma: no cover 34 | from .gss_win32 import GSSBase, GSSClient, GSSServer, GSSError 35 | else: 36 | from .gss_unix import GSSBase, GSSClient, GSSServer, GSSError 37 | 38 | gss_available = True 39 | except ImportError: # pragma: no cover 40 | gss_available = False 41 | 42 | class GSSError(ValueError): # type: ignore 43 | """Stub class for reporting that GSS is not available""" 44 | 45 | def __init__(self, maj_code: int, min_code: int, 46 | token: Optional[bytes] = None): 47 | super().__init__('GSS not available') 48 | 49 | self.maj_code = maj_code 50 | self.min_code = min_code 51 | self.token = token 52 | 53 | class GSSBase: # type: ignore 54 | """Base class for reporting that GSS is not available""" 55 | 56 | class GSSClient(GSSBase): # type: ignore 57 | """Stub client class for reporting that GSS is not available""" 58 | 59 | def __init__(self, _host: str, _store: Optional[BytesOrStrDict], 60 | _delegate_creds: bool): 61 | raise GSSError(0, 0) 62 | 63 | class GSSServer(GSSBase): # type: ignore 64 | """Stub client class for reporting that GSS is not available""" 65 | 66 | def __init__(self, _host: str, _store: Optional[BytesOrStrDict]): 67 | raise GSSError(0, 0) 68 | -------------------------------------------------------------------------------- /asyncssh/gss_unix.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2022 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """GSSAPI wrapper for UNIX""" 22 | 23 | from typing import Optional, Sequence, SupportsBytes, cast 24 | 25 | from gssapi import Credentials, Name, NameType, OID 26 | from gssapi import RequirementFlag, SecurityContext 27 | from gssapi.exceptions import GSSError 28 | 29 | from .asn1 import OBJECT_IDENTIFIER 30 | from .misc import BytesOrStrDict 31 | 32 | 33 | def _mech_to_oid(mech: OID) -> bytes: 34 | """Return a DER-encoded OID corresponding to the requested GSS mechanism""" 35 | 36 | mech_bytes = bytes(cast(SupportsBytes, mech)) 37 | return bytes((OBJECT_IDENTIFIER, len(mech_bytes))) + mech_bytes 38 | 39 | 40 | class GSSBase: 41 | """GSS base class""" 42 | 43 | def __init__(self, host: str, store: Optional[BytesOrStrDict]): 44 | if '@' in host: 45 | self._host = Name(host) 46 | else: 47 | self._host = Name('host@' + host, NameType.hostbased_service) 48 | 49 | self._store = store 50 | 51 | self._mechs = [_mech_to_oid(mech) for mech in self._creds.mechs] 52 | self._ctx: Optional[SecurityContext] = None 53 | 54 | @property 55 | def _creds(self) -> Credentials: 56 | """Abstract method to construct GSS credentials""" 57 | 58 | raise NotImplementedError 59 | 60 | def _init_context(self) -> None: 61 | """Abstract method to construct GSS security context""" 62 | 63 | raise NotImplementedError 64 | 65 | @property 66 | def mechs(self) -> Sequence[bytes]: 67 | """Return GSS mechanisms available for this host""" 68 | 69 | return self._mechs 70 | 71 | @property 72 | def complete(self) -> bool: 73 | """Return whether or not GSS negotiation is complete""" 74 | 75 | return self._ctx.complete if self._ctx else False 76 | 77 | @property 78 | def provides_mutual_auth(self) -> bool: 79 | """Return whether or not this context provides mutual authentication""" 80 | 81 | assert self._ctx is not None 82 | 83 | return bool(self._ctx.actual_flags & 84 | RequirementFlag.mutual_authentication) 85 | 86 | @property 87 | def provides_integrity(self) -> bool: 88 | """Return whether or not this context provides integrity protection""" 89 | 90 | assert self._ctx is not None 91 | 92 | return bool(self._ctx.actual_flags & RequirementFlag.integrity) 93 | 94 | @property 95 | def user(self) -> str: 96 | """Return user principal associated with this context""" 97 | 98 | assert self._ctx is not None 99 | 100 | return str(self._ctx.initiator_name) 101 | 102 | @property 103 | def host(self) -> str: 104 | """Return host principal associated with this context""" 105 | 106 | assert self._ctx is not None 107 | 108 | return str(self._ctx.target_name) 109 | 110 | def reset(self) -> None: 111 | """Reset GSS security context""" 112 | 113 | self._ctx = None 114 | 115 | def step(self, token: Optional[bytes] = None) -> Optional[bytes]: 116 | """Perform next step in GSS security exchange""" 117 | 118 | if not self._ctx: 119 | self._init_context() 120 | 121 | assert self._ctx is not None 122 | 123 | return self._ctx.step(token) 124 | 125 | def sign(self, data: bytes) -> bytes: 126 | """Sign a block of data""" 127 | 128 | assert self._ctx is not None 129 | 130 | return self._ctx.get_signature(data) 131 | 132 | def verify(self, data: bytes, sig: bytes) -> bool: 133 | """Verify a signature for a block of data""" 134 | 135 | assert self._ctx is not None 136 | 137 | try: 138 | self._ctx.verify_signature(data, sig) 139 | return True 140 | except GSSError: 141 | return False 142 | 143 | 144 | class GSSClient(GSSBase): 145 | """GSS client""" 146 | 147 | def __init__(self, host: str, store: Optional[BytesOrStrDict], 148 | delegate_creds: bool): 149 | super().__init__(host, store) 150 | 151 | flags = RequirementFlag.mutual_authentication | \ 152 | RequirementFlag.integrity 153 | 154 | if delegate_creds: 155 | flags |= RequirementFlag.delegate_to_peer 156 | 157 | self._flags = flags 158 | 159 | @property 160 | def _creds(self) -> Credentials: 161 | """Abstract method to construct GSS credentials""" 162 | 163 | return Credentials(usage='initiate', store=self._store) 164 | 165 | def _init_context(self) -> None: 166 | """Construct GSS client security context""" 167 | 168 | self._ctx = SecurityContext(name=self._host, creds=self._creds, 169 | flags=self._flags) 170 | 171 | 172 | class GSSServer(GSSBase): 173 | """GSS server""" 174 | 175 | @property 176 | def _creds(self) -> Credentials: 177 | """Abstract method to construct GSS credentials""" 178 | 179 | return Credentials(name=self._host, usage='accept', store=self._store) 180 | 181 | def _init_context(self) -> None: 182 | """Construct GSS server security context""" 183 | 184 | self._ctx = SecurityContext(creds=self._creds) 185 | -------------------------------------------------------------------------------- /asyncssh/gss_win32.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2023 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """GSSAPI wrapper for Windows""" 22 | 23 | # Some of the imports below won't be found when running pylint on UNIX 24 | # pylint: disable=import-error 25 | 26 | from typing import Optional, Sequence, Union 27 | 28 | from sspi import ClientAuth, ServerAuth 29 | from sspi import error as SSPIError 30 | 31 | from sspicon import ISC_REQ_DELEGATE, ISC_REQ_INTEGRITY, ISC_REQ_MUTUAL_AUTH 32 | from sspicon import ISC_RET_INTEGRITY, ISC_RET_MUTUAL_AUTH 33 | from sspicon import ASC_REQ_INTEGRITY, ASC_REQ_MUTUAL_AUTH 34 | from sspicon import ASC_RET_INTEGRITY, ASC_RET_MUTUAL_AUTH 35 | from sspicon import SECPKG_ATTR_NATIVE_NAMES 36 | 37 | from .asn1 import ObjectIdentifier, der_encode 38 | from .misc import BytesOrStrDict 39 | 40 | 41 | _krb5_oid = der_encode(ObjectIdentifier('1.2.840.113554.1.2.2')) 42 | 43 | 44 | class GSSBase: 45 | """GSS base class""" 46 | 47 | # Overridden in child classes 48 | _mutual_auth_flag = 0 49 | _integrity_flag = 0 50 | 51 | def __init__(self, host: str): 52 | if '@' in host: 53 | self._host = host 54 | else: 55 | self._host = 'host/' + host 56 | 57 | self._ctx: Optional[Union[ClientAuth, ServerAuth]] = None 58 | self._init_token: Optional[bytes] = None 59 | 60 | @property 61 | def mechs(self) -> Sequence[bytes]: 62 | """Return GSS mechanisms available for this host""" 63 | 64 | return [_krb5_oid] 65 | 66 | @property 67 | def complete(self) -> bool: 68 | """Return whether or not GSS negotiation is complete""" 69 | 70 | assert self._ctx is not None 71 | 72 | return self._ctx.authenticated 73 | 74 | @property 75 | def provides_mutual_auth(self) -> bool: 76 | """Return whether or not this context provides mutual authentication""" 77 | 78 | assert self._ctx is not None 79 | 80 | return bool(self._ctx.ctxt_attr & self._mutual_auth_flag) 81 | 82 | @property 83 | def provides_integrity(self) -> bool: 84 | """Return whether or not this context provides integrity protection""" 85 | 86 | assert self._ctx is not None 87 | 88 | return bool(self._ctx.ctxt_attr & self._integrity_flag) 89 | 90 | @property 91 | def user(self) -> str: 92 | """Return user principal associated with this context""" 93 | 94 | assert self._ctx is not None 95 | 96 | names = self._ctx.ctxt.QueryContextAttributes(SECPKG_ATTR_NATIVE_NAMES) 97 | return names[0] 98 | 99 | @property 100 | def host(self) -> str: 101 | """Return host principal associated with this context""" 102 | 103 | assert self._ctx is not None 104 | 105 | names = self._ctx.ctxt.QueryContextAttributes(SECPKG_ATTR_NATIVE_NAMES) 106 | return names[1] 107 | 108 | def reset(self) -> None: 109 | """Reset GSS security context""" 110 | 111 | assert self._ctx is not None 112 | 113 | self._ctx.reset() 114 | self._init_token = None 115 | 116 | def step(self, token: Optional[bytes] = None) -> Optional[bytes]: 117 | """Perform next step in GSS security exchange""" 118 | 119 | assert self._ctx is not None 120 | 121 | if self._init_token: 122 | token = self._init_token 123 | self._init_token = None 124 | return token 125 | 126 | try: 127 | _, buf = self._ctx.authorize(token) 128 | return buf[0].Buffer 129 | except SSPIError as exc: 130 | raise GSSError(details=exc.strerror) from None 131 | 132 | def sign(self, data: bytes) -> bytes: 133 | """Sign a block of data""" 134 | 135 | assert self._ctx is not None 136 | 137 | try: 138 | return self._ctx.sign(data) 139 | except SSPIError as exc: # pragna: no cover 140 | raise GSSError(details=exc.strerror) from None 141 | 142 | def verify(self, data: bytes, sig: bytes) -> bool: 143 | """Verify a signature for a block of data""" 144 | 145 | assert self._ctx is not None 146 | 147 | try: 148 | self._ctx.verify(data, sig) 149 | return True 150 | except SSPIError: 151 | return False 152 | 153 | 154 | class GSSClient(GSSBase): 155 | """GSS client""" 156 | 157 | _mutual_auth_flag = ISC_RET_MUTUAL_AUTH 158 | _integrity_flag = ISC_RET_INTEGRITY 159 | 160 | def __init__(self, host: str, store: Optional[BytesOrStrDict], 161 | delegate_creds: bool): 162 | if store is not None: # pragna: no cover 163 | raise GSSError(details='GSS store not supported on Windows') 164 | 165 | super().__init__(host) 166 | 167 | flags = ISC_REQ_MUTUAL_AUTH | ISC_REQ_INTEGRITY 168 | 169 | if delegate_creds: 170 | flags |= ISC_REQ_DELEGATE 171 | 172 | try: 173 | self._ctx = ClientAuth('Kerberos', targetspn=self._host, 174 | scflags=flags) 175 | except SSPIError as exc: # pragna: no cover 176 | raise GSSError(1, 1, details=exc.strerror) from None 177 | 178 | self._init_token = self.step(None) 179 | 180 | 181 | class GSSServer(GSSBase): 182 | """GSS server""" 183 | 184 | _mutual_auth_flag = ASC_RET_MUTUAL_AUTH 185 | _integrity_flag = ASC_RET_INTEGRITY 186 | 187 | def __init__(self, host: str, store: Optional[BytesOrStrDict]): 188 | if store is not None: # pragna: no cover 189 | raise GSSError(details='GSS store not supported on Windows') 190 | 191 | super().__init__(host) 192 | 193 | flags = ASC_REQ_MUTUAL_AUTH | ASC_REQ_INTEGRITY 194 | 195 | try: 196 | self._ctx = ServerAuth('Kerberos', spn=self._host, scflags=flags) 197 | except SSPIError as exc: 198 | raise GSSError(1, 1, details=exc.strerror) from None 199 | 200 | 201 | class GSSError(Exception): 202 | """Class for reporting GSS errors""" 203 | 204 | def __init__(self, maj_code: int = 0, min_code: int = 0, 205 | token: Optional[bytes] = None, details: str = ''): 206 | super().__init__(details) 207 | 208 | self.maj_code = maj_code 209 | self.min_code = min_code 210 | self.token = token 211 | -------------------------------------------------------------------------------- /asyncssh/kex.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2024 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """SSH key exchange handlers""" 22 | 23 | import binascii 24 | from hashlib import md5 25 | from typing import TYPE_CHECKING, Dict, List, Sequence, Tuple, Type 26 | 27 | from .logging import SSHLogger 28 | from .misc import HashType 29 | from .packet import SSHPacketHandler 30 | 31 | 32 | if TYPE_CHECKING: 33 | # pylint: disable=cyclic-import 34 | from .connection import SSHConnection 35 | 36 | 37 | _KexAlgList = List[bytes] 38 | _KexAlgMap = Dict[bytes, Tuple[Type['Kex'], HashType, object]] 39 | 40 | 41 | _kex_algs: _KexAlgList = [] 42 | _default_kex_algs:_KexAlgList = [] 43 | _kex_handlers: _KexAlgMap = {} 44 | 45 | _gss_kex_algs: _KexAlgList = [] 46 | _default_gss_kex_algs: _KexAlgList = [] 47 | _gss_kex_handlers: _KexAlgMap = {} 48 | 49 | 50 | class Kex(SSHPacketHandler): 51 | """Parent class for key exchange handlers""" 52 | 53 | def __init__(self, alg: bytes, conn: 'SSHConnection', hash_alg: HashType): 54 | self.algorithm = alg 55 | 56 | self._conn = conn 57 | self._logger = conn.logger 58 | self._hash_alg = hash_alg 59 | 60 | 61 | async def start(self) -> None: 62 | """Start key exchange""" 63 | 64 | raise NotImplementedError 65 | 66 | def send_packet(self, pkttype: int, *args: bytes) -> None: 67 | """Send a kex packet""" 68 | 69 | self._conn.send_packet(pkttype, *args, handler=self) 70 | 71 | @property 72 | def logger(self) -> SSHLogger: 73 | """A logger associated with this connection""" 74 | 75 | return self._logger 76 | 77 | def compute_key(self, k: bytes, h: bytes, x: bytes, 78 | session_id: bytes, keylen: int) -> bytes: 79 | """Compute keys from output of key exchange""" 80 | 81 | key = b'' 82 | while len(key) < keylen: 83 | hash_obj = self._hash_alg() 84 | hash_obj.update(k) 85 | hash_obj.update(h) 86 | hash_obj.update(key if key else x + session_id) 87 | key += hash_obj.digest() 88 | 89 | return key[:keylen] 90 | 91 | 92 | def register_kex_alg(alg: bytes, handler: Type[Kex], hash_alg: HashType, 93 | args: Tuple, default: bool) -> None: 94 | """Register a key exchange algorithm""" 95 | 96 | _kex_algs.append(alg) 97 | 98 | if default: 99 | _default_kex_algs.append(alg) 100 | 101 | _kex_handlers[alg] = (handler, hash_alg, args) 102 | 103 | 104 | def register_gss_kex_alg(alg: bytes, handler: Type[Kex], hash_alg: HashType, 105 | args: Tuple, default: bool) -> None: 106 | """Register a GSSAPI key exchange algorithm""" 107 | 108 | _gss_kex_algs.append(alg) 109 | 110 | if default: 111 | _default_gss_kex_algs.append(alg) 112 | 113 | _gss_kex_handlers[alg] = (handler, hash_alg, args) 114 | 115 | 116 | def get_kex_algs() -> List[bytes]: 117 | """Return supported key exchange algorithms""" 118 | 119 | return _gss_kex_algs + _kex_algs 120 | 121 | 122 | def get_default_kex_algs() -> List[bytes]: 123 | """Return default key exchange algorithms""" 124 | 125 | return _default_gss_kex_algs + _default_kex_algs 126 | 127 | 128 | def expand_kex_algs(kex_algs: Sequence[bytes], mechs: Sequence[bytes], 129 | host_key_available: bool) -> List[bytes]: 130 | """Add mechanisms to GSS entries in key exchange algorithm list""" 131 | 132 | expanded_kex_algs: List[bytes] = [] 133 | 134 | for alg in kex_algs: 135 | if alg.startswith(b'gss-'): 136 | for mech in mechs: 137 | suffix = b'-' + binascii.b2a_base64(md5(mech).digest())[:-1] 138 | expanded_kex_algs.append(alg + suffix) 139 | elif host_key_available: 140 | expanded_kex_algs.append(alg) 141 | 142 | return expanded_kex_algs 143 | 144 | 145 | def get_kex(conn: 'SSHConnection', alg: bytes) -> Kex: 146 | """Return a key exchange handler 147 | 148 | The function looks up a key exchange algorithm and returns a 149 | handler which can perform that type of key exchange. 150 | 151 | """ 152 | 153 | if alg.startswith(b'gss-'): 154 | alg = alg.rsplit(b'-', 1)[0] 155 | handler, hash_alg, args = _gss_kex_handlers[alg] 156 | else: 157 | handler, hash_alg, args = _kex_handlers[alg] 158 | 159 | return handler(alg, conn, hash_alg, *args) 160 | -------------------------------------------------------------------------------- /asyncssh/kex_rsa.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018-2024 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """RSA key exchange handler""" 22 | 23 | from hashlib import sha1, sha256 24 | from typing import TYPE_CHECKING, Optional, cast 25 | 26 | from .kex import Kex, register_kex_alg 27 | from .misc import HashType, KeyExchangeFailed, ProtocolError 28 | from .misc import get_symbol_names, randrange 29 | from .packet import MPInt, String, SSHPacket 30 | from .public_key import KeyImportError, SSHKey 31 | from .public_key import decode_ssh_public_key, generate_private_key 32 | from .rsa import RSAKey 33 | 34 | 35 | if TYPE_CHECKING: 36 | # pylint: disable=cyclic-import 37 | from .connection import SSHConnection, SSHClientConnection 38 | from .connection import SSHServerConnection 39 | 40 | 41 | # SSH KEXRSA message values 42 | MSG_KEXRSA_PUBKEY = 30 43 | MSG_KEXRSA_SECRET = 31 44 | MSG_KEXRSA_DONE = 32 45 | 46 | 47 | class _KexRSA(Kex): 48 | """Handler for RSA key exchange""" 49 | 50 | _handler_names = get_symbol_names(globals(), 'MSG_KEXRSA') 51 | 52 | def __init__(self, alg: bytes, conn: 'SSHConnection', hash_alg: HashType, 53 | key_size: int, hash_size: int): 54 | super().__init__(alg, conn, hash_alg) 55 | 56 | self._key_size = key_size 57 | self._k_limit = 1 << (key_size - 2*hash_size - 49) 58 | 59 | self._host_key_data = b'' 60 | 61 | self._trans_key: Optional[SSHKey] = None 62 | self._trans_key_data = b'' 63 | 64 | self._k = 0 65 | self._encrypted_k = b'' 66 | 67 | async def start(self) -> None: 68 | """Start RSA key exchange""" 69 | 70 | if self._conn.is_server(): 71 | server_conn = cast('SSHServerConnection', self._conn) 72 | host_key = server_conn.get_server_host_key() 73 | assert host_key is not None 74 | self._host_key_data = host_key.public_data 75 | 76 | self._trans_key = generate_private_key( 77 | 'ssh-rsa', key_size=self._key_size) 78 | self._trans_key_data = self._trans_key.public_data 79 | 80 | self.send_packet(MSG_KEXRSA_PUBKEY, String(self._host_key_data), 81 | String(self._trans_key_data)) 82 | 83 | def _compute_hash(self) -> bytes: 84 | """Compute a hash of key information associated with the connection""" 85 | 86 | hash_obj = self._hash_alg() 87 | hash_obj.update(self._conn.get_hash_prefix()) 88 | hash_obj.update(String(self._host_key_data)) 89 | hash_obj.update(String(self._trans_key_data)) 90 | hash_obj.update(String(self._encrypted_k)) 91 | hash_obj.update(MPInt(self._k)) 92 | return hash_obj.digest() 93 | 94 | def _process_pubkey(self, _pkttype: int, _pktid: int, 95 | packet: SSHPacket) -> None: 96 | """Process a KEXRSA pubkey message""" 97 | 98 | if self._conn.is_server(): 99 | raise ProtocolError('Unexpected KEXRSA pubkey msg') 100 | 101 | self._host_key_data = packet.get_string() 102 | self._trans_key_data = packet.get_string() 103 | packet.check_end() 104 | 105 | try: 106 | pubkey = decode_ssh_public_key(self._trans_key_data) 107 | except KeyImportError: 108 | raise ProtocolError('Invalid KEXRSA pubkey msg') from None 109 | 110 | trans_key = cast(RSAKey, pubkey) 111 | self._k = randrange(self._k_limit) 112 | self._encrypted_k = \ 113 | cast(bytes, trans_key.encrypt(MPInt(self._k), self.algorithm)) 114 | 115 | self.send_packet(MSG_KEXRSA_SECRET, String(self._encrypted_k)) 116 | 117 | def _process_secret(self, _pkttype: int, _pktid: int, 118 | packet: SSHPacket) -> None: 119 | """Process a KEXRSA secret message""" 120 | 121 | if self._conn.is_client(): 122 | raise ProtocolError('Unexpected KEXRSA secret msg') 123 | 124 | self._encrypted_k = packet.get_string() 125 | packet.check_end() 126 | 127 | trans_key = cast(RSAKey, self._trans_key) 128 | decrypted_k = trans_key.decrypt(self._encrypted_k, self.algorithm) 129 | if not decrypted_k: 130 | raise KeyExchangeFailed('Key exchange decryption failed') 131 | 132 | packet = SSHPacket(decrypted_k) 133 | self._k = packet.get_mpint() 134 | packet.check_end() 135 | 136 | server_conn = cast('SSHServerConnection', self._conn) 137 | host_key = server_conn.get_server_host_key() 138 | assert host_key is not None 139 | 140 | h = self._compute_hash() 141 | sig = host_key.sign(h) 142 | 143 | self.send_packet(MSG_KEXRSA_DONE, String(sig)) 144 | 145 | self._conn.send_newkeys(MPInt(self._k), h) 146 | 147 | def _process_done(self, _pkttype: int, _pktid: int, 148 | packet: SSHPacket) -> None: 149 | """Process a KEXRSA done message""" 150 | 151 | if self._conn.is_server(): 152 | raise ProtocolError('Unexpected KEXRSA done msg') 153 | 154 | sig = packet.get_string() 155 | packet.check_end() 156 | 157 | client_conn = cast('SSHClientConnection', self._conn) 158 | host_key = client_conn.validate_server_host_key(self._host_key_data) 159 | 160 | h = self._compute_hash() 161 | if not host_key.verify(h, sig): 162 | raise KeyExchangeFailed('Key exchange hash mismatch') 163 | 164 | self._conn.send_newkeys(MPInt(self._k), h) 165 | 166 | _packet_handlers = { 167 | MSG_KEXRSA_PUBKEY: _process_pubkey, 168 | MSG_KEXRSA_SECRET: _process_secret, 169 | MSG_KEXRSA_DONE: _process_done 170 | } 171 | 172 | 173 | for _name, _hash_alg, _key_size, _hash_size, _default in ( 174 | (b'rsa2048-sha256', sha256, 2048, 256, True), 175 | (b'rsa1024-sha1', sha1, 1024, 160, False)): 176 | register_kex_alg(_name, _KexRSA, _hash_alg, 177 | (_key_size, _hash_size), _default) 178 | -------------------------------------------------------------------------------- /asyncssh/keysign.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018-2021 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """SSH keysign client""" 22 | 23 | import asyncio 24 | from pathlib import Path 25 | import subprocess 26 | from typing import Iterable, Sequence, Union, cast 27 | 28 | from .misc import FilePath 29 | from .packet import Byte, String, UInt32, PacketDecodeError, SSHPacket 30 | from .public_key import SSHKey, SSHKeyPair, SSHCertificate 31 | 32 | 33 | _KeySignKey = Union[SSHKey, SSHCertificate] 34 | KeySignPath = Union[None, bool, FilePath] 35 | 36 | 37 | KEYSIGN_VERSION = 2 38 | 39 | _DEFAULT_KEYSIGN_DIRS = ('/opt/local/libexec', '/usr/local/libexec', 40 | '/usr/libexec', '/usr/libexec/openssh', 41 | '/usr/lib/openssh') 42 | 43 | 44 | class SSHKeySignKeyPair(SSHKeyPair): 45 | """Surrogate for a key where signing is done via ssh-keysign""" 46 | 47 | def __init__(self, keysign_path: str, sock_fd: int, 48 | key_or_cert: _KeySignKey): 49 | algorithm = key_or_cert.algorithm 50 | sig_algorithms = key_or_cert.sig_algorithms[:1] 51 | public_data = key_or_cert.public_data 52 | comment = key_or_cert.get_comment_bytes() 53 | 54 | super().__init__(algorithm, algorithm, sig_algorithms, sig_algorithms, 55 | public_data, comment) 56 | 57 | self._keysign_path = keysign_path 58 | self._sock_fd = sock_fd 59 | 60 | async def sign_async(self, data: bytes) -> bytes: 61 | """Use ssh-keysign to sign a block of data with this key""" 62 | 63 | proc = await asyncio.create_subprocess_exec( 64 | self._keysign_path, stdin=subprocess.PIPE, stdout=subprocess.PIPE, 65 | stderr=subprocess.PIPE, pass_fds=[self._sock_fd]) 66 | 67 | request = String(Byte(KEYSIGN_VERSION) + UInt32(self._sock_fd) + 68 | String(data)) 69 | stdout, stderr = await proc.communicate(request) 70 | 71 | if stderr: 72 | error = stderr.decode().strip() 73 | raise ValueError(error) 74 | 75 | try: 76 | packet = SSHPacket(stdout) 77 | resp = packet.get_string() 78 | packet.check_end() 79 | 80 | packet = SSHPacket(resp) 81 | version = packet.get_byte() 82 | sig = packet.get_string() 83 | packet.check_end() 84 | 85 | if version != KEYSIGN_VERSION: 86 | raise ValueError('unexpected version') 87 | 88 | return sig 89 | except PacketDecodeError: 90 | raise ValueError('invalid response') from None 91 | 92 | 93 | def find_keysign(path: KeySignPath) -> str: 94 | """Return path to ssh-keysign executable""" 95 | 96 | if path is True: 97 | for keysign_dir in _DEFAULT_KEYSIGN_DIRS: 98 | path = Path(keysign_dir, 'ssh-keysign') 99 | if path.exists(): 100 | break 101 | else: 102 | raise ValueError('Keysign not found') 103 | else: 104 | if not path or not Path(cast(FilePath, path)).exists(): 105 | raise ValueError('Keysign not found') 106 | 107 | return str(path) 108 | 109 | 110 | def get_keysign_keys(keysign_path: str, sock_fd: int, 111 | keys: Iterable[_KeySignKey]) -> \ 112 | Sequence[SSHKeySignKeyPair]: 113 | """Return keypair objects which invoke ssh-keysign""" 114 | 115 | return [SSHKeySignKeyPair(keysign_path, sock_fd, key) for key in keys] 116 | -------------------------------------------------------------------------------- /asyncssh/pattern.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2021 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """Pattern matching for principal and host names""" 22 | 23 | from fnmatch import fnmatch 24 | from typing import Union 25 | 26 | from .misc import IPAddress, ip_network 27 | 28 | 29 | _HostPattern = Union['WildcardHostPattern', 'CIDRHostPattern'] 30 | _AnyPattern = Union['WildcardPattern', _HostPattern] 31 | 32 | 33 | class _BaseWildcardPattern: 34 | """A base class for matching '*' and '?' wildcards""" 35 | 36 | def __init__(self, pattern: str): 37 | # We need to escape square brackets in host patterns if we 38 | # want to use Python's fnmatch. 39 | self._pattern = ''.join('[[]' if ch == '[' else 40 | '[]]' if ch == ']' else 41 | ch for ch in pattern) 42 | 43 | def _matches(self, value: str) -> bool: 44 | """Return whether a wild card pattern matches a value""" 45 | 46 | return fnmatch(value, self._pattern) 47 | 48 | 49 | class WildcardPattern(_BaseWildcardPattern): 50 | """A pattern matcher for '*' and '?' wildcards""" 51 | 52 | def matches(self, value: str) -> bool: 53 | """Return whether a wild card pattern matches a value""" 54 | 55 | return super()._matches(value) 56 | 57 | 58 | class WildcardHostPattern(_BaseWildcardPattern): 59 | """Match a host name or address against a wildcard pattern""" 60 | 61 | def matches(self, host: str, addr: str, _ip: IPAddress) -> bool: 62 | """Return whether a host or address matches a wild card host pattern""" 63 | 64 | return (bool(host) and super()._matches(host)) or \ 65 | (bool(addr) and super()._matches(addr)) 66 | 67 | 68 | class CIDRHostPattern: 69 | """Match IPv4/v6 address against CIDR-style subnet pattern""" 70 | 71 | def __init__(self, pattern: str): 72 | self._network = ip_network(pattern) 73 | 74 | def matches(self, _host: str, _addr: str, ip: IPAddress) -> bool: 75 | """Return whether an IP address matches a CIDR address pattern""" 76 | 77 | return bool(ip) and ip in self._network 78 | 79 | 80 | class _PatternList: 81 | """Match against a list of comma-separated positive and negative patterns 82 | 83 | This class is a base class for building a pattern matcher that 84 | takes a set of comma-separated positive and negative patterns, 85 | returning `True` if one or more positive patterns match and 86 | no negative ones do. 87 | 88 | The pattern matching is done by objects returned by the 89 | build_pattern method. The arguments passed in when a match 90 | is performed will vary depending on what class build_pattern 91 | returns. 92 | 93 | """ 94 | 95 | def __init__(self, patterns: str): 96 | self._pos_patterns = [] 97 | self._neg_patterns = [] 98 | 99 | for pattern in patterns.split(','): 100 | if pattern.startswith('!'): 101 | negate = True 102 | pattern = pattern[1:] 103 | else: 104 | negate = False 105 | 106 | matcher = self.build_pattern(pattern) 107 | 108 | if negate: 109 | self._neg_patterns.append(matcher) 110 | else: 111 | self._pos_patterns.append(matcher) 112 | 113 | def build_pattern(self, pattern: str) -> _AnyPattern: 114 | """Abstract method to build a pattern object""" 115 | 116 | raise NotImplementedError 117 | 118 | def matches(self, *args) -> bool: 119 | """Match a set of values against positive & negative pattern lists""" 120 | 121 | pos_match = any(p.matches(*args) for p in self._pos_patterns) 122 | neg_match = any(p.matches(*args) for p in self._neg_patterns) 123 | 124 | return pos_match and not neg_match 125 | 126 | 127 | class WildcardPatternList(_PatternList): 128 | """Match names against wildcard patterns""" 129 | 130 | def build_pattern(self, pattern: str) -> WildcardPattern: 131 | """Build a wild card pattern""" 132 | 133 | return WildcardPattern(pattern) 134 | 135 | 136 | class HostPatternList(_PatternList): 137 | """Match host names & addresses against wildcard and CIDR patterns""" 138 | 139 | def build_pattern(self, pattern: str) -> _HostPattern: 140 | """Build a CIDR address or wild card host pattern""" 141 | 142 | try: 143 | return CIDRHostPattern(pattern) 144 | except ValueError: 145 | return WildcardHostPattern(pattern) 146 | -------------------------------------------------------------------------------- /asyncssh/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronf/asyncssh/76c14dd0034d33773e2f50f91d167bfd22c3e021/asyncssh/py.typed -------------------------------------------------------------------------------- /asyncssh/saslprep.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2024 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """SASLprep implementation 22 | 23 | This module implements the stringprep algorithm defined in RFC 3454 24 | and the SASLprep profile of stringprep defined in RFC 4013. This 25 | profile is used to normalize usernames and passwords sent in the 26 | SSH protocol. 27 | 28 | """ 29 | 30 | # The stringprep module should not be flagged as deprecated 31 | # pylint: disable=deprecated-module 32 | import stringprep 33 | # pylint: enable=deprecated-module 34 | import unicodedata 35 | 36 | from typing import Callable, Optional, Sequence 37 | from typing_extensions import Literal 38 | 39 | 40 | class SASLPrepError(ValueError): 41 | """Invalid data provided to saslprep""" 42 | 43 | 44 | def _check_bidi(s: str) -> None: 45 | """Enforce bidirectional character check from RFC 3454 (stringprep)""" 46 | 47 | r_and_al_cat = False 48 | l_cat = False 49 | 50 | for c in s: 51 | if not r_and_al_cat and stringprep.in_table_d1(c): 52 | r_and_al_cat = True 53 | 54 | if not l_cat and stringprep.in_table_d2(c): 55 | l_cat = True 56 | 57 | if r_and_al_cat and l_cat: 58 | raise SASLPrepError('Both RandALCat and LCat characters present') 59 | 60 | if r_and_al_cat and not (stringprep.in_table_d1(s[0]) and 61 | stringprep.in_table_d1(s[-1])): 62 | raise SASLPrepError('RandALCat character not at both start and end') 63 | 64 | 65 | def _stringprep(s: str, check_unassigned: bool, 66 | mapping: Optional[Callable[[str], str]], 67 | normalization: Literal['NFC', 'NFD', 'NFKC', 'NFKD'], 68 | prohibited: Sequence[Callable[[str], bool]], 69 | bidi: bool) -> str: 70 | """Implement a stringprep profile as defined in RFC 3454""" 71 | 72 | if check_unassigned: # pragma: no branch 73 | for c in s: 74 | if stringprep.in_table_a1(c): 75 | raise SASLPrepError(f'Unassigned character: {c!r}') 76 | 77 | if mapping: # pragma: no branch 78 | s = mapping(s) 79 | 80 | if normalization: # pragma: no branch 81 | s = unicodedata.normalize(normalization, s) 82 | 83 | if prohibited: # pragma: no branch 84 | for c in s: 85 | for lookup in prohibited: 86 | if lookup(c): 87 | raise SASLPrepError(f'Prohibited character: {c!r}') 88 | 89 | if bidi: # pragma: no branch 90 | _check_bidi(s) 91 | 92 | return s 93 | 94 | 95 | def _map_saslprep(s: str) -> str: 96 | """Map stringprep table B.1 to nothing and C.1.2 to ASCII space""" 97 | 98 | r = [] 99 | 100 | for c in s: 101 | if stringprep.in_table_c12(c): 102 | r.append(' ') 103 | elif not stringprep.in_table_b1(c): 104 | r.append(c) 105 | 106 | return ''.join(r) 107 | 108 | 109 | def saslprep(s: str) -> str: 110 | """Implement SASLprep profile defined in RFC 4013""" 111 | 112 | prohibited = (stringprep.in_table_c12, stringprep.in_table_c21_c22, 113 | stringprep.in_table_c3, stringprep.in_table_c4, 114 | stringprep.in_table_c5, stringprep.in_table_c6, 115 | stringprep.in_table_c7, stringprep.in_table_c8, 116 | stringprep.in_table_c9) 117 | 118 | return _stringprep(s, True, _map_saslprep, 'NFKC', prohibited, True) 119 | -------------------------------------------------------------------------------- /asyncssh/version.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2024 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """AsyncSSH version information""" 22 | 23 | __author__ = 'Ron Frederick' 24 | 25 | __author_email__ = 'ronf@timeheart.net' 26 | 27 | __url__ = 'http://asyncssh.timeheart.net' 28 | 29 | __version__ = '2.21.0' 30 | -------------------------------------------------------------------------------- /docs/_templates/sidebarbottom.html: -------------------------------------------------------------------------------- 1 |

2 |

Change Log

3 |

4 |

Contributing

5 |

6 |

API Documentation

7 |

8 |

Source on PyPI

9 |

10 |

Source on GitHub

11 |

12 |

Issue Tracker

13 |

14 |

Search

15 | 16 | -------------------------------------------------------------------------------- /docs/_templates/sidebartop.html: -------------------------------------------------------------------------------- 1 | 2 | AsyncSSH
3 | Version {{version}} 4 |
5 |

6 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography >= 39.0 2 | typing_extensions >= 3.6 3 | -------------------------------------------------------------------------------- /docs/rftheme/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "basic/layout.html" %} 2 | 3 | {# Omit the top navigation bar. #} 4 | {% block relbar1 %} 5 | {% endblock %} 6 | 7 | {# Omit the bottom navigation bar. #} 8 | {% block relbar2 %} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /docs/rftheme/static/rftheme.css_t: -------------------------------------------------------------------------------- 1 | @import url("classic.css"); 2 | 3 | .tight-list * { 4 | line-height: 110% !important; 5 | margin: 0 0 3px !important; 6 | } 7 | 8 | div.sphinxsidebar { 9 | top: 0; 10 | } 11 | 12 | div.sphinxsidebarwrapper { 13 | padding-top: 8px; 14 | } 15 | 16 | div.body p, div.body dd, div.body li { 17 | text-align: left; 18 | } 19 | 20 | tt, .note tt { 21 | font-size: 1.15em; 22 | background: none; 23 | } 24 | 25 | div.body p.rubric { 26 | font-size: 1.3em; 27 | margin: 15px 0 5px; 28 | } 29 | -------------------------------------------------------------------------------- /docs/rftheme/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = classic 3 | stylesheet = rftheme.css 4 | pygments_style = sphinx 5 | -------------------------------------------------------------------------------- /docs/rtd-req.txt: -------------------------------------------------------------------------------- 1 | cryptography==2.8 2 | sphinx==4.2.0 3 | -------------------------------------------------------------------------------- /examples/callback_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | from typing import Optional 25 | 26 | class MySSHClientSession(asyncssh.SSHClientSession): 27 | def data_received(self, data: str, datatype: asyncssh.DataType) -> None: 28 | print(data, end='') 29 | 30 | def connection_lost(self, exc: Optional[Exception]) -> None: 31 | if exc: 32 | print('SSH session error: ' + str(exc), file=sys.stderr) 33 | 34 | class MySSHClient(asyncssh.SSHClient): 35 | def connection_made(self, conn: asyncssh.SSHClientConnection) -> None: 36 | print(f'Connection made to {conn.get_extra_info('peername')[0]}.') 37 | 38 | def auth_completed(self) -> None: 39 | print('Authentication successful.') 40 | 41 | async def run_client() -> None: 42 | conn, client = await asyncssh.create_connection(MySSHClient, 'localhost') 43 | 44 | async with conn: 45 | chan, session = await conn.create_session(MySSHClientSession, 'ls abc') 46 | await chan.wait_closed() 47 | 48 | try: 49 | asyncio.run(run_client()) 50 | except (OSError, asyncssh.Error) as exc: 51 | sys.exit('SSH connection failed: ' + str(exc)) 52 | -------------------------------------------------------------------------------- /examples/callback_client2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | from typing import Optional 25 | 26 | class MySSHClientSession(asyncssh.SSHClientSession): 27 | def data_received(self, data: str, datatype: asyncssh.DataType) -> None: 28 | print(data, end='') 29 | 30 | def connection_lost(self, exc: Optional[Exception]) -> None: 31 | if exc: 32 | print('SSH session error: ' + str(exc), file=sys.stderr) 33 | 34 | async def run_client() -> None: 35 | async with asyncssh.connect('localhost') as conn: 36 | chan, session = await conn.create_session(MySSHClientSession, 'ls abc') 37 | await chan.wait_closed() 38 | 39 | try: 40 | asyncio.run(run_client()) 41 | except (OSError, asyncssh.Error) as exc: 42 | sys.exit('SSH connection failed: ' + str(exc)) 43 | -------------------------------------------------------------------------------- /examples/callback_client3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | from typing import Optional 25 | 26 | class MySSHClientSession(asyncssh.SSHClientSession): 27 | def data_received(self, data: str, datatype: asyncssh.DataType) -> None: 28 | if datatype == asyncssh.EXTENDED_DATA_STDERR: 29 | print(data, end='', file=sys.stderr) 30 | else: 31 | print(data, end='') 32 | 33 | def connection_lost(self, exc: Optional[Exception]) -> None: 34 | if exc: 35 | print('SSH session error: ' + str(exc), file=sys.stderr) 36 | 37 | async def run_client() -> None: 38 | async with asyncssh.connect('localhost') as conn: 39 | chan, session = await conn.create_session(MySSHClientSession, 'ls abc') 40 | await chan.wait_closed() 41 | 42 | try: 43 | asyncio.run(run_client()) 44 | except (OSError, asyncssh.Error) as exc: 45 | sys.exit('SSH connection failed: ' + str(exc)) 46 | -------------------------------------------------------------------------------- /examples/callback_math_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | # To run this program, the file ``ssh_host_key`` must exist with an SSH 24 | # private key in it to use as a server host key. An SSH host certificate 25 | # can optionally be provided in the file ``ssh_host_key-cert.pub``. 26 | # 27 | # The file ``ssh_user_ca`` must exist with a cert-authority entry of 28 | # the certificate authority which can sign valid client certificates. 29 | 30 | import asyncio, asyncssh, sys 31 | 32 | class MySSHServerSession(asyncssh.SSHServerSession): 33 | def __init__(self): 34 | self._input = '' 35 | self._total = 0 36 | 37 | def connection_made(self, chan: asyncssh.SSHServerChannel): 38 | self._chan = chan 39 | 40 | def shell_requested(self) -> bool: 41 | return True 42 | 43 | def session_started(self) -> None: 44 | self._chan.write('Enter numbers one per line, or EOF when done:\n') 45 | 46 | def data_received(self, data: str, datatype: asyncssh.DataType) -> None: 47 | self._input += data 48 | 49 | lines = self._input.split('\n') 50 | for line in lines[:-1]: 51 | try: 52 | if line: 53 | self._total += int(line) 54 | except ValueError: 55 | self._chan.write_stderr(f'Invalid number: {line}\n') 56 | 57 | self._input = lines[-1] 58 | 59 | def eof_received(self) -> bool: 60 | self._chan.write(f'Total = {self._total}\n') 61 | self._chan.exit(0) 62 | return False 63 | 64 | def break_received(self, msec: int) -> bool: 65 | return self.eof_received() 66 | 67 | def soft_eof_received(self) -> None: 68 | self.eof_received() 69 | 70 | class MySSHServer(asyncssh.SSHServer): 71 | def session_requested(self) -> asyncssh.SSHServerSession: 72 | return MySSHServerSession() 73 | 74 | async def start_server() -> None: 75 | await asyncssh.create_server(MySSHServer, '', 8022, 76 | server_host_keys=['ssh_host_key'], 77 | authorized_client_keys='ssh_user_ca') 78 | 79 | loop = asyncio.new_event_loop() 80 | 81 | try: 82 | loop.run_until_complete(start_server()) 83 | except (OSError, asyncssh.Error) as exc: 84 | sys.exit('Error starting server: ' + str(exc)) 85 | 86 | loop.run_forever() 87 | -------------------------------------------------------------------------------- /examples/chat_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2016-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | # To run this program, the file ``ssh_host_key`` must exist with an SSH 24 | # private key in it to use as a server host key. An SSH host certificate 25 | # can optionally be provided in the file ``ssh_host_key-cert.pub``. 26 | # 27 | # The file ``ssh_user_ca`` must exist with a cert-authority entry of 28 | # the certificate authority which can sign valid client certificates. 29 | 30 | import asyncio, asyncssh, sys 31 | from typing import List, cast 32 | 33 | class ChatClient: 34 | _clients: List['ChatClient'] = [] 35 | 36 | def __init__(self, process: asyncssh.SSHServerProcess): 37 | self._process = process 38 | 39 | @classmethod 40 | async def handle_client(cls, process: asyncssh.SSHServerProcess): 41 | await cls(process).run() 42 | 43 | async def readline(self) -> str: 44 | return cast(str, self._process.stdin.readline()) 45 | 46 | def write(self, msg: str) -> None: 47 | self._process.stdout.write(msg) 48 | 49 | def broadcast(self, msg: str) -> None: 50 | for client in self._clients: 51 | if client != self: 52 | client.write(msg) 53 | 54 | async def run(self) -> None: 55 | self.write('Welcome to chat!\n\n') 56 | 57 | self.write('Enter your name: ') 58 | name = (await self.readline()).rstrip('\n') 59 | 60 | self.write(f'\n{len(self._clients)} other users are connected.\n\n') 61 | 62 | self._clients.append(self) 63 | self.broadcast(f'*** {name} has entered chat ***\n') 64 | 65 | try: 66 | async for line in self._process.stdin: 67 | self.broadcast(f'{name}: {line}') 68 | except asyncssh.BreakReceived: 69 | pass 70 | 71 | self.broadcast(f'*** {name} has left chat ***\n') 72 | self._clients.remove(self) 73 | 74 | async def start_server() -> None: 75 | await asyncssh.listen('', 8022, server_host_keys=['ssh_host_key'], 76 | authorized_client_keys='ssh_user_ca', 77 | process_factory=ChatClient.handle_client) 78 | 79 | loop = asyncio.new_event_loop() 80 | 81 | try: 82 | loop.run_until_complete(start_server()) 83 | except (OSError, asyncssh.Error) as exc: 84 | sys.exit('Error starting server: ' + str(exc)) 85 | 86 | loop.run_forever() 87 | -------------------------------------------------------------------------------- /examples/check_exit_status.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | 25 | async def run_client() -> None: 26 | async with asyncssh.connect('localhost') as conn: 27 | result = await conn.run('ls abc') 28 | 29 | if result.exit_status == 0: 30 | print(result.stdout, end='') 31 | else: 32 | print(result.stderr, end='', file=sys.stderr) 33 | print(f'Program exited with status {result.exit_status}', 34 | file=sys.stderr) 35 | 36 | try: 37 | asyncio.run(run_client()) 38 | except (OSError, asyncssh.Error) as exc: 39 | sys.exit('SSH connection failed: ' + str(exc)) 40 | -------------------------------------------------------------------------------- /examples/chroot_sftp_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2016-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | # To run this program, the file ``ssh_host_key`` must exist with an SSH 24 | # private key in it to use as a server host key. An SSH host certificate 25 | # can optionally be provided in the file ``ssh_host_key-cert.pub``. 26 | # 27 | # The file ``ssh_user_ca`` must exist with a cert-authority entry of 28 | # the certificate authority which can sign valid client certificates. 29 | 30 | import asyncio, asyncssh, os, sys 31 | 32 | class MySFTPServer(asyncssh.SFTPServer): 33 | def __init__(self, chan: asyncssh.SSHServerChannel): 34 | root = '/tmp/sftp/' + chan.get_extra_info('username') 35 | os.makedirs(root, exist_ok=True) 36 | super().__init__(chan, chroot=root) 37 | 38 | async def start_server() -> None: 39 | await asyncssh.listen('', 8022, server_host_keys=['ssh_host_key'], 40 | authorized_client_keys='ssh_user_ca', 41 | sftp_factory=MySFTPServer) 42 | 43 | loop = asyncio.new_event_loop() 44 | 45 | try: 46 | loop.run_until_complete(start_server()) 47 | except (OSError, asyncssh.Error) as exc: 48 | sys.exit('Error starting server: ' + str(exc)) 49 | 50 | loop.run_forever() 51 | -------------------------------------------------------------------------------- /examples/direct_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | from typing import Optional 25 | 26 | class MySSHTCPSession(asyncssh.SSHTCPSession): 27 | def data_received(self, data: bytes, datatype: asyncssh.DataType) -> None: 28 | # We use sys.stdout.buffer here because we're writing bytes 29 | sys.stdout.buffer.write(data) 30 | 31 | def connection_lost(self, exc: Optional[Exception]) -> None: 32 | if exc: 33 | print('Direct connection error:', str(exc), file=sys.stderr) 34 | 35 | async def run_client() -> None: 36 | async with asyncssh.connect('localhost') as conn: 37 | chan, session = await conn.create_connection(MySSHTCPSession, 38 | 'www.google.com', 80) 39 | 40 | # By default, TCP connections send and receive bytes 41 | chan.write(b'HEAD / HTTP/1.0\r\n\r\n') 42 | chan.write_eof() 43 | 44 | await chan.wait_closed() 45 | 46 | try: 47 | asyncio.run(run_client()) 48 | except (OSError, asyncssh.Error) as exc: 49 | sys.exit('SSH connection failed: ' + str(exc)) 50 | -------------------------------------------------------------------------------- /examples/direct_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | # To run this program, the file ``ssh_host_key`` must exist with an SSH 24 | # private key in it to use as a server host key. An SSH host certificate 25 | # can optionally be provided in the file ``ssh_host_key-cert.pub``. 26 | # 27 | # The file ``ssh_user_ca`` must exist with a cert-authority entry of 28 | # the certificate authority which can sign valid client certificates. 29 | 30 | import asyncio, asyncssh, sys 31 | 32 | class MySSHTCPSession(asyncssh.SSHTCPSession): 33 | def connection_made(self, chan: asyncssh.SSHTCPChannel) -> None: 34 | self._chan = chan 35 | 36 | def data_received(self, data: bytes, datatype: asyncssh.DataType) -> None: 37 | self._chan.write(data) 38 | 39 | class MySSHServer(asyncssh.SSHServer): 40 | def connection_requested(self, dest_host: str, dest_port: int, 41 | orig_host: str, orig_port: int) -> \ 42 | asyncssh.SSHTCPSession: 43 | if dest_port == 7: 44 | return MySSHTCPSession() 45 | else: 46 | raise asyncssh.ChannelOpenError( 47 | asyncssh.OPEN_ADMINISTRATIVELY_PROHIBITED, 48 | 'Only echo connections allowed') 49 | 50 | async def start_server() -> None: 51 | await asyncssh.create_server(MySSHServer, '', 8022, 52 | server_host_keys=['ssh_host_key'], 53 | authorized_client_keys='ssh_user_ca') 54 | 55 | loop = asyncio.new_event_loop() 56 | 57 | try: 58 | loop.run_until_complete(start_server()) 59 | except (OSError, asyncssh.Error) as exc: 60 | sys.exit('SSH server failed: ' + str(exc)) 61 | 62 | loop.run_forever() 63 | -------------------------------------------------------------------------------- /examples/editor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | # To run this program, the file ``ssh_host_key`` must exist with an SSH 24 | # private key in it to use as a server host key. An SSH host certificate 25 | # can optionally be provided in the file ``ssh_host_key-cert.pub``. 26 | # 27 | # The file ``ssh_user_ca`` must exist with a cert-authority entry of 28 | # the certificate authority which can sign valid client certificates. 29 | 30 | import asyncio, asyncssh, sys 31 | from typing import cast 32 | 33 | async def handle_client(process: asyncssh.SSHServerProcess): 34 | channel = cast(asyncssh.SSHLineEditorChannel, process.channel) 35 | 36 | username = process.get_extra_info('username') 37 | process.stdout.write(f'Welcome to my SSH server, {username}!\n\n') 38 | 39 | channel.set_echo(False) 40 | process.stdout.write('Tell me a secret: ') 41 | secret = await process.stdin.readline() 42 | 43 | channel.set_line_mode(False) 44 | process.stdout.write('\nYour secret is safe with me! ' 45 | 'Press any key to exit...') 46 | await process.stdin.read(1) 47 | 48 | process.stdout.write('\n') 49 | process.exit(0) 50 | 51 | async def start_server() -> None: 52 | await asyncssh.listen('', 8022, server_host_keys=['ssh_host_key'], 53 | authorized_client_keys='ssh_user_ca', 54 | process_factory=handle_client) 55 | 56 | loop = asyncio.new_event_loop() 57 | 58 | try: 59 | loop.run_until_complete(start_server()) 60 | except (OSError, asyncssh.Error) as exc: 61 | sys.exit('Error starting server: ' + str(exc)) 62 | 63 | loop.run_forever() 64 | -------------------------------------------------------------------------------- /examples/gather_results.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2016-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh 24 | 25 | async def run_client(host, command: str) -> asyncssh.SSHCompletedProcess: 26 | async with asyncssh.connect(host) as conn: 27 | return await conn.run(command) 28 | 29 | async def run_multiple_clients() -> None: 30 | # Put your lists of hosts here 31 | hosts = 5 * ['localhost'] 32 | 33 | tasks = (run_client(host, 'ls abc') for host in hosts) 34 | results = await asyncio.gather(*tasks, return_exceptions=True) 35 | 36 | for i, result in enumerate(results, 1): 37 | if isinstance(result, Exception): 38 | print(f'Task {i} failed: {result}') 39 | elif result.exit_status != 0: 40 | print(f'Task {i} exited with status {result.exit_status}:') 41 | print(result.stderr, end='') 42 | else: 43 | print(f'Task {i} succeeded:') 44 | print(result.stdout, end='') 45 | 46 | print(75*'-') 47 | 48 | asyncio.run(run_multiple_clients()) 49 | -------------------------------------------------------------------------------- /examples/listening_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | 25 | class MySSHTCPSession(asyncssh.SSHTCPSession): 26 | def connection_made(self, chan: asyncssh.SSHTCPChannel) -> None: 27 | self._chan = chan 28 | 29 | def data_received(self, data: bytes, datatype: asyncssh.DataType): 30 | self._chan.write(data) 31 | 32 | def connection_requested(orig_host: str, 33 | orig_port: int) -> asyncssh.SSHTCPSession: 34 | print(f'Connection received from {orig_host}, port {orig_port}') 35 | return MySSHTCPSession() 36 | 37 | async def run_client() -> None: 38 | async with asyncssh.connect('localhost') as conn: 39 | server = await conn.create_server(connection_requested, '', 8888, 40 | encoding='utf-8') 41 | 42 | if server: 43 | await server.wait_closed() 44 | else: 45 | print('Listener couldn\'t be opened.', file=sys.stderr) 46 | 47 | try: 48 | asyncio.run(run_client()) 49 | except (OSError, asyncssh.Error) as exc: 50 | sys.exit('SSH connection failed: ' + str(exc)) 51 | -------------------------------------------------------------------------------- /examples/local_forwarding_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | 25 | async def run_client() -> None: 26 | async with asyncssh.connect('localhost') as conn: 27 | listener = await conn.forward_local_port('', 8080, 'www.google.com', 80) 28 | await listener.wait_closed() 29 | 30 | try: 31 | asyncio.run(run_client()) 32 | except (OSError, asyncssh.Error) as exc: 33 | sys.exit('SSH connection failed: ' + str(exc)) 34 | -------------------------------------------------------------------------------- /examples/local_forwarding_client2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | 25 | async def run_client() -> None: 26 | async with asyncssh.connect('localhost') as conn: 27 | listener = await conn.forward_local_port('', 0, 'www.google.com', 80) 28 | print(f'Listening on port {listener.get_port()}...') 29 | await listener.wait_closed() 30 | 31 | try: 32 | asyncio.run(run_client()) 33 | except (OSError, asyncssh.Error) as exc: 34 | sys.exit('SSH connection failed: ' + str(exc)) 35 | -------------------------------------------------------------------------------- /examples/local_forwarding_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | # To run this program, the file ``ssh_host_key`` must exist with an SSH 24 | # private key in it to use as a server host key. An SSH host certificate 25 | # can optionally be provided in the file ``ssh_host_key-cert.pub``. 26 | # 27 | # The file ``ssh_user_ca`` must exist with a cert-authority entry of 28 | # the certificate authority which can sign valid client certificates. 29 | 30 | import asyncio, asyncssh, sys 31 | 32 | class MySSHServer(asyncssh.SSHServer): 33 | def connection_requested(self, dest_host: str, dest_port: int, 34 | orig_host: str, orig_port: int) -> bool: 35 | if dest_port == 80: 36 | return True 37 | else: 38 | raise asyncssh.ChannelOpenError( 39 | asyncssh.OPEN_ADMINISTRATIVELY_PROHIBITED, 40 | 'Only connections to port 80 are allowed') 41 | 42 | async def start_server() -> None: 43 | await asyncssh.create_server(MySSHServer, '', 8022, 44 | server_host_keys=['ssh_host_key'], 45 | authorized_client_keys='ssh_user_ca') 46 | 47 | loop = asyncio.new_event_loop() 48 | 49 | try: 50 | loop.run_until_complete(start_server()) 51 | except (OSError, asyncssh.Error) as exc: 52 | sys.exit('SSH server failed: ' + str(exc)) 53 | 54 | loop.run_forever() 55 | -------------------------------------------------------------------------------- /examples/math_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2016-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | 25 | async def run_client() -> None: 26 | async with asyncssh.connect('localhost') as conn: 27 | async with conn.create_process('bc') as process: 28 | for op in ['2+2', '1*2*3*4', '2^32']: 29 | process.stdin.write(op + '\n') 30 | result = await process.stdout.readline() 31 | print(op, '=', result, end='') 32 | 33 | try: 34 | asyncio.run(run_client()) 35 | except (OSError, asyncssh.Error) as exc: 36 | sys.exit('SSH connection failed: ' + str(exc)) 37 | -------------------------------------------------------------------------------- /examples/math_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | # To run this program, the file ``ssh_host_key`` must exist with an SSH 24 | # private key in it to use as a server host key. An SSH host certificate 25 | # can optionally be provided in the file ``ssh_host_key-cert.pub``. 26 | # 27 | # The file ``ssh_user_ca`` must exist with a cert-authority entry of 28 | # the certificate authority which can sign valid client certificates. 29 | 30 | import asyncio, asyncssh, sys 31 | 32 | async def handle_client(process: asyncssh.SSHServerProcess) -> None: 33 | process.stdout.write('Enter numbers one per line, or EOF when done:\n') 34 | 35 | total = 0 36 | 37 | try: 38 | async for line in process.stdin: 39 | line = line.rstrip('\n') 40 | if line: 41 | try: 42 | total += int(line) 43 | except ValueError: 44 | process.stderr.write(f'Invalid number: {line}\n') 45 | except asyncssh.BreakReceived: 46 | pass 47 | 48 | process.stdout.write(f'Total = {total}\n') 49 | process.exit(0) 50 | 51 | async def start_server() -> None: 52 | await asyncssh.listen('', 8022, server_host_keys=['ssh_host_key'], 53 | authorized_client_keys='ssh_user_ca', 54 | process_factory=handle_client) 55 | 56 | loop = asyncio.new_event_loop() 57 | 58 | try: 59 | loop.run_until_complete(start_server()) 60 | except (OSError, asyncssh.Error) as exc: 61 | sys.exit('Error starting server: ' + str(exc)) 62 | 63 | loop.run_forever() 64 | -------------------------------------------------------------------------------- /examples/redirect_input.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | 25 | async def run_client() -> None: 26 | async with asyncssh.connect('localhost') as conn: 27 | await conn.run('tail -r', input='1\n2\n3\n', stdout='/tmp/stdout') 28 | 29 | try: 30 | asyncio.run(run_client()) 31 | except (OSError, asyncssh.Error) as exc: 32 | sys.exit('SSH connection failed: ' + str(exc)) 33 | -------------------------------------------------------------------------------- /examples/redirect_local_pipe.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, subprocess, sys 24 | 25 | async def run_client() -> None: 26 | async with asyncssh.connect('localhost') as conn: 27 | local_proc = subprocess.Popen(r'echo "1\n2\n3"', shell=True, 28 | stdout=subprocess.PIPE) 29 | remote_result = await conn.run('tail -r', stdin=local_proc.stdout) 30 | print(remote_result.stdout, end='') 31 | 32 | try: 33 | asyncio.run(run_client()) 34 | except (OSError, asyncssh.Error) as exc: 35 | sys.exit('SSH connection failed: ' + str(exc)) 36 | -------------------------------------------------------------------------------- /examples/redirect_remote_pipe.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | 25 | async def run_client() -> None: 26 | async with asyncssh.connect('localhost') as conn: 27 | proc1 = await conn.create_process(r'echo "1\n2\n3"') 28 | proc2_result = await conn.run('tail -r', stdin=proc1.stdout) 29 | print(proc2_result.stdout, end='') 30 | 31 | try: 32 | asyncio.run(run_client()) 33 | except (OSError, asyncssh.Error) as exc: 34 | sys.exit('SSH connection failed: ' + str(exc)) 35 | -------------------------------------------------------------------------------- /examples/redirect_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2017-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | # To run this program, the file ``ssh_host_key`` must exist with an SSH 24 | # private key in it to use as a server host key. An SSH host certificate 25 | # can optionally be provided in the file ``ssh_host_key-cert.pub``. 26 | # 27 | # The file ``ssh_user_ca`` must exist with a cert-authority entry of 28 | # the certificate authority which can sign valid client certificates. 29 | 30 | import asyncio, asyncssh, subprocess, sys 31 | 32 | async def handle_client(process: asyncssh.SSHServerProcess) -> None: 33 | bc_proc = subprocess.Popen('bc', shell=True, stdin=subprocess.PIPE, 34 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 35 | 36 | await process.redirect(stdin=bc_proc.stdin, stdout=bc_proc.stdout, 37 | stderr=bc_proc.stderr) 38 | await process.stdout.drain() 39 | process.exit(0) 40 | 41 | async def start_server() -> None: 42 | await asyncssh.listen('', 8022, server_host_keys=['ssh_host_key'], 43 | authorized_client_keys='ssh_user_ca', 44 | process_factory=handle_client) 45 | 46 | loop = asyncio.new_event_loop() 47 | 48 | try: 49 | loop.run_until_complete(start_server()) 50 | except (OSError, asyncssh.Error) as exc: 51 | sys.exit('Error starting server: ' + str(exc)) 52 | 53 | loop.run_forever() 54 | -------------------------------------------------------------------------------- /examples/remote_forwarding_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | 25 | async def run_client() -> None: 26 | async with asyncssh.connect('localhost') as conn: 27 | listener = await conn.forward_remote_port('', 8080, 'localhost', 80) 28 | await listener.wait_closed() 29 | 30 | try: 31 | asyncio.run(run_client()) 32 | except (OSError, asyncssh.Error) as exc: 33 | sys.exit('SSH connection failed: ' + str(exc)) 34 | -------------------------------------------------------------------------------- /examples/remote_forwarding_client2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | from functools import partial 25 | from typing import Awaitable 26 | 27 | def connection_requested(conn: asyncssh.SSHClientConnection, orig_host: str, 28 | orig_port: int) -> Awaitable[asyncssh.SSHForwarder]: 29 | if orig_host in ('127.0.0.1', '::1'): 30 | return conn.forward_connection('localhost', 80) 31 | else: 32 | raise asyncssh.ChannelOpenError( 33 | asyncssh.OPEN_ADMINISTRATIVELY_PROHIBITED, 34 | 'Connections only allowed from localhost') 35 | 36 | async def run_client() -> None: 37 | async with asyncssh.connect('localhost') as conn: 38 | listener = await conn.create_server( 39 | partial(connection_requested, conn), '', 8080) 40 | await listener.wait_closed() 41 | 42 | try: 43 | asyncio.run(run_client()) 44 | except (OSError, asyncssh.Error) as exc: 45 | sys.exit('SSH connection failed: ' + str(exc)) 46 | -------------------------------------------------------------------------------- /examples/remote_forwarding_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | # To run this program, the file ``ssh_host_key`` must exist with an SSH 24 | # private key in it to use as a server host key. An SSH host certificate 25 | # can optionally be provided in the file ``ssh_host_key-cert.pub``. 26 | # 27 | # The file ``ssh_user_ca`` must exist with a cert-authority entry of 28 | # the certificate authority which can sign valid client certificates. 29 | 30 | import asyncio, asyncssh, sys 31 | 32 | class MySSHServer(asyncssh.SSHServer): 33 | def server_requested(self, listen_host: str, listen_port: int) -> bool: 34 | return listen_port == 8080 35 | 36 | async def start_server() -> None: 37 | await asyncssh.create_server(MySSHServer, '', 8022, 38 | server_host_keys=['ssh_host_key'], 39 | authorized_client_keys='ssh_user_ca') 40 | 41 | loop = asyncio.new_event_loop() 42 | 43 | try: 44 | loop.run_until_complete(start_server()) 45 | except (OSError, asyncssh.Error) as exc: 46 | sys.exit('SSH server failed: ' + str(exc)) 47 | 48 | loop.run_forever() 49 | -------------------------------------------------------------------------------- /examples/reverse_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | # To run this program, the file client_host_key must exist on the client, 24 | # containing an SSH private key for the client to use to authenticate 25 | # itself as a host to the server. An SSH certificate can optionally be 26 | # provided in the file client_host_key-cert.pub. 27 | # 28 | # The file trusted_server_keys must also exist on the client, containing a 29 | # list of trusted server keys or a cert-authority entry with a public key 30 | # trusted to sign server keys if certificates are used. This file should 31 | # be in "authorized_keys" format. 32 | 33 | import asyncio, asyncssh, sys 34 | from asyncio.subprocess import PIPE 35 | 36 | async def handle_request(process: asyncssh.SSHServerProcess) -> None: 37 | """Run a command on the client, piping I/O over an SSH session""" 38 | 39 | assert process.command is not None 40 | 41 | local_proc = await asyncio.create_subprocess_shell( 42 | process.command, stdin=PIPE, stdout=PIPE, stderr=PIPE) 43 | 44 | await process.redirect(stdin=local_proc.stdin, stdout=local_proc.stdout, 45 | stderr=local_proc.stderr) 46 | 47 | process.exit(await local_proc.wait()) 48 | await process.wait_closed() 49 | 50 | async def run_reverse_client() -> None: 51 | """Make an outbound connection and then become an SSH server on it""" 52 | 53 | conn = await asyncssh.connect_reverse( 54 | 'localhost', 8022, server_host_keys=['client_host_key'], 55 | authorized_client_keys='trusted_server_keys', 56 | process_factory=handle_request, encoding=None) 57 | 58 | await conn.wait_closed() 59 | 60 | try: 61 | asyncio.run(run_reverse_client()) 62 | except (OSError, asyncssh.Error) as exc: 63 | sys.exit('Reverse SSH connection failed: ' + str(exc)) 64 | -------------------------------------------------------------------------------- /examples/reverse_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | # To run this program, the file server_key must exist on the server, 24 | # containing an SSH private key for the server to use to authenticate itself 25 | # to the client. An SSH certificate can optionally be provided in the file 26 | # server_key-cert.pub. 27 | # 28 | # The file trusted_client_host_keys must also exist on the server, containing 29 | # a list of trusted client host keys or a @cert-authority entry with a public 30 | # key trusted to sign client host keys if certificates are used. This file 31 | # should be in "known_hosts" format. 32 | 33 | import asyncio, asyncssh, sys 34 | 35 | async def run_commands(conn: asyncssh.SSHClientConnection) -> None: 36 | """Run a series of commands on the client which connected to us""" 37 | 38 | commands = ('ls', 'sleep 30 && date', 'sleep 5 && cat /proc/cpuinfo') 39 | 40 | async with conn: 41 | tasks = [conn.run(cmd) for cmd in commands] 42 | 43 | for task in asyncio.as_completed(tasks): 44 | result = await task 45 | print('Command:', result.command) 46 | print('Return code:', result.returncode) 47 | print('Stdout:') 48 | print(result.stdout, end='') 49 | print('Stderr:') 50 | print(result.stderr, end='') 51 | print(75*'-') 52 | 53 | async def start_reverse_server() -> None: 54 | """Accept inbound connections and then become an SSH client on them""" 55 | 56 | await asyncssh.listen_reverse(port=8022, client_keys=['server_key'], 57 | known_hosts='trusted_client_host_keys', 58 | acceptor=run_commands) 59 | 60 | loop = asyncio.new_event_loop() 61 | 62 | try: 63 | loop.run_until_complete(start_reverse_server()) 64 | except (OSError, asyncssh.Error) as exc: 65 | sys.exit('Error starting server: ' + str(exc)) 66 | 67 | loop.run_forever() 68 | -------------------------------------------------------------------------------- /examples/scp_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2017-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | 25 | async def run_client() -> None: 26 | await asyncssh.scp('localhost:example.txt', '.') 27 | 28 | try: 29 | asyncio.run(run_client()) 30 | except (OSError, asyncssh.Error) as exc: 31 | sys.exit('SFTP operation failed: ' + str(exc)) 32 | -------------------------------------------------------------------------------- /examples/set_environment.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | 25 | async def run_client() -> None: 26 | async with asyncssh.connect('localhost') as conn: 27 | result = await conn.run('env', env={'LANG': 'en_GB', 28 | 'LC_COLLATE': 'C'}) 29 | print(result.stdout, end='') 30 | 31 | try: 32 | asyncio.run(run_client()) 33 | except (OSError, asyncssh.Error) as exc: 34 | sys.exit('SSH connection failed: ' + str(exc)) 35 | -------------------------------------------------------------------------------- /examples/set_terminal.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | 25 | async def run_client() -> None: 26 | async with asyncssh.connect('localhost') as conn: 27 | result = await conn.run('echo $TERM; stty size', 28 | term_type='xterm-color', 29 | term_size=(80, 24)) 30 | print(result.stdout, end='') 31 | 32 | try: 33 | asyncio.run(run_client()) 34 | except (OSError, asyncssh.Error) as exc: 35 | sys.exit('SSH connection failed: ' + str(exc)) 36 | -------------------------------------------------------------------------------- /examples/sftp_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2015-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | 25 | async def run_client() -> None: 26 | async with asyncssh.connect('localhost') as conn: 27 | async with conn.start_sftp_client() as sftp: 28 | await sftp.get('example.txt') 29 | 30 | try: 31 | asyncio.run(run_client()) 32 | except (OSError, asyncssh.Error) as exc: 33 | sys.exit('SFTP operation failed: ' + str(exc)) 34 | -------------------------------------------------------------------------------- /examples/show_environment.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | # To run this program, the file ``ssh_host_key`` must exist with an SSH 24 | # private key in it to use as a server host key. An SSH host certificate 25 | # can optionally be provided in the file ``ssh_host_key-cert.pub``. 26 | # 27 | # The file ``ssh_user_ca`` must exist with a cert-authority entry of 28 | # the certificate authority which can sign valid client certificates. 29 | 30 | import asyncio, asyncssh, sys 31 | 32 | async def handle_client(process: asyncssh.SSHServerProcess) -> None: 33 | if process.env: 34 | keywidth = max(map(len, process.env.keys()))+1 35 | process.stdout.write('Environment:\n') 36 | for key, value in process.env.items(): 37 | process.stdout.write(f' {key+":":{keywidth}} {value}\n') 38 | process.exit(0) 39 | else: 40 | process.stderr.write('No environment sent.\n') 41 | process.exit(1) 42 | 43 | async def start_server() -> None: 44 | await asyncssh.listen('', 8022, server_host_keys=['ssh_host_key'], 45 | authorized_client_keys='ssh_user_ca', 46 | process_factory=handle_client) 47 | 48 | loop = asyncio.new_event_loop() 49 | 50 | try: 51 | loop.run_until_complete(start_server()) 52 | except (OSError, asyncssh.Error) as exc: 53 | sys.exit('Error starting server: ' + str(exc)) 54 | 55 | loop.run_forever() 56 | -------------------------------------------------------------------------------- /examples/show_terminal.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | # To run this program, the file ``ssh_host_key`` must exist with an SSH 24 | # private key in it to use as a server host key. An SSH host certificate 25 | # can optionally be provided in the file ``ssh_host_key-cert.pub``. 26 | # 27 | # The file ``ssh_user_ca`` must exist with a cert-authority entry of 28 | # the certificate authority which can sign valid client certificates. 29 | 30 | import asyncio, asyncssh, sys 31 | 32 | async def handle_client(process: asyncssh.SSHServerProcess) -> None: 33 | width, height, pixwidth, pixheight = process.term_size 34 | 35 | process.stdout.write(f'Terminal type: {process.term_type}, ' 36 | f'size: {width}x{height}') 37 | if pixwidth and pixheight: 38 | process.stdout.write(f' ({pixwidth}x{pixheight} pixels)') 39 | process.stdout.write('\nTry resizing your window!\n') 40 | 41 | while not process.stdin.at_eof(): 42 | try: 43 | await process.stdin.read() 44 | except asyncssh.TerminalSizeChanged as exc: 45 | process.stdout.write(f'New window size: {exc.width}x{exc.height}') 46 | if exc.pixwidth and exc.pixheight: 47 | process.stdout.write(f' ({exc.pixwidth}' 48 | f'x{exc.pixheight} pixels)') 49 | process.stdout.write('\n') 50 | 51 | async def start_server() -> None: 52 | await asyncssh.listen('', 8022, server_host_keys=['ssh_host_key'], 53 | authorized_client_keys='ssh_user_ca', 54 | process_factory=handle_client) 55 | 56 | loop = asyncio.new_event_loop() 57 | 58 | try: 59 | loop.run_until_complete(start_server()) 60 | except (OSError, asyncssh.Error) as exc: 61 | sys.exit('Error starting server: ' + str(exc)) 62 | 63 | loop.run_forever() 64 | -------------------------------------------------------------------------------- /examples/simple_cert_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | # To run this program, the file ``ssh_host_key`` must exist with an SSH 24 | # private key in it to use as a server host key. An SSH host certificate 25 | # can optionally be provided in the file ``ssh_host_key-cert.pub``. 26 | # 27 | # The file ``ssh_user_ca`` must exist with a cert-authority entry of 28 | # the certificate authority which can sign valid client certificates. 29 | 30 | import asyncio, asyncssh, sys 31 | 32 | def handle_client(process: asyncssh.SSHServerProcess) -> None: 33 | username = process.get_extra_info('username') 34 | process.stdout.write(f'Welcome to my SSH server, {username}!\n') 35 | process.exit(0) 36 | 37 | async def start_server() -> None: 38 | await asyncssh.listen('', 8022, server_host_keys=['ssh_host_key'], 39 | authorized_client_keys='ssh_user_ca', 40 | process_factory=handle_client) 41 | 42 | loop = asyncio.new_event_loop() 43 | 44 | try: 45 | loop.run_until_complete(start_server()) 46 | except (OSError, asyncssh.Error) as exc: 47 | sys.exit('Error starting server: ' + str(exc)) 48 | 49 | loop.run_forever() 50 | -------------------------------------------------------------------------------- /examples/simple_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | 25 | async def run_client() -> None: 26 | async with asyncssh.connect('localhost') as conn: 27 | try: 28 | result = await conn.run('ls abc', check=True) 29 | except asyncssh.ProcessError as exc: 30 | print(exc.stderr, end='') 31 | print(f'Process exited with status {exc.exit_status}', 32 | file=sys.stderr) 33 | else: 34 | print(result.stdout, end='') 35 | 36 | try: 37 | asyncio.run(run_client()) 38 | except (OSError, asyncssh.Error) as exc: 39 | sys.exit('SSH connection failed: ' + str(exc)) 40 | -------------------------------------------------------------------------------- /examples/simple_keyed_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | # To run this program, the file ``ssh_host_key`` must exist with an SSH 24 | # private key in it to use as a server host key. An SSH host certificate 25 | # can optionally be provided in the file ``ssh_host_key-cert.pub``. 26 | # 27 | # Authentication requires the directory authorized_keys to exist with 28 | # files in it named based on the username containing the client keys 29 | # and certificate authority keys which are accepted for that user. 30 | 31 | import asyncio, asyncssh, sys 32 | 33 | def handle_client(process: asyncssh.SSHServerProcess) -> None: 34 | username = process.get_extra_info('username') 35 | process.stdout.write(f'Welcome to my SSH server, {username}!\n') 36 | process.exit(0) 37 | 38 | class MySSHServer(asyncssh.SSHServer): 39 | def connection_made(self, conn: asyncssh.SSHServerConnection) -> None: 40 | self._conn = conn 41 | 42 | def begin_auth(self, username: str) -> bool: 43 | try: 44 | self._conn.set_authorized_keys(f'authorized_keys/{username}') 45 | except OSError: 46 | pass 47 | 48 | return True 49 | 50 | async def start_server() -> None: 51 | await asyncssh.create_server(MySSHServer, '', 8022, 52 | server_host_keys=['ssh_host_key'], 53 | process_factory=handle_client) 54 | 55 | loop = asyncio.new_event_loop() 56 | 57 | try: 58 | loop.run_until_complete(start_server()) 59 | except (OSError, asyncssh.Error) as exc: 60 | sys.exit('Error starting server: ' + str(exc)) 61 | 62 | loop.run_forever() 63 | -------------------------------------------------------------------------------- /examples/simple_scp_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2015-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | # To run this program, the file ``ssh_host_key`` must exist with an SSH 24 | # private key in it to use as a server host key. An SSH host certificate 25 | # can optionally be provided in the file ``ssh_host_key-cert.pub``. 26 | # 27 | # The file ``ssh_user_ca`` must exist with a cert-authority entry of 28 | # the certificate authority which can sign valid client certificates. 29 | 30 | import asyncio, asyncssh, sys 31 | 32 | async def start_server() -> None: 33 | await asyncssh.listen('', 8022, server_host_keys=['ssh_host_key'], 34 | authorized_client_keys='ssh_user_ca', 35 | sftp_factory=True, allow_scp=True) 36 | 37 | loop = asyncio.new_event_loop() 38 | 39 | try: 40 | loop.run_until_complete(start_server()) 41 | except (OSError, asyncssh.Error) as exc: 42 | sys.exit('Error starting server: ' + str(exc)) 43 | 44 | loop.run_forever() 45 | -------------------------------------------------------------------------------- /examples/simple_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | # To run this program, the file ``ssh_host_key`` must exist with an SSH 24 | # private key in it to use as a server host key. An SSH host certificate 25 | # can optionally be provided in the file ``ssh_host_key-cert.pub``. 26 | 27 | import asyncio, asyncssh, bcrypt, sys 28 | from typing import Optional 29 | 30 | passwords = {'guest': b'', # guest account with no password 31 | 'user123': bcrypt.hashpw(b'secretpw', bcrypt.gensalt()), 32 | } 33 | 34 | def handle_client(process: asyncssh.SSHServerProcess) -> None: 35 | username = process.get_extra_info('username') 36 | process.stdout.write(f'Welcome to my SSH server, {username}!\n') 37 | process.exit(0) 38 | 39 | class MySSHServer(asyncssh.SSHServer): 40 | def connection_made(self, conn: asyncssh.SSHServerConnection) -> None: 41 | peername = conn.get_extra_info('peername')[0] 42 | print(f'SSH connection received from {peername}.') 43 | 44 | def connection_lost(self, exc: Optional[Exception]) -> None: 45 | if exc: 46 | print('SSH connection error: ' + str(exc), file=sys.stderr) 47 | else: 48 | print('SSH connection closed.') 49 | 50 | def begin_auth(self, username: str) -> bool: 51 | # If the user's password is the empty string, no auth is required 52 | return passwords.get(username) != b'' 53 | 54 | def password_auth_supported(self) -> bool: 55 | return True 56 | 57 | def validate_password(self, username: str, password: str) -> bool: 58 | if username not in passwords: 59 | return False 60 | pw = passwords[username] 61 | if not password and not pw: 62 | return True 63 | return bcrypt.checkpw(password.encode('utf-8'), pw) 64 | 65 | async def start_server() -> None: 66 | await asyncssh.create_server(MySSHServer, '', 8022, 67 | server_host_keys=['ssh_host_key'], 68 | process_factory=handle_client) 69 | 70 | loop = asyncio.new_event_loop() 71 | 72 | try: 73 | loop.run_until_complete(start_server()) 74 | except (OSError, asyncssh.Error) as exc: 75 | sys.exit('Error starting server: ' + str(exc)) 76 | 77 | loop.run_forever() 78 | -------------------------------------------------------------------------------- /examples/simple_sftp_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2015-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | # To run this program, the file ``ssh_host_key`` must exist with an SSH 24 | # private key in it to use as a server host key. An SSH host certificate 25 | # can optionally be provided in the file ``ssh_host_key-cert.pub``. 26 | # 27 | # The file ``ssh_user_ca`` must exist with a cert-authority entry of 28 | # the certificate authority which can sign valid client certificates. 29 | 30 | import asyncio, asyncssh, sys 31 | 32 | async def start_server() -> None: 33 | await asyncssh.listen('', 8022, server_host_keys=['ssh_host_key'], 34 | authorized_client_keys='ssh_user_ca', 35 | sftp_factory=True) 36 | 37 | loop = asyncio.new_event_loop() 38 | 39 | try: 40 | loop.run_until_complete(start_server()) 41 | except (OSError, asyncssh.Error) as exc: 42 | sys.exit('Error starting server: ' + str(exc)) 43 | 44 | loop.run_forever() 45 | -------------------------------------------------------------------------------- /examples/stream_direct_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | 25 | async def run_client() -> None: 26 | async with asyncssh.connect('localhost') as conn: 27 | reader, writer = await conn.open_connection('www.google.com', 80) 28 | 29 | # By default, TCP connections send and receive bytes 30 | writer.write(b'HEAD / HTTP/1.0\r\n\r\n') 31 | writer.write_eof() 32 | 33 | # We use sys.stdout.buffer here because we're writing bytes 34 | response = await reader.read() 35 | sys.stdout.buffer.write(response) 36 | 37 | try: 38 | asyncio.run(run_client()) 39 | except (OSError, asyncssh.Error) as exc: 40 | sys.exit('SSH connection failed: ' + str(exc)) 41 | -------------------------------------------------------------------------------- /examples/stream_direct_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | # To run this program, the file ``ssh_host_key`` must exist with an SSH 24 | # private key in it to use as a server host key. An SSH host certificate 25 | # can optionally be provided in the file ``ssh_host_key-cert.pub``. 26 | # 27 | # The file ``ssh_user_ca`` must exist with a cert-authority entry of 28 | # the certificate authority which can sign valid client certificates. 29 | 30 | import asyncio, asyncssh, sys 31 | 32 | async def handle_connection(reader: asyncssh.SSHReader, 33 | writer: asyncssh.SSHWriter) -> None: 34 | while not reader.at_eof(): 35 | data = await reader.read(8192) 36 | 37 | try: 38 | writer.write(data) 39 | except BrokenPipeError: 40 | break 41 | 42 | writer.close() 43 | 44 | class MySSHServer(asyncssh.SSHServer): 45 | def connection_requested(self, dest_host: str, dest_port: int, 46 | orig_host: str, orig_port: int) -> \ 47 | asyncssh.SSHSocketSessionFactory: 48 | if dest_port == 7: 49 | return handle_connection 50 | else: 51 | raise asyncssh.ChannelOpenError( 52 | asyncssh.OPEN_ADMINISTRATIVELY_PROHIBITED, 53 | 'Only echo connections allowed') 54 | 55 | async def start_server() -> None: 56 | await asyncssh.create_server(MySSHServer, '', 8022, 57 | server_host_keys=['ssh_host_key'], 58 | authorized_client_keys='ssh_user_ca') 59 | 60 | loop = asyncio.new_event_loop() 61 | 62 | try: 63 | loop.run_until_complete(start_server()) 64 | except (OSError, asyncssh.Error) as exc: 65 | sys.exit('SSH server failed: ' + str(exc)) 66 | 67 | loop.run_forever() 68 | -------------------------------------------------------------------------------- /examples/stream_listening_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | # 3 | # Copyright (c) 2013-2024 by Ron Frederick and others. 4 | # 5 | # This program and the accompanying materials are made available under 6 | # the terms of the Eclipse Public License v2.0 which accompanies this 7 | # distribution and is available at: 8 | # 9 | # http://www.eclipse.org/legal/epl-2.0/ 10 | # 11 | # This program may also be made available under the following secondary 12 | # licenses when the conditions for such availability set forth in the 13 | # Eclipse Public License v2.0 are satisfied: 14 | # 15 | # GNU General Public License, Version 2.0, or any later versions of 16 | # that license 17 | # 18 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 19 | # 20 | # Contributors: 21 | # Ron Frederick - initial implementation, API, and documentation 22 | 23 | import asyncio, asyncssh, sys 24 | 25 | async def handle_connection(reader, writer): 26 | while not reader.at_eof(): 27 | data = await reader.read(8192) 28 | writer.write(data) 29 | 30 | writer.close() 31 | 32 | def connection_requested(orig_host, orig_port): 33 | print(f'Connection received from {orig_host}, port {orig_port}') 34 | return handle_connection 35 | 36 | async def run_client(): 37 | async with asyncssh.connect('localhost') as conn: 38 | server = await conn.start_server(connection_requested, '', 8888, 39 | encoding='utf-8') 40 | await server.wait_closed() 41 | 42 | try: 43 | asyncio.run(run_client()) 44 | except (OSError, asyncssh.Error) as exc: 45 | sys.exit('SSH connection failed: ' + str(exc)) 46 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | allow_redefinition = True 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ['setuptools'] 3 | build-backend = 'setuptools.build_meta' 4 | 5 | [project] 6 | name = 'asyncssh' 7 | license = {text = 'EPL-2.0 OR GPL-2.0-or-later'} 8 | description = 'AsyncSSH: Asynchronous SSHv2 client and server library' 9 | readme = 'README.rst' 10 | authors = [{name = 'Ron Frederick', email = 'ronf@timeheart.net'}] 11 | classifiers = [ 12 | 'Development Status :: 5 - Production/Stable', 13 | 'Environment :: Console', 14 | 'Intended Audience :: Developers', 15 | 'License :: OSI Approved', 16 | 'Operating System :: MacOS :: MacOS X', 17 | 'Operating System :: POSIX', 18 | 'Programming Language :: Python :: 3.8', 19 | 'Programming Language :: Python :: 3.9', 20 | 'Programming Language :: Python :: 3.10', 21 | 'Programming Language :: Python :: 3.11', 22 | 'Programming Language :: Python :: 3.12', 23 | 'Programming Language :: Python :: 3.13', 24 | 'Topic :: Internet', 25 | 'Topic :: Security :: Cryptography', 26 | 'Topic :: Software Development :: Libraries :: Python Modules', 27 | 'Topic :: System :: Networking', 28 | ] 29 | requires-python = '>= 3.6' 30 | dependencies = [ 31 | 'cryptography >= 39.0', 32 | 'typing_extensions >= 4.0.0', 33 | ] 34 | dynamic = ['version'] 35 | 36 | [project.optional-dependencies] 37 | bcrypt = ['bcrypt >= 3.1.3'] 38 | fido2 = ['fido2 >= 0.9.2, < 2'] 39 | gssapi = ['gssapi >= 1.2.0'] 40 | libnacl = ['libnacl >= 1.4.2'] 41 | pkcs11 = ['python-pkcs11 >= 0.7.0'] 42 | pyOpenSSL = ['pyOpenSSL >= 23.0.0'] 43 | pywin32 = ['pywin32 >= 227'] 44 | 45 | 46 | [project.urls] 47 | Homepage = 'http://asyncssh.timeheart.net' 48 | Documentation = 'https://asyncssh.readthedocs.io' 49 | Source = 'https://github.com/ronf/asyncssh' 50 | Tracker = 'https://github.com/ronf/asyncssh/issues' 51 | 52 | [tool.setuptools.dynamic] 53 | version = {attr = 'asyncssh.version.__version__'} 54 | 55 | [tool.setuptools.packages.find] 56 | include = ['asyncssh*'] 57 | 58 | [tool.setuptools.package-data] 59 | asyncssh = ['py.typed'] 60 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-2018 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """Unit tests for AsyncSSH""" 22 | -------------------------------------------------------------------------------- /tests/gss_stub.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2018 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """Stub GSS module for unit tests""" 22 | 23 | 24 | def step(host, token): 25 | """Perform next step in GSS authentication""" 26 | 27 | complete = False 28 | 29 | if token == b'errtok': 30 | return token, complete 31 | elif ((token is None and 'empty_init' in host) or 32 | (token == b'1' and 'empty_continue' in host)): 33 | return b'', complete 34 | elif token == b'0': 35 | if 'continue_token' in host: 36 | token = b'continue' 37 | else: 38 | complete = True 39 | token = b'extra' if 'extra_token' in host else None 40 | elif token: 41 | token = bytes((token[0]-1,)) 42 | else: 43 | token = host[0].encode('ascii') 44 | 45 | if token == b'0': 46 | if 'step_error' in host: 47 | return (b'errtok' if 'errtok' in host else b'error'), complete 48 | 49 | complete = True 50 | 51 | return token, complete 52 | -------------------------------------------------------------------------------- /tests/gssapi_stub.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """Stub GSSAPI module for unit tests""" 22 | 23 | from enum import IntEnum 24 | 25 | from asyncssh.gss import GSSError 26 | 27 | from .gss_stub import step 28 | 29 | 30 | class Name: 31 | """Stub class for GSS principal name""" 32 | 33 | def __init__(self, base, _name_type=None): 34 | if 'init_error' in base: 35 | raise GSSError(99, 99) 36 | 37 | self.host = base[5:] 38 | 39 | 40 | class Credentials: 41 | """Stub class for GSS credentials""" 42 | 43 | def __init__(self, name=None, usage=None, store=None): 44 | # pylint: disable=unused-argument 45 | 46 | self.host = name.host if name else '' 47 | self.server = usage == 'accept' 48 | 49 | @property 50 | def mechs(self): 51 | """Return GSS mechanisms available for this host""" 52 | 53 | if self.server: 54 | return [0] if 'unknown_mech' in self.host else [1, 2] 55 | else: 56 | return [2] 57 | 58 | 59 | class RequirementFlag(IntEnum): 60 | """Stub class for GSS requirement flags""" 61 | 62 | # pylint: disable=invalid-name 63 | 64 | delegate_to_peer = 1 65 | mutual_authentication = 2 66 | integrity = 4 67 | 68 | 69 | class SecurityContext: 70 | """Stub class for GSS security context""" 71 | 72 | def __init__(self, name=None, creds=None, flags=None): 73 | host = creds.host if creds.server else name.host 74 | 75 | if flags is None: 76 | flags = RequirementFlag.mutual_authentication | \ 77 | RequirementFlag.integrity 78 | 79 | if ((creds.server and 'no_server_integrity' in host) or 80 | (not creds.server and 'no_client_integrity' in host)): 81 | flags &= ~RequirementFlag.integrity 82 | 83 | self._host = host 84 | self._server = creds.server 85 | self._actual_flags = flags 86 | self._complete = False 87 | 88 | @property 89 | def complete(self): 90 | """Return whether or not GSS negotiation is complete""" 91 | 92 | return self._complete 93 | 94 | @property 95 | def actual_flags(self): 96 | """Return flags set on this context""" 97 | 98 | return self._actual_flags 99 | 100 | @property 101 | def initiator_name(self): 102 | """Return user principal associated with this context""" 103 | 104 | return 'user@TEST' 105 | 106 | @property 107 | def target_name(self): 108 | """Return host principal associated with this context""" 109 | 110 | return 'host@TEST' 111 | 112 | def step(self, token=None): 113 | """Perform next step in GSS security exchange""" 114 | 115 | token, complete = step(self._host, token) 116 | 117 | if complete: 118 | self._complete = True 119 | 120 | if token == b'error': 121 | raise GSSError(99, 99) 122 | elif token == b'errtok': 123 | raise GSSError(99, 99, token) 124 | else: 125 | return token 126 | 127 | def get_signature(self, _data): 128 | """Sign a block of data""" 129 | 130 | if 'sign_error' in self._host: 131 | raise GSSError(99, 99) 132 | 133 | return b'fail' if 'verify_error' in self._host else b'' 134 | 135 | def verify_signature(self, _data, sig): 136 | """Verify a signature for a block of data""" 137 | 138 | # pylint: disable=no-self-use 139 | 140 | if sig == b'fail': 141 | raise GSSError(99, 99) 142 | -------------------------------------------------------------------------------- /tests/keysign_stub.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018-2019 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """Stub ssh-keysign module for unit tests""" 22 | 23 | import asyncssh 24 | from asyncssh.keysign import KEYSIGN_VERSION 25 | from asyncssh.packet import Byte, String, SSHPacket 26 | 27 | 28 | class SSHKeysignStub: 29 | """Stub class to replace process running ssh-keysign""" 30 | 31 | async def communicate(self, request): 32 | """Process SSH key signing request""" 33 | 34 | # pylint: disable=no-self-use 35 | 36 | packet = SSHPacket(request) 37 | request = packet.get_string() 38 | packet.check_end() 39 | 40 | packet = SSHPacket(request) 41 | version = packet.get_byte() 42 | _ = packet.get_uint32() # sock_fd 43 | data = packet.get_string() 44 | packet.check_end() 45 | 46 | if version == 0: 47 | return b'', b'' 48 | elif version == 1: 49 | return b'', b'invalid request' 50 | else: 51 | skey = asyncssh.load_keypairs('skey_ecdsa')[0] 52 | sig = skey.sign(data) 53 | return String(Byte(KEYSIGN_VERSION) + String(sig)), b'' 54 | 55 | 56 | async def create_subprocess_exec_stub(*_args, **_kwargs): 57 | """Return a stub for a subprocess running the ssh-keysign executable""" 58 | 59 | return SSHKeysignStub() 60 | -------------------------------------------------------------------------------- /tests/sspi_stub.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2022 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | # Georg Sauthoff - fix for "setup.py test" command on non-Windows 21 | 22 | """Stub SSPI module for unit tests""" 23 | 24 | 25 | import sys 26 | 27 | from .gss_stub import step 28 | 29 | if sys.platform == 'win32': # pragma: no cover 30 | from asyncssh.gss_win32 import ASC_RET_INTEGRITY, ISC_RET_INTEGRITY 31 | from asyncssh.gss_win32 import SECPKG_ATTR_NATIVE_NAMES, SSPIError 32 | 33 | 34 | class SSPIBuffer: 35 | """Stub class for SSPI buffer""" 36 | 37 | def __init__(self, data): 38 | self._data = data 39 | 40 | @property 41 | def Buffer(self): # pylint: disable=invalid-name 42 | """Return the data in the buffer""" 43 | 44 | return self._data 45 | 46 | 47 | class SSPIContext: 48 | """Stub class for SSPI security context""" 49 | 50 | def QueryContextAttributes(self, attr): # pylint: disable=invalid-name 51 | """Return principal information associated with this context""" 52 | 53 | # pylint: disable=no-self-use 54 | 55 | if attr == SECPKG_ATTR_NATIVE_NAMES: 56 | return ['user@TEST', 'host@TEST'] 57 | else: # pragma: no cover 58 | return None 59 | 60 | 61 | class SSPIAuth: 62 | """Stub class for SSPI authentication""" 63 | 64 | def __init__(self, _package=None, spn=None, targetspn=None, scflags=None): 65 | host = spn or targetspn 66 | 67 | if 'init_error' in host: 68 | raise SSPIError('Authentication initialization error') 69 | 70 | if targetspn and 'no_client_integrity' in host: 71 | scflags &= ~ISC_RET_INTEGRITY 72 | elif spn and 'no_server_integrity' in host: 73 | scflags &= ~ASC_RET_INTEGRITY 74 | 75 | self._host = host[5:] 76 | self._flags = scflags 77 | self._ctxt = SSPIContext() 78 | self._complete = False 79 | self._error = False 80 | 81 | @property 82 | def authenticated(self): 83 | """Return whether authentication is complete""" 84 | 85 | return self._complete 86 | 87 | @property 88 | def ctxt(self): 89 | """Return authentication context""" 90 | 91 | return self._ctxt 92 | 93 | @property 94 | def ctxt_attr(self): 95 | """Return authentication flags""" 96 | 97 | return self._flags 98 | 99 | def reset(self): 100 | """Reset SSPI security context""" 101 | 102 | self._complete = False 103 | 104 | def authorize(self, token): 105 | """Perform next step in SSPI authentication""" 106 | 107 | if self._error: 108 | self._error = False 109 | raise SSPIError('Token authentication error') 110 | 111 | new_token, complete = step(self._host, token) 112 | 113 | if complete: 114 | self._complete = True 115 | 116 | if new_token in (b'error', b'errtok'): 117 | if token: 118 | raise SSPIError('Token authentication error') 119 | else: 120 | self._error = True 121 | return True, [SSPIBuffer(b'')] 122 | else: 123 | return bool(new_token), [SSPIBuffer(new_token)] 124 | 125 | def sign(self, data): 126 | """Sign a block of data""" 127 | 128 | # pylint: disable=no-self-use,unused-argument 129 | 130 | if 'sign_error' in self._host: 131 | raise SSPIError('Signing error') 132 | 133 | return b'fail' if 'verify_error' in self._host else b'' 134 | 135 | def verify(self, data, sig): 136 | """Verify a signature for a block of data""" 137 | 138 | # pylint: disable=no-self-use,unused-argument 139 | 140 | if sig == b'fail': 141 | raise SSPIError('Signature verification error') 142 | -------------------------------------------------------------------------------- /tests/test_compression.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """Unit tests for compression""" 22 | 23 | import os 24 | import unittest 25 | 26 | from asyncssh.compression import get_compression_algs, get_compression_params 27 | from asyncssh.compression import get_compressor, get_decompressor 28 | 29 | 30 | class TestCompression(unittest.TestCase): 31 | """Unit tests for compression module""" 32 | 33 | def test_compression_algs(self): 34 | """Unit test compression algorithms""" 35 | 36 | for alg in get_compression_algs(): 37 | with self.subTest(alg=alg): 38 | get_compression_params(alg) 39 | 40 | data = os.urandom(256) 41 | 42 | compressor = get_compressor(alg) 43 | decompressor = get_decompressor(alg) 44 | 45 | if compressor: 46 | cmpdata = compressor.compress(data) 47 | self.assertEqual(decompressor.decompress(cmpdata), data) 48 | -------------------------------------------------------------------------------- /tests/test_encryption.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2020 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """Unit tests for symmetric key encryption""" 22 | 23 | import os 24 | import random 25 | import unittest 26 | 27 | from asyncssh.encryption import register_encryption_alg, get_encryption_algs 28 | from asyncssh.encryption import get_encryption_params, get_encryption 29 | from asyncssh.mac import get_mac_algs 30 | 31 | 32 | class _TestEncryption(unittest.TestCase): 33 | """Unit tests for encryption module""" 34 | 35 | def check_encryption_alg(self, enc_alg, mac_alg): 36 | """Check a symmetric encryption algorithm""" 37 | 38 | enc_keysize, enc_ivsize, enc_blocksize, mac_keysize, _, etm = \ 39 | get_encryption_params(enc_alg, mac_alg) 40 | 41 | enc_blocksize = max(8, enc_blocksize) 42 | 43 | enc_key = os.urandom(enc_keysize) 44 | enc_iv = os.urandom(enc_ivsize) 45 | mac_key = os.urandom(mac_keysize) 46 | 47 | seq = random.getrandbits(32) 48 | 49 | enc = get_encryption(enc_alg, enc_key, enc_iv, mac_alg, mac_key, etm) 50 | dec = get_encryption(enc_alg, enc_key, enc_iv, mac_alg, mac_key, etm) 51 | 52 | for i in range(2, 6): 53 | data = os.urandom(4*etm + i*enc_blocksize) 54 | hdr, packet = data[:4], data[4:] 55 | 56 | encdata, encmac = enc.encrypt_packet(seq, hdr, packet) 57 | 58 | first, rest = encdata[:enc_blocksize], encdata[enc_blocksize:] 59 | 60 | decfirst, dechdr = dec.decrypt_header(seq, first, 4) 61 | 62 | decdata = dec.decrypt_packet(seq, decfirst, rest, 4, encmac) 63 | 64 | self.assertEqual(dechdr, hdr) 65 | self.assertEqual(decdata, packet) 66 | 67 | seq = (seq + 1) & 0xffffffff 68 | 69 | def test_encryption_algs(self): 70 | """Unit test encryption algorithms""" 71 | 72 | for enc_alg in get_encryption_algs(): 73 | for mac_alg in get_mac_algs(): 74 | with self.subTest(enc_alg=enc_alg, mac_alg=mac_alg): 75 | self.check_encryption_alg(enc_alg, mac_alg) 76 | 77 | def test_unavailable_cipher(self): 78 | """Test registering encryption that uses an unavailable cipher""" 79 | 80 | # pylint: disable=no-self-use 81 | 82 | register_encryption_alg('xxx', 'xxx', '', True) 83 | -------------------------------------------------------------------------------- /tests/test_mac.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2021 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """Unit tests for message authentication""" 22 | 23 | import os 24 | import unittest 25 | 26 | from asyncssh.mac import get_mac_algs, get_mac_params, get_mac 27 | 28 | 29 | class _TestMAC(unittest.TestCase): 30 | """Unit tests for mac module""" 31 | 32 | def test_mac_algs(self): 33 | """Unit test MAC algorithms""" 34 | 35 | for mac_alg in get_mac_algs(): 36 | with self.subTest(mac_alg=mac_alg): 37 | mac_keysize, _, _ = get_mac_params(mac_alg) 38 | 39 | mac_key = os.urandom(mac_keysize) 40 | packet = os.urandom(256) 41 | 42 | enc_mac = get_mac(mac_alg, mac_key) 43 | dec_mac = get_mac(mac_alg, mac_key) 44 | 45 | badpacket = bytearray(packet) 46 | badpacket[-1] ^= 0xff 47 | 48 | mac = enc_mac.sign(0, packet) 49 | 50 | badmac = bytearray(mac) 51 | badmac[-1] ^= 0xff 52 | 53 | self.assertTrue(dec_mac.verify(0, packet, mac)) 54 | self.assertFalse(dec_mac.verify(0, bytes(badpacket), mac)) 55 | self.assertFalse(dec_mac.verify(0, packet, bytes(badmac))) 56 | 57 | def test_umac_wrapper(self): 58 | """Unit test some unused parts of the UMAC wrapper code""" 59 | 60 | try: 61 | # pylint: disable=import-outside-toplevel 62 | from asyncssh.crypto import umac32 63 | except ImportError: # pragma: no cover 64 | self.skipTest('umac not available') 65 | 66 | mac_key = os.urandom(16) 67 | 68 | mac1 = umac32(mac_key) 69 | mac1.update(b'test') 70 | 71 | mac2 = mac1.copy() 72 | 73 | mac1.update(b'123') 74 | mac2.update(b'123') 75 | 76 | self.assertEqual(mac1.hexdigest(), mac2.hexdigest()) 77 | -------------------------------------------------------------------------------- /tests/test_saslprep.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 by Ron Frederick and others. 2 | # 3 | # This program and the accompanying materials are made available under 4 | # the terms of the Eclipse Public License v2.0 which accompanies this 5 | # distribution and is available at: 6 | # 7 | # http://www.eclipse.org/legal/epl-2.0/ 8 | # 9 | # This program may also be made available under the following secondary 10 | # licenses when the conditions for such availability set forth in the 11 | # Eclipse Public License v2.0 are satisfied: 12 | # 13 | # GNU General Public License, Version 2.0, or any later versions of 14 | # that license 15 | # 16 | # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17 | # 18 | # Contributors: 19 | # Ron Frederick - initial implementation, API, and documentation 20 | 21 | """Unit tests for SASL string preparation""" 22 | 23 | import unittest 24 | 25 | from asyncssh.saslprep import saslprep, SASLPrepError 26 | 27 | class _TestSASLPrep(unittest.TestCase): 28 | """Unit tests for saslprep module""" 29 | 30 | def test_nonstring(self): 31 | """Test passing a non-string value""" 32 | 33 | with self.assertRaises(TypeError): 34 | saslprep(b'xxx') 35 | 36 | def test_unassigned(self): 37 | """Test passing strings with unassigned code points""" 38 | 39 | for s in ('\u0221', '\u038b', '\u0510', '\u070e', '\u0900', '\u0a00'): 40 | with self.assertRaises(SASLPrepError, msg=f'U+{ord(s):08x}'): 41 | saslprep('abc' + s + 'def') 42 | 43 | def test_map_to_nothing(self): 44 | """Test passing strings with characters that map to nothing""" 45 | 46 | for s in ('\u00ad', '\u034f', '\u1806', '\u200c', '\u2060', '\ufe00'): 47 | self.assertEqual(saslprep('abc' + s + 'def'), 'abcdef', 48 | msg=f'U+{ord(s):08x}') 49 | 50 | def test_map_to_whitespace(self): 51 | """Test passing strings with characters that map to whitespace""" 52 | for s in ('\u00a0', '\u1680', '\u2000', '\u202f', '\u205f', '\u3000'): 53 | self.assertEqual(saslprep('abc' + s + 'def'), 'abc def', 54 | msg=f'U+{ord(s):08x}') 55 | 56 | def test_normalization(self): 57 | """Test Unicode normalization form KC conversions""" 58 | for (s, n) in (('\u00aa', 'a'), ('\u2168', 'IX')): 59 | self.assertEqual(saslprep('abc' + s + 'def'), 'abc' + n + 'def', 60 | msg=f'U+{ord(s):08x}') 61 | 62 | def test_prohibited(self): 63 | """Test passing strings with prohibited characters""" 64 | for s in ('\u0000', '\u007f', '\u0080', '\u06dd', '\u180e', '\u200e', 65 | '\u2028', '\u202a', '\u206a', '\u2ff0', '\u2ffb', '\ud800', 66 | '\udfff', '\ue000', '\ufdd0', '\ufef9', '\ufffc', '\uffff', 67 | '\U0001d173', '\U000E0001', '\U00100000', '\U0010fffd'): 68 | with self.assertRaises(SASLPrepError, msg=f'U+{ord(s):08x}'): 69 | saslprep('abc' + s + 'def') 70 | 71 | def test_bidi(self): 72 | """Test passing strings with bidirectional characters""" 73 | 74 | for s in ('\u05be\u05c0\u05c3\u05d0', # RorAL only 75 | 'abc\u00c0\u00c1\u00c2', # L only 76 | '\u0627\u0031\u0628'): # Mix of RorAL and other 77 | self.assertEqual(saslprep(s), s) 78 | 79 | with self.assertRaises(SASLPrepError): 80 | saslprep('abc\u05be\u05c0\u05c3') # Mix of RorAL and L 81 | 82 | with self.assertRaises(SASLPrepError): 83 | saslprep('\u0627\u0031') # RorAL not at both start & end 84 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.8 3 | skip_missing_interpreters = True 4 | envlist = 5 | clean 6 | report 7 | py3{8,9,10,11,12,13}-{linux,darwin,windows} 8 | 9 | [testenv] 10 | deps = 11 | aiofiles>=0.6.0 12 | bcrypt>=3.1.3 13 | fido2>=0.9.2 14 | libnacl>=1.4.2 15 | pyOpenSSL>=17.0.0 16 | pytest>=7.0.1 17 | pytest-cov>=3.0.0 18 | setuptools>=18.5 19 | linux,darwin: gssapi>=1.2.0 20 | linux,darwin: python-pkcs11>=0.7.0 21 | linux,darwin: uvloop>=0.9.1 22 | windows: pywin32>=227 23 | platform = 24 | linux: linux 25 | darwin: darwin 26 | windows: win32 27 | usedevelop = True 28 | setenv = 29 | PIP_USE_PEP517 = 1 30 | COVERAGE_FILE = .coverage.{envname} 31 | commands = 32 | {envpython} -m pytest --cov --cov-report=term-missing:skip-covered {posargs} 33 | depends = 34 | clean 35 | 36 | [testenv:clean] 37 | deps = coverage 38 | skip_install = true 39 | setenv = 40 | COVERAGE_FILE = 41 | commands = coverage erase 42 | depends = 43 | 44 | [testenv:report] 45 | deps = coverage 46 | skip_install = true 47 | parallel_show_output = true 48 | setenv = 49 | COVERAGE_FILE = 50 | commands = 51 | coverage combine 52 | coverage report --show-missing 53 | coverage html 54 | coverage xml 55 | depends = 56 | py3{8,9,10,11,12,13}-{linux,darwin,windows} 57 | 58 | [pytest] 59 | testpaths = tests 60 | --------------------------------------------------------------------------------