pax_global_header00006660000000000000000000000064126063062020014507gustar00rootroot0000000000000052 comment=15dc2622da9657f33748b159e8c3fe35e85d7839 asyncssh-1.3.0/000077500000000000000000000000001260630620200133435ustar00rootroot00000000000000asyncssh-1.3.0/.coveragerc000066400000000000000000000001411260630620200154600ustar00rootroot00000000000000[run] branch = True [report] exclude_lines = pragma: no cover raise NotImplementedError asyncssh-1.3.0/.gitignore000066400000000000000000000001421260630620200153300ustar00rootroot00000000000000.*.swp MANIFEST __pycache__/ *.py[cod] asyncssh.egg-info build/ dist/ docs/Makefile docs/_build/ asyncssh-1.3.0/CONTRIBUTING.rst000066400000000000000000000074021260630620200160070ustar00rootroot00000000000000Contributing to AsyncSSH ======================== Input on AsyncSSH is extremely welcome. Below are some recommendations of the best ways to contribute. Asking questions ---------------- If you have a general question about how to use AsyncSSH, you are welcome to post it to the end-user mailing list at `asyncssh-users@googlegroups.com `_. If you have a question related to the development of AsyncSSH, you can post it to the development mailing list at `asyncssh-dev@googlegroups.com `_. You are also welcome to use the AsyncSSH `issue tracker `_ to ask questions. Reporting bugs -------------- Please use the `issue tracker `_ to report any bugs you find. Before creating a new issue, please check the currently open issues to see if your problem has already been reported. If you create a new issue, please include the version of AsyncSSH you are using, information about the OS you are running on and the installed version of Python and any other libraries that are involved. Please also include detailed information about how to reproduce the problem, including any traceback information you were able to collect or other relevant output. If you have sample code which exhibits the problem, feel free to include that as well. If possible, please test against the latest version of AsyncSSH. Also, if you are testing code in something other than the master branch, it would be helpful to know if you also see the problem in master. Requesting feature enhancements ------------------------------- The `issue tracker `_ should also be used to post feature enhancement requests. While I can't make any promises about what features will be added in the future, suggestions are always welcome! Contributing code ----------------- Before submitting a pull request, please create an issue on the `issue tracker `_ explaining what functionality you'd like to contribute and how it could be used. Discussing the approach you'd like to take up front will make it far more likely I'll be able to accept your changes, or explain what issues might prevent that before you spend a lot of effort. If you find a typo or other small bug in the code, you're welcome to submit a patch without filing an issue first, but for anything larger than a few lines I strongly recommend coordinating up front. Any code you submit will need to be provided with a compatible license. AsyncSSH code is currently released under the `Eclipse Public License v1.0 `_. Before submitting a pull request, make sure to indicate that you are ok with releasing your code under this license and how you'd like to be listed in the contributors list. Branches -------- There are two long-lived branches in AsyncSSH at the moment: * The master branch is intended to contain the latest stable version of the code. All official versions of AsyncSSH are released from this branch, and a each release has a corresponding tag added matching its release number. Bug fixes and simple improvements may be checked directly into this branch, but most new features will be added to the develop branch first. * The develop branch is intended to contain features for developers to test before they are ready to be added to an official release. APIs in the develop branch may be subject to change until they are migrated back to master, and there's no guarantee of backward compatibility in this branch. However, pulling from this branch will provide early access to new functionality and a chance to influence this functionality before it is released. asyncssh-1.3.0/COPYRIGHT000066400000000000000000000006011260630620200146330ustar00rootroot00000000000000Copyright (c) 2013-2014 by Ron Frederick . All rights reserved. This program and the accompanying materials are made available under the terms of the Eclipse Public License v1.0 which accompanies this distribution and is available at: http://www.eclipse.org/legal/epl-v10.html Contributors: Ron Frederick - initial implementation, API, and documentation asyncssh-1.3.0/LICENSE000066400000000000000000000263721260630620200143620ustar00rootroot00000000000000Eclipse Public License - v 1.0 THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 1. DEFINITIONS "Contribution" means: a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and b) in the case of each subsequent Contributor: i) changes to the Program, and ii) additions to the Program; where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. "Contributor" means any person or entity that distributes the Program. "Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. "Program" means the Contributions distributed in accordance with this Agreement. "Recipient" means anyone who receives the Program under this Agreement, including all Contributors. 2. GRANT OF RIGHTS a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. 3. REQUIREMENTS A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: a) it complies with the terms and conditions of this Agreement; and b) its license agreement: i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. When the Program is made available in source code form: a) it must be made available under this Agreement; and b) a copy of this Agreement must be included with each copy of the Program. Contributors may not remove or alter any copyright notices contained within the Program. Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. 4. COMMERCIAL DISTRIBUTION Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. 5. NO WARRANTY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. 6. DISCLAIMER OF LIABILITY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 7. GENERAL If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. asyncssh-1.3.0/MANIFEST.in000066400000000000000000000000761260630620200151040ustar00rootroot00000000000000include COPYRIGHT LICENSE README.rst examples/*.py tests/*.py asyncssh-1.3.0/README.rst000066400000000000000000000111201260630620200150250ustar00rootroot00000000000000AsyncSSH: Asynchronous SSH for Python ===================================== AsyncSSH is a Python package which provides an asynchronous client and server implementation of the SSHv2 protocol on top of the Python 3.4+ asyncio framework. .. code:: python import asyncio, asyncssh, sys @asyncio.coroutine def run_client(): with (yield from asyncssh.connect('localhost')) as conn: stdin, stdout, stderr = yield from conn.open_session('echo "Hello!"') output = yield from stdout.read() print(output, end='') status = stdout.channel.get_exit_status() if status: print('Program exited with status %d' % status, file=sys.stderr) else: print('Program exited successfully') asyncio.get_event_loop().run_until_complete(run_client()) Check out the `examples`__ to get started! __ http://asyncssh.readthedocs.org/en/stable/#client-examples Features -------- * Full support for SSHv2 and SFTP client and server functions * Shell, command, and subsystem channels * Environment variables, terminal type, and window size * Direct and forwarded TCP/IP channels * Local and remote TCP/IP port forwarding * SFTP protocol version 3 with OpenSSH extensions * Multiple simultaneous sessions on a single SSH connection * Multiple SSH connections in a single event loop * Byte and string based I/O with settable encoding * A variety of `key exchange`__, `encryption`__, and `MAC`__ algorithms * Support for `gzip compression`__ * Including OpenSSH variant to delay compression until after auth * Password, public key, and keyboard-interactive user authentication methods * Many types and formats of `public keys and certificates`__ * OpenSSH-style `known_hosts file`__ support * OpenSSH-style `authorized_keys file`__ support * Compatibility with OpenSSH "Encrypt then MAC" option for better security * Time and byte-count based session key renegotiation * Designed to be easy to extend to support new forms of key exchange, authentication, encryption, and compression algorithms License ------- This package is released under the following terms: Copyright (c) 2013-2015 by Ron Frederick . All rights reserved. This program and the accompanying materials are made available under the terms of the **Eclipse Public License v1.0** which accompanies this distribution and is available at: http://www.eclipse.org/legal/epl-v10.html For more information about this license, please see the `Eclipse Public License FAQ `_. Prerequisites ------------- To use ``asyncssh``, you need the following: * Python 3.4 or later * cryptography (PyCA) 1.0.0 or later Installation ------------ Install AsyncSSH by running: :: pip install asyncssh Optional Extras ^^^^^^^^^^^^^^^ There are some optional modules you can install to enable additional functionality: * Install bcrypt from https://code.google.com/p/py-bcrypt if you want support for OpenSSH private key encryption. * Install libsodium from https://github.com/jedisct1/libsodium and libnacl from https://github.com/saltstack/libnacl if you want support for curve25519 Diffie Hellman key exchange, ed25519 keys, and the chacha20-poly1305 cipher. AsyncSSH defines the following optional PyPI extra packages to make it easy to install any or all of these dependencies: | bcrypt | libnacl For example, to install all of these, you can run: :: pip install 'asyncssh[bcrypt,libnacl]' Note that you will still need to manually install the libsodium library listed above for libnacl to work correctly. Unfortunately, since libsodium is not a Python package, it cannot be directly installed using pip. Mailing Lists ------------- Three mailing lists are available for AsyncSSH: * `asyncssh-announce@googlegroups.com`__: Project announcements * `asyncssh-dev@googlegroups.com`__: Development discussions * `asyncssh-users@googlegroups.com`__: End-user discussions __ http://asyncssh.readthedocs.org/en/stable/api.html#key-exchange-algorithms __ http://asyncssh.readthedocs.org/en/stable/api.html#encryption-algorithms __ http://asyncssh.readthedocs.org/en/stable/api.html#mac-algorithms __ http://asyncssh.readthedocs.org/en/stable/api.html#compression-algorithms __ http://asyncssh.readthedocs.org/en/stable/api.html#public-key-support __ http://asyncssh.readthedocs.org/en/stable/api.html#known-hosts __ http://asyncssh.readthedocs.org/en/stable/api.html#authorized-keys __ http://groups.google.com/d/forum/asyncssh-announce __ http://groups.google.com/d/forum/asyncssh-dev __ http://groups.google.com/d/forum/asyncssh-users asyncssh-1.3.0/asyncssh/000077500000000000000000000000001260630620200151765ustar00rootroot00000000000000asyncssh-1.3.0/asyncssh/__init__.py000066400000000000000000000036711260630620200173160ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """An asynchronous SSH2 library for Python""" from .version import __author__, __author_email__, __url__, __version__ # pylint: disable=wildcard-import from .constants import * # pylint: enable=wildcard-import from .auth_keys import SSHAuthorizedKeys from .auth_keys import import_authorized_keys, read_authorized_keys from .channel import SSHClientChannel, SSHServerChannel, SSHTCPChannel from .connection import SSHClient, SSHServer from .connection import SSHClientConnection, SSHServerConnection from .connection import create_connection, create_server, connect, listen from .listener import SSHListener from .logging import logger from .misc import Error, DisconnectError, ChannelOpenError from .misc import BreakReceived, SignalReceived, TerminalSizeChanged from .pbe import KeyEncryptionError from .public_key import SSHKey, SSHCertificate, KeyImportError, KeyExportError from .public_key import import_private_key, import_public_key from .public_key import import_certificate from .public_key import read_private_key, read_public_key, read_certificate from .public_key import read_private_key_list, read_public_key_list from .public_key import read_certificate_list from .session import SSHClientSession, SSHServerSession, SSHTCPSession from .sftp import SFTPClient, SFTPServer, SFTPFile, SFTPError from .sftp import SFTPAttrs, SFTPVFSAttrs, SFTPName from .sftp import SEEK_SET, SEEK_CUR, SEEK_END from .stream import SSHReader, SSHWriter # Import these explicitly to trigger register calls in them from . import ed25519, ecdsa, rsa, dsa, ecdh, dh asyncssh-1.3.0/asyncssh/asn1.py000066400000000000000000000501251260630620200164150ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Utilities for encoding and decoding ASN.1 DER data The der_encode function takes a Python value and encodes it in DER format, returning a byte string. In addition to supporting standard Python types, BitString can be used to encode a DER bit string, ObjectIdentifier can be used to encode OIDs, values can be wrapped in a TaggedDERObject to set an alternate DER tag on them, and non-standard types can be encoded by placing them in a RawDERObject. The der_decode function takes a byte string in DER format and decodes it into the corresponding Python values. """ # pylint: disable=bad-whitespace # ASN.1 object classes UNIVERSAL = 0x00 APPLICATION = 0x01 CONTEXT_SPECIFIC = 0x02 PRIVATE = 0x03 # ASN.1 universal object tags END_OF_CONTENT = 0x00 BOOLEAN = 0x01 INTEGER = 0x02 BIT_STRING = 0x03 OCTET_STRING = 0x04 NULL = 0x05 OBJECT_IDENTIFIER = 0x06 UTF8_STRING = 0x0c SEQUENCE = 0x10 SET = 0x11 # pylint: enable=bad-whitespace _asn1_class = ('Universal', 'Application', 'Context-specific', 'Private') _der_class_by_tag = {} _der_class_by_type = {} def _encode_identifier(asn1_class, constructed, tag): """Encode a DER object's identifier""" if asn1_class not in (UNIVERSAL, APPLICATION, CONTEXT_SPECIFIC, PRIVATE): raise ASN1EncodeError('Invalid ASN.1 class') flags = (asn1_class << 6) | (0x20 if constructed else 0x00) if tag < 0x20: identifier = [flags | tag] else: identifier = [tag & 0x7f] while tag >= 0x80: tag >>= 7 identifier.append(0x80 | (tag & 0x7f)) identifier.append(flags | 0x1f) return bytes(identifier[::-1]) class ASN1Error(ValueError): """ASN.1 coding error""" class ASN1EncodeError(ASN1Error): """ASN.1 DER encoding error""" class ASN1DecodeError(ASN1Error): """ASN.1 DER decoding error""" class DERTag: """A decorator used by classes which convert values to/from DER Classes which convert Python values to and from DER format should use the DERTag decorator to indicate what DER tag value they understand. When DER data is decoded, the tag is looked up in the list to see which class to call to perform the decoding. Classes which convert existing Python types to and from DER format can specify the list of types they understand in the optional "types" argument. Otherwise, conversion is expected to be to and from the new class being defined. """ def __init__(self, tag, types=(), constructed=False): self._tag = tag self._types = types self._identifier = _encode_identifier(UNIVERSAL, constructed, tag) def __call__(self, cls): cls.identifier = self._identifier _der_class_by_tag[self._tag] = cls if self._types: for t in self._types: _der_class_by_type[t] = cls else: _der_class_by_type[cls] = cls return cls class RawDERObject: """A class which can encode a DER object of an arbitrary type This object is initialized with an ASN.1 class, tag, and a byte string representing the already encoded data. Such objects will never have the constructed flag set, since that is represented here as a TaggedDERObject. """ def __init__(self, tag, content, asn1_class): self.asn1_class = asn1_class self.tag = tag self.content = content def __repr__(self): return ('RawDERObject(%s, %s, %r)' % (_asn1_class[self.asn1_class], self.tag, self.content)) def __eq__(self, other): return (isinstance(other, self.__class__) and self.asn1_class == other.asn1_class and self.tag == other.tag and self.content == other.content) def __hash__(self): return hash((self.asn1_class, self.tag, self.content)) def encode_identifier(self): """Encode the DER identifier for this object as a byte string""" return _encode_identifier(self.asn1_class, False, self.tag) def encode(self): """Encode the content for this object as a DER byte string""" return self.content class TaggedDERObject: """An explicitly tagged DER object This object provides a way to wrap an ASN.1 object with an explicit tag. The value (including the tag representing its actual type) is then encoded as part of its value. By default, the ASN.1 class for these objects is CONTEXT_SPECIFIC, and the DER encoding always marks these values as constructed. """ def __init__(self, tag, value, asn1_class=CONTEXT_SPECIFIC): self.asn1_class = asn1_class self.tag = tag self.value = value def __repr__(self): if self.asn1_class == CONTEXT_SPECIFIC: return 'TaggedDERObject(%s, %r)' % (self.tag, self.value) else: return ('TaggedDERObject(%s, %s, %r)' % (_asn1_class[self.asn1_class], self.tag, self.value)) def __eq__(self, other): return (isinstance(other, self.__class__) and self.asn1_class == other.asn1_class and self.tag == other.tag and self.value == other.value) def __hash__(self): return hash((self.asn1_class, self.tag, self.value)) def encode_identifier(self): """Encode the DER identifier for this object as a byte string""" return _encode_identifier(self.asn1_class, True, self.tag) def encode(self): """Encode the content for this object as a DER byte string""" return der_encode(self.value) @DERTag(NULL, (type(None),)) class _Null: """A null value""" @staticmethod def encode(value): """Encode a DER null value""" # pylint: disable=unused-argument return b'' @classmethod def decode(cls, constructed, content): """Decode a DER null value""" if constructed: raise ASN1DecodeError('NULL should not be constructed') if content: raise ASN1DecodeError('NULL should not have associated content') return None @DERTag(BOOLEAN, (bool,)) class _Boolean: """A boolean value""" @staticmethod def encode(value): """Encode a DER boolean value""" return b'\xff' if value else b'\0' @classmethod def decode(cls, constructed, content): """Decode a DER boolean value""" if constructed: raise ASN1DecodeError('BOOLEAN should not be constructed') if content not in {b'\x00', b'\xff'}: raise ASN1DecodeError('BOOLEAN content must be 0x00 or 0xff') return bool(content[0]) @DERTag(INTEGER, (int,)) class _Integer: """An integer value""" @staticmethod def encode(value): """Encode a DER integer value""" l = value.bit_length() l = l // 8 + 1 if l % 8 == 0 else (l + 7) // 8 result = value.to_bytes(l, 'big', signed=True) return result[1:] if result.startswith(b'\xff\x80') else result @classmethod def decode(cls, constructed, content): """Decode a DER integer value""" if constructed: raise ASN1DecodeError('INTEGER should not be constructed') return int.from_bytes(content, 'big', signed=True) @DERTag(OCTET_STRING, (bytes, bytearray)) class _OctetString: """An octet string value""" @staticmethod def encode(value): """Encode a DER octet string""" return value @classmethod def decode(cls, constructed, content): """Decode a DER octet string""" if constructed: raise ASN1DecodeError('OCTET STRING should not be constructed') return content @DERTag(UTF8_STRING, (str,)) class _UTF8String: """A UTF-8 string value""" @staticmethod def encode(value): """Encode a DER UTF-8 string""" return value.encode('utf-8') @classmethod def decode(cls, constructed, content): """Decode a DER UTF-8 string""" if constructed: raise ASN1DecodeError('UTF8 STRING should not be constructed') return content.decode('utf-8') @DERTag(SEQUENCE, (list, tuple), constructed=True) class _Sequence: """A sequence of values""" @staticmethod def encode(value): """Encode a sequence of DER values""" return b''.join(der_encode(item) for item in value) @classmethod def decode(cls, constructed, content): """Decode a sequence of DER values""" if not constructed: raise ASN1DecodeError('SEQUENCE should always be constructed') offset = 0 length = len(content) value = [] while offset < length: # pylint: disable=unpacking-non-sequence item, consumed = der_decode(content[offset:], partial_ok=True) # pylint: enable=unpacking-non-sequence value.append(item) offset += consumed return tuple(value) @DERTag(SET, (set, frozenset), constructed=True) class _Set: """A set of DER values""" @staticmethod def encode(value): """Encode a set of DER values""" return b''.join(sorted(der_encode(item) for item in value)) @classmethod def decode(cls, constructed, content): """Decode a set of DER values""" if not constructed: raise ASN1DecodeError('SET should always be constructed') offset = 0 length = len(content) value = set() while offset < length: # pylint: disable=unpacking-non-sequence item, consumed = der_decode(content[offset:], partial_ok=True) # pylint: enable=unpacking-non-sequence value.add(item) offset += consumed return frozenset(value) @DERTag(BIT_STRING) class BitString: """A string of bits This object can be initialized either with a byte string and an optional count of the number of least-significant bits in the last byte which should not be included in the value, or with a string consisting only of the digits '0' and '1'. An optional 'named' flag can also be set, indicating that the BitString was specified with named bits, indicating that the proper DER encoding of it should strip any trailing zeroes. """ def __init__(self, value, unused=0, named=False): if unused < 0 or unused > 7: raise ASN1EncodeError('Unused bit count must be between 0 and 7') if isinstance(value, bytes): if unused: if not value: raise ASN1EncodeError('Can\'t have unused bits with empty ' 'value') elif value[-1] & ((1 << unused) - 1): raise ASN1EncodeError('Unused bits in value should be ' 'zero') elif isinstance(value, str): if unused: raise ASN1EncodeError('Unused bit count should not be set ' 'when providing a string') used = len(value) % 8 unused = 8 - used if used else 0 value += unused * '0' value = bytes(int(value[i:i+8], 2) for i in range(0, len(value), 8)) else: raise ASN1EncodeError('Unexpected type of bit string value') if named: while value and not value[-1] & (1 << unused): unused += 1 if unused == 8: value = value[:-1] unused = 0 self.value = value self.unused = unused def __str__(self): result = ''.join(bin(b)[2:].zfill(8) for b in self.value) if self.unused: result = result[:-self.unused] return result def __repr__(self): return "BitString('%s')" % self def __eq__(self, other): return (isinstance(other, self.__class__) and self.value == other.value and self.unused == other.unused) def __hash__(self): return hash((self.value, self.unused)) def encode(self): """Encode a DER bit string""" return bytes((self.unused,)) + self.value @classmethod def decode(cls, constructed, content): """Decode a DER bit string""" if constructed: raise ASN1DecodeError('BIT STRING should not be constructed') if not content or content[0] > 7: raise ASN1DecodeError('Invalid unused bit count') return cls(content[1:], unused=content[0]) @DERTag(OBJECT_IDENTIFIER) class ObjectIdentifier: """An object identifier (OID) value This object can be initialized from a string of dot-separated integer values, representing a hierarchical namespace. All OIDs show have at least two components, with the first being between 0 and 2 (indicating ITU-T, ISO, or joint assignment). In cases where the first component is 0 or 1, the second component must be in the range 0 to 39 due to the way these first two components are encoded. """ def __init__(self, value): self.value = value def __str__(self): return self.value def __repr__(self): return "ObjectIdentifier('%s')" % self.value def __eq__(self, other): return isinstance(other, self.__class__) and self.value == other.value def __hash__(self): return hash(self.value) def encode(self): """Encode a DER object identifier""" def _bytes(component): """Convert a single element of an OID to a DER byte string""" if component < 0: raise ASN1EncodeError('Components of object identifier must ' 'be greater than or equal to 0') result = [component & 0x7f] while component >= 0x80: component >>= 7 result.append(0x80 | (component & 0x7f)) return bytes(result[::-1]) try: components = [int(c) for c in self.value.split('.')] except ValueError: raise ASN1EncodeError('Component values must be integers') if len(components) < 2: raise ASN1EncodeError('Object identifiers must have at least two ' 'components') elif components[0] < 0 or components[0] > 2: raise ASN1EncodeError('First component of object identifier must ' 'be between 0 and 2') elif components[0] < 2 and (components[1] < 0 or components[1] > 39): raise ASN1EncodeError('Second component of object identifier must ' 'be between 0 and 39') components[0:2] = [components[0]*40 + components[1]] return b''.join(_bytes(c) for c in components) @classmethod def decode(cls, constructed, content): """Decode a DER object identifier""" if constructed: raise ASN1DecodeError('OBJECT IDENTIFIER should not be ' 'constructed') if not content: raise ASN1DecodeError('Empty object identifier') b = content[0] components = list(divmod(b, 40)) if b < 80 else [2, b-80] component = 0 for b in content[1:]: if b == 0x80 and component == 0: raise ASN1DecodeError('Invalid component') elif b < 0x80: components.append(component | b) component = 0 else: component |= b & 0x7f component <<= 7 if component: raise ASN1DecodeError('Incomplete component') return cls('.'.join(str(c) for c in components)) def der_encode(value): """Encode a value in DER format This function takes a Python value and encodes it in DER format. The following mapping of types is used: NoneType -> NULL bool -> BOOLEAN int -> INTEGER bytes, bytearray -> OCTET STRING str -> UTF8 STRING list, tuple -> SEQUENCE set, frozenset -> SET BitString -> BIT STRING ObjectIdentifier -> OBJECT IDENTIFIER An explicitly tagged DER object can be encoded by passing in a TaggedDERObject which specifies the ASN.1 class, tag, and value to encode. Other types can be encoded by passing in a RawDERObject which specifies the ASN.1 class, tag, and raw content octets to encode. """ t = type(value) if t in (RawDERObject, TaggedDERObject): identifier = value.encode_identifier() content = value.encode() elif t in _der_class_by_type: cls = _der_class_by_type[t] identifier = cls.identifier content = cls.encode(value) else: raise ASN1EncodeError('Cannot DER encode type %s' % t.__name__) length = len(content) if length < 0x80: len_bytes = bytes((length,)) else: len_bytes = length.to_bytes((length.bit_length() + 7) // 8, 'big') len_bytes = bytes((0x80 | len(len_bytes),)) + len_bytes return identifier + len_bytes + content def der_decode(data, partial_ok=False): """Decode a value in DER format This function takes a byte string in DER format and converts it to a corresponding set of Python objects. The following mapping of ASN.1 tags to Python types is used: NULL -> NoneType BOOLEAN -> bool INTEGER -> int OCTET STRING -> bytes UTF8 STRING -> str SEQUENCE -> tuple SET -> frozenset BIT_STRING -> BitString OBJECT IDENTIFIER -> ObjectIdentifier Explicitly tagged objects are returned as type TaggedDERObject, with fields holding the object class, tag, and tagged value. Other object tags are returned as type RawDERObject, with fields holding the object class, tag, and raw content octets. If partial_ok is True, this function returns a tuple of the decoded value and number of bytes consumed. Otherwise, all data bytes must be consumed and only the decoded value is returned. """ if len(data) < 2: raise ASN1DecodeError('Incomplete data') tag = data[0] asn1_class, constructed, tag = tag >> 6, bool(tag & 0x20), tag & 0x1f offset = 1 if tag == 0x1f: tag = 0 for b in data[offset:]: offset += 1 if b < 0x80: tag |= b break else: tag |= b & 0x7f tag <<= 7 else: raise ASN1DecodeError('Incomplete tag') if offset >= len(data): raise ASN1DecodeError('Incomplete data') length = data[offset] offset += 1 if length > 0x80: len_size = length & 0x7f length = int.from_bytes(data[offset:offset+len_size], 'big') offset += len_size elif length == 0x80: raise ASN1DecodeError('Indefinite length not allowed') if offset+length > len(data): raise ASN1DecodeError('Incomplete data') if not partial_ok and offset+length < len(data): raise ASN1DecodeError('Data contains unexpected bytes at end') if asn1_class == UNIVERSAL and tag in _der_class_by_tag: cls = _der_class_by_tag[tag] value = cls.decode(constructed, data[offset:offset+length]) elif constructed: value = TaggedDERObject(tag, der_decode(data[offset:offset+length]), asn1_class) else: value = RawDERObject(tag, data[offset:offset+length], asn1_class) if partial_ok: return value, offset+length else: return value asyncssh-1.3.0/asyncssh/auth.py000066400000000000000000000366671260630620200165330ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """SSH authentication handlers""" import asyncio from .constants import DISC_PROTOCOL_ERROR from .misc import DisconnectError from .packet import Boolean, Byte, String, UInt32, SSHPacketHandler from .saslprep import saslprep, SASLPrepError # pylint: disable=bad-whitespace # SSH message values for public key auth MSG_USERAUTH_PK_OK = 60 # SSH message values for password auth MSG_USERAUTH_PASSWD_CHANGEREQ = 60 # SSH message values for 'keyboard-interactive' auth MSG_USERAUTH_INFO_REQUEST = 60 MSG_USERAUTH_INFO_RESPONSE = 61 # pylint: enable=bad-whitespace _auth_methods = [] _client_auth_handlers = {} _server_auth_handlers = {} class _Auth(SSHPacketHandler): """Parent class for authentication""" def __init__(self): self._coro = None def cancel(self): """Cancel any authentication in progress""" if self._coro: self._coro.cancel() self._coro = None class _ClientAuth(_Auth): """Parent class for client authentication""" def __init__(self, conn, method): super().__init__() self._conn = conn self._method = method self._coro = asyncio.async(self._start()) @asyncio.coroutine def _start(self): """Abstract method for starting client authentication""" # Provided by subclass raise NotImplementedError def auth_succeeded(self): """Callback when auth succeeds""" def auth_failed(self): """Callback when auth fails""" def send_request(self, *args, key=None): """Send a user authentication request""" self._conn.send_userauth_request(self._method, *args, key=key) class _ClientNullAuth(_ClientAuth): """Client side implementation of null auth""" @asyncio.coroutine def _start(self): """Start client null authentication""" self.send_request() packet_handlers = {} class _ClientPublicKeyAuth(_ClientAuth): """Client side implementation of public key auth""" @asyncio.coroutine def _start(self): """Start client public key authentication""" self._alg, self._key, self._key_data = \ yield from self._conn.public_key_auth_requested() if self._alg is None: self._conn.try_next_auth() return self.send_request(Boolean(False), String(self._alg), String(self._key_data)) def _process_public_key_ok(self, pkttype, packet): """Process a public key ok response""" # pylint: disable=unused-argument algorithm = packet.get_string() key_data = packet.get_string() packet.check_end() if algorithm != self._alg or key_data != self._key_data: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Key mismatch') self.send_request(Boolean(True), String(algorithm), String(key_data), key=self._key) return True packet_handlers = { MSG_USERAUTH_PK_OK: _process_public_key_ok } class _ClientKbdIntAuth(_ClientAuth): """Client side implementation of keyboard-interactive auth""" @asyncio.coroutine def _start(self): """Start client keyboard interactive authentication""" submethods = yield from self._conn.kbdint_auth_requested() if submethods is None: self._conn.try_next_auth() return self.send_request(String(''), String(submethods)) @asyncio.coroutine def _receive_challenge(self, name, instruction, lang, prompts): """Receive and respond to a keyboard interactive challenge""" responses = \ yield from self._conn.kbdint_challenge_received(name, instruction, lang, prompts) if responses is None: self._conn.try_next_auth() return self._conn.send_packet(Byte(MSG_USERAUTH_INFO_RESPONSE), UInt32(len(responses)), b''.join(String(r) for r in responses)) def _process_info_request(self, pkttype, packet): """Process a keyboard interactive authentication request""" # pylint: disable=unused-argument name = packet.get_string() instruction = packet.get_string() lang = packet.get_string() try: name = name.decode('utf-8') instruction = instruction.decode('utf-8') lang = lang.decode('ascii') except UnicodeDecodeError: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid keyboard ' 'interactive info request') from None num_prompts = packet.get_uint32() prompts = [] for _ in range(num_prompts): prompt = packet.get_string() echo = packet.get_boolean() try: prompt = prompt.decode('utf-8') except UnicodeDecodeError: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid keyboard ' 'interactive info request') from None prompts.append((prompt, echo)) self.cancel() self._coro = asyncio.async(self._receive_challenge(name, instruction, lang, prompts)) return True packet_handlers = { MSG_USERAUTH_INFO_REQUEST: _process_info_request } class _ClientPasswordAuth(_ClientAuth): """Client side implementation of password auth""" def __init__(self, conn, method): super().__init__(conn, method) self._password_change = False @asyncio.coroutine def _start(self): """Start client password authentication""" password = yield from self._conn.password_auth_requested() if password is None: self._conn.try_next_auth() return self.send_request(Boolean(False), String(password)) @asyncio.coroutine def _change_password(self): """Start password change""" result = yield from self._conn.password_change_requested() if result == NotImplemented: # Password change not supported - move on to the next auth method self._conn.try_next_auth() return old_password, new_password = result self._password_change = True self.send_request(Boolean(True), String(old_password.encode('utf-8')), String(new_password.encode('utf-8'))) def auth_succeeded(self): if self._password_change: self._password_change = False self._conn.password_changed() def auth_failed(self): if self._password_change: self._password_change = False self._conn.password_change_failed() def _process_password_change(self, pkttype, packet): """Process a password change request""" # pylint: disable=unused-argument prompt = packet.get_string() lang = packet.get_string() try: prompt = prompt.decode('utf-8') lang = lang.decode('ascii') except UnicodeDecodeError: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid password change request') from None self.cancel() self._coro = asyncio.async(self._change_password()) return True packet_handlers = { MSG_USERAUTH_PASSWD_CHANGEREQ: _process_password_change } class _ServerAuth(_Auth): """Parent class for server authentication""" def __init__(self, conn, username, packet): super().__init__() self._conn = conn self._username = username self._coro = asyncio.async(self._start(packet)) @asyncio.coroutine def _start(self, packet): """Abstract method for starting server authentication""" # Provided by subclass raise NotImplementedError def send_failure(self, partial_success=False): """Send a user authentication failure response""" self._conn.send_userauth_failure(partial_success) def send_success(self): """Send a user authentication success response""" self._conn.send_userauth_success() class _ServerNullAuth(_ServerAuth): """Server side implementation of null auth""" @classmethod def supported(cls, conn): """Return that null authentication is never a supported auth mode""" # pylint: disable=unused-argument return False @asyncio.coroutine def _start(self, packet): """Always fail null server authentication""" packet.check_end() self.send_failure() class _ServerPublicKeyAuth(_ServerAuth): """Server side implementation of public key auth""" @classmethod def supported(cls, conn): """Return whether public key authentication is supported""" return conn.public_key_auth_supported() @asyncio.coroutine def _start(self, packet): """Start server public key authentication""" sig_present = packet.get_boolean() algorithm = packet.get_string() key_data = packet.get_string() if sig_present: msg = packet.get_consumed_payload() signature = packet.get_string() else: msg = None signature = None packet.check_end() if (yield from self._conn.validate_public_key(self._username, key_data, msg, signature)): if sig_present: self.send_success() else: self._conn.send_packet(Byte(MSG_USERAUTH_PK_OK), String(algorithm), String(key_data)) else: self.send_failure() class _ServerKbdIntAuth(_ServerAuth): """Server side implementation of keyboard-interactive auth""" @classmethod def supported(cls, conn): """Return whether keyboard interactive authentication is supported""" return conn.kbdint_auth_supported() @asyncio.coroutine def _start(self, packet): """Start server keyboard interactive authentication""" lang = packet.get_string() submethods = packet.get_string() packet.check_end() try: lang = lang.decode('ascii') submethods = submethods.decode('utf-8') except UnicodeDecodeError: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid keyboard ' 'interactive auth request') from None challenge = yield from self._conn.get_kbdint_challenge(self._username, lang, submethods) self._send_challenge(challenge) def _send_challenge(self, challenge): """Send a keyboard interactive authentication request""" if isinstance(challenge, (tuple, list)): name, instruction, lang, prompts = challenge num_prompts = len(prompts) prompts = (String(prompt) + Boolean(echo) for prompt, echo in prompts) self._conn.send_packet(Byte(MSG_USERAUTH_INFO_REQUEST), String(name), String(instruction), String(lang), UInt32(num_prompts), *prompts) elif challenge: self.send_success() else: self.send_failure() @asyncio.coroutine def _validate_response(self, responses): """Validate a keyboard interactive authentication response""" next_challenge = \ yield from self._conn.validate_kbdint_response(self._username, responses) self._send_challenge(next_challenge) def _process_info_response(self, pkttype, packet): """Process a keyboard interactive authentication response""" # pylint: disable=unused-argument num_responses = packet.get_uint32() responses = [] for _ in range(num_responses): response = packet.get_string() try: response = response.decode('utf-8') except UnicodeDecodeError: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid keyboard ' 'interactive info response') from None responses.append(response) packet.check_end() self.cancel() self._coro = asyncio.async(self._validate_response(responses)) packet_handlers = { MSG_USERAUTH_INFO_RESPONSE: _process_info_response } class _ServerPasswordAuth(_ServerAuth): """Server side implementation of password auth""" @classmethod def supported(cls, conn): """Return whether password authentication is supported""" return conn.password_auth_supported() @asyncio.coroutine def _start(self, packet): """Start server password authentication""" password_change = packet.get_boolean() password = packet.get_string() new_password = packet.get_string() if password_change else b'' packet.check_end() try: password = saslprep(password.decode('utf-8')) new_password = saslprep(new_password.decode('utf-8')) except (UnicodeDecodeError, SASLPrepError): raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid password auth ' 'request') from None # TODO: Handle password change request if (yield from self._conn.validate_password(self._username, password)): self.send_success() else: self.send_failure() def register_auth_method(alg, client_handler, server_handler): """Register an authentication method""" _auth_methods.append(alg) _client_auth_handlers[alg] = client_handler _server_auth_handlers[alg] = server_handler def lookup_client_auth(conn, method): """Look up the client authentication method to use""" if method in _auth_methods: return _client_auth_handlers[method](conn, method) else: return None def get_server_auth_methods(conn): """Return a list of supported auth methods""" auth_methods = [] for method in _auth_methods: if _server_auth_handlers[method].supported(conn): auth_methods.append(method) return auth_methods def lookup_server_auth(conn, username, method, packet): """Look up the server authentication method to use""" if method in _auth_methods: return _server_auth_handlers[method](conn, username, packet) else: conn.send_userauth_failure(False) return None # pylint: disable=bad-whitespace _auth_method_list = ( (b'none', _ClientNullAuth, _ServerNullAuth), (b'publickey', _ClientPublicKeyAuth, _ServerPublicKeyAuth), (b'keyboard-interactive', _ClientKbdIntAuth, _ServerKbdIntAuth), (b'password', _ClientPasswordAuth, _ServerPasswordAuth) ) # pylint: enable=bad-whitespace for _args in _auth_method_list: register_auth_method(*_args) asyncssh-1.3.0/asyncssh/auth_keys.py000066400000000000000000000144011260630620200175440ustar00rootroot00000000000000# Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Parser for SSH known_hosts files""" import socket from .misc import ip_address from .pattern import HostPatternList, WildcardPatternList from .public_key import import_public_key, KeyImportError class _SSHAuthorizedKeyEntry: """An entry in an SSH authorized_keys list""" def __init__(self, line): self.options = {} try: self.key = import_public_key(line) return except KeyImportError: pass line = self._parse_options(line) self.key = import_public_key(line) def _set_string(self, option, value): """Set an option with a string value""" self.options[option] = value def _add_environment(self, option, value): """Add an environment key/value pair""" if value.startswith('=') or '=' not in value: raise ValueError('Invalid environment entry in authorized_keys') name, value = value.split('=', 1) self.options.setdefault(option, {})[name] = value def _add_from(self, option, value): """Add a from host pattern""" self.options.setdefault(option, []).append(HostPatternList(value)) def _add_permitopen(self, option, value): """Add a permitopen host/port pair""" try: host, port = value.rsplit(':', 1) if host.startswith('[') and host.endswith(']'): host = host[1:-1] port = None if port == '*' else int(port) except: raise ValueError('Illegal permitopen value: %s' % value) from None self.options.setdefault(option, set()).add((host, port)) def _add_principals(self, option, value): """Add a principals wildcard pattern list""" self.options.setdefault(option, []).append(WildcardPatternList(value)) _handlers = { 'command': _set_string, 'environment': _add_environment, 'from': _add_from, 'permitopen': _add_permitopen, 'principals': _add_principals } def _add_option(self): """Add an option value""" if self._option.startswith('='): raise ValueError('Missing option name in authorized_keys') if '=' in self._option: option, value = self._option.split('=', 1) handler = self._handlers.get(option) if handler: handler(self, option, value) else: self.options.setdefault(option, []).append(value) else: self.options[self._option] = True def _parse_options(self, line): """Parse options in this entry""" self._option = '' idx = 0 quoted = False escaped = False for idx, ch in enumerate(line): if escaped: self._option += ch escaped = False elif ch == '\\': escaped = True elif ch == '"': quoted = not quoted elif quoted: self._option += ch elif ch in ' \t': break elif ch == ',': self._add_option() self._option = '' else: self._option += ch self._add_option() if quoted: raise ValueError('Unbalanced quote in authorized_keys') elif escaped: raise ValueError('Unbalanced backslash in authorized_keys') return line[idx:].strip() class SSHAuthorizedKeys: """An SSH authorized keys list""" def __init__(self, data): self._user_entries = [] self._ca_entries = [] for line in data.splitlines(): line = line.strip() if not line or line.startswith('#'): continue try: entry = _SSHAuthorizedKeyEntry(line) except KeyImportError: continue if 'cert-authority' in entry.options: self._ca_entries.append(entry) else: self._user_entries.append(entry) def validate(self, key, client_addr, cert_principals=None, ca=False): """Return whether a public key or CA is valid for authentication""" for entry in self._ca_entries if ca else self._user_entries: if entry.key != key: continue from_patterns = entry.options.get('from') if from_patterns is not None: client_host, _ = socket.getnameinfo((client_addr, 0), socket.NI_NUMERICSERV) client_ip = ip_address(client_addr) if not all(pattern.matches(client_host, client_addr, client_ip) for pattern in from_patterns): continue principal_patterns = entry.options.get('principals') if cert_principals is not None and principal_patterns is not None: if not all(any(pattern.matches(principal) for principal in cert_principals) for pattern in principal_patterns): continue return entry.options return None def import_authorized_keys(data): """Import SSH authorized keys This function imports public keys and associated options in OpenSSH authorized keys format. :param string data: The key data to import. :returns: An :class:`SSHAuthorizedKeys` object """ return SSHAuthorizedKeys(data) def read_authorized_keys(filename): """Read SSH authorized keys from a file This function reads public keys and associated options in OpenSSH authorized_keys format from a file. :param string filename: The file to read the keys from. :returns: An :class:`SSHAuthorizedKeys` object """ with open(filename, 'r') as f: return import_authorized_keys(f.read()) asyncssh-1.3.0/asyncssh/channel.py000066400000000000000000001424221260630620200171650ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """SSH channel and session handlers""" import asyncio from .constants import DEFAULT_LANG, DISC_PROTOCOL_ERROR, EXTENDED_DATA_STDERR from .constants import MSG_CHANNEL_OPEN, MSG_CHANNEL_WINDOW_ADJUST from .constants import MSG_CHANNEL_DATA, MSG_CHANNEL_EXTENDED_DATA from .constants import MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MSG_CHANNEL_REQUEST from .constants import MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE from .constants import OPEN_CONNECT_FAILED, PTY_OP_RESERVED, PTY_OP_END from .constants import OPEN_REQUEST_PTY_FAILED, OPEN_REQUEST_SESSION_FAILED from .misc import ChannelOpenError, DisconnectError from .packet import Boolean, Byte, String, UInt32, SSHPacketHandler from .sftp import SFTPServerSession _EOF = object() class SSHChannel(SSHPacketHandler): """Parent class for SSH channels""" _read_datatypes = set() _write_datatypes = set() def __init__(self, conn, loop, encoding, window, max_pktsize): """Initialize an SSH channel If encoding is set, data sent and received will be in the form of strings, converted on the wire to bytes using the specified encoding. If encoding is None, data sent and received must be provided as bytes. Window specifies the initial receive window size. Max_pktsize specifies the maximum length of a single data packet. """ self._conn = conn self._loop = loop self._session = None self._encoding = encoding self._extra = {'connection': conn} self._send_state = 'closed' self._send_chan = None self._send_window = None self._send_pktsize = None self._send_paused = False self._send_buf = [] self._send_buf_len = 0 self._recv_state = 'closed' self._init_recv_window = window self._recv_window = window self._recv_pktsize = max_pktsize self._recv_paused = True self._recv_buf = [] self._recv_partial = {} self._open_waiter = None self._request_waiters = [] self._close_waiters = [] self.set_write_buffer_limits() self._recv_chan = conn.add_channel(self) def get_loop(self): """Return the event loop used by this channel""" return self._loop def get_encoding(self): """Return the encoding used by this channel""" return self._encoding def get_recv_window(self): """Return the configured receive window for this channel""" return self._init_recv_window def get_read_datatypes(self): """Return the legal read data types for this channel""" return self._read_datatypes def _cleanup(self, exc=None): """Clean up this channel""" if self._open_waiter: self._open_waiter.set_exception( ChannelOpenError(OPEN_CONNECT_FAILED, 'SSH connection closed')) self._open_waiter = None if self._request_waiters: for waiter in self._request_waiters: waiter.set_exception(exc) self._request_waiters = [] if self._close_waiters: for waiter in self._close_waiters: if not waiter.cancelled(): waiter.set_result(None) self._close_waiters = [] if self._session: self._session.connection_lost(exc) self._session = None if self._conn: if self._recv_chan: self._conn.remove_channel(self._recv_chan) self._recv_chan = None self._conn = None self._send_state = 'closed' self._recv_state = 'closed' def _pause_resume_writing(self): """Pause or resume writing based on send buffer low/high water marks""" if self._send_paused: if self._send_buf_len <= self._send_low_water: self._send_paused = False self._session.resume_writing() else: if self._send_buf_len > self._send_high_water: self._send_paused = True self._session.pause_writing() def _flush_send_buf(self): """Flush as much data in send buffer as the send window allows""" while self._send_buf and self._send_window: pktsize = min(self._send_window, self._send_pktsize) buf, datatype = self._send_buf[0] if len(buf) > pktsize: data = buf[:pktsize] del buf[:pktsize] else: data = buf del self._send_buf[0] self._send_buf_len -= len(data) self._send_window -= len(data) if datatype is None: self._send_packet(MSG_CHANNEL_DATA, String(data)) else: self._send_packet(MSG_CHANNEL_EXTENDED_DATA, UInt32(datatype), String(data)) self._pause_resume_writing() if not self._send_buf: if self._send_state == 'eof_pending': self._send_packet(MSG_CHANNEL_EOF) self._send_state = 'eof_sent' elif self._send_state == 'close_pending': self._send_packet(MSG_CHANNEL_CLOSE) self._send_state = 'close_sent' def _deliver_data(self, data, datatype): """Deliver incoming data to the session""" if data == _EOF: if datatype in self._recv_partial: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Unicode decode error') if not self._session.eof_received() and self._send_state == 'open': self.write_eof() else: self._recv_window -= len(data) if self._recv_window < self._init_recv_window / 2: self._send_packet(MSG_CHANNEL_WINDOW_ADJUST, UInt32(self._init_recv_window - self._recv_window)) self._recv_window = self._init_recv_window if self._encoding: if datatype in self._recv_partial: encdata = self._recv_partial.pop(datatype) + data else: encdata = data while encdata: try: data = encdata.decode(self._encoding) encdata = b'' except UnicodeDecodeError as exc: if exc.start > 0: # Avoid pylint false positive # pylint: disable=invalid-slice-index data = encdata[:exc.start].decode() encdata = encdata[exc.start:] elif exc.reason == 'unexpected end of data': break else: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Unicode decode error') self._session.data_received(data, datatype) if encdata: self._recv_partial[datatype] = encdata else: self._session.data_received(data, datatype) def _accept_data(self, data, datatype=None): """Accept new data on the channel This method accepts new data on the channel, immediately delivering it to the session if it hasn't paused reading. If it has paused, data is buffered until reading is resumed. Data sent after the channel has been closed by the session is dropped. """ if not data: return if self._send_state in {'close_pending', 'close_sent', 'closed'}: return if data != _EOF and len(data) > self._recv_window: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Window exceeded') if self._recv_paused: self._recv_buf.append((data, datatype)) else: self._deliver_data(data, datatype) def process_connection_close(self, exc): """Process the SSH connection closing""" self._cleanup(exc) def process_open(self, send_chan, send_window, send_pktsize, session): """Process a channel open request""" if self._recv_state != 'closed': raise DisconnectError(DISC_PROTOCOL_ERROR, 'Channel already open') self._send_state = 'open_received' self._send_chan = send_chan self._send_window = send_window self._send_pktsize = send_pktsize asyncio.async(self._finish_open_request(session), loop=self._loop) @asyncio.coroutine def _finish_open_request(self, session): """Finish processing a channel open request""" try: if asyncio.iscoroutine(session): session = yield from session self._session = session self._conn.send_channel_open_confirmation(self._send_chan, self._recv_chan, self._recv_window, self._recv_pktsize) self._send_state = 'open' self._recv_state = 'open' self._session.connection_made(self) except ChannelOpenError as exc: self._conn.send_channel_open_failure(self._send_chan, exc.code, exc.reason, exc.lang) self._loop.call_soon(self._cleanup) def process_open_confirmation(self, send_chan, send_window, send_pktsize, packet): """Process a channel open confirmation""" if not self._open_waiter: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Channel not being opened') self._send_chan = send_chan self._send_window = send_window self._send_pktsize = send_pktsize self._send_state = 'open' self._recv_state = 'open' if not self._open_waiter.cancelled(): self._open_waiter.set_result(packet) self._open_waiter = None def process_open_failure(self, code, reason, lang): """Process a channel open failure""" if not self._open_waiter: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Channel not being opened') self._open_waiter.set_exception(ChannelOpenError(code, reason, lang)) self._open_waiter = None self._loop.call_soon(self._cleanup) def _process_window_adjust(self, pkttype, packet): """Process a send window adjustment""" # pylint: disable=unused-argument if self._recv_state not in {'open', 'eof_received'}: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Channel not open') adjust = packet.get_uint32() packet.check_end() self._send_window += adjust self._flush_send_buf() def _process_data(self, pkttype, packet): """Process incoming data""" # pylint: disable=unused-argument if self._recv_state != 'open': raise DisconnectError(DISC_PROTOCOL_ERROR, 'Channel not open for sending') data = packet.get_string() packet.check_end() self._accept_data(data) def _process_extended_data(self, pkttype, packet): """Process incoming extended data""" # pylint: disable=unused-argument if self._recv_state != 'open': raise DisconnectError(DISC_PROTOCOL_ERROR, 'Channel not open for sending') datatype = packet.get_uint32() data = packet.get_string() packet.check_end() if datatype not in self._read_datatypes: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid extended data type') self._accept_data(data, datatype) def _process_eof(self, pkttype, packet): """Process an incoming end of file""" # pylint: disable=unused-argument if self._recv_state != 'open': raise DisconnectError(DISC_PROTOCOL_ERROR, 'Channel not open for sending') packet.check_end() self._recv_state = 'eof_received' self._accept_data(_EOF) def _process_close(self, pkttype, packet): """Process an incoming channel close""" # pylint: disable=unused-argument if self._recv_state not in {'open', 'eof_received'}: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Channel not open') packet.check_end() # Flush any unsent data self._send_buf = [] self._send_buf_len = 0 # If we haven't yet sent a close, send one now if self._send_state not in {'close_sent', 'closed'}: self._send_packet(MSG_CHANNEL_CLOSE) self._send_state = 'close_sent' self._loop.call_soon(self._cleanup) def _process_request(self, pkttype, packet): """Process an incoming channel request""" # pylint: disable=unused-argument if self._recv_state not in {'open', 'eof_received'}: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Channel not open') if self._send_state in {'close_pending', 'close_sent', 'closed'}: return request = packet.get_string() want_reply = packet.get_boolean() try: request = request.decode('ascii') except UnicodeDecodeError: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid channel request') from None name = '_process_' + request.replace('-', '_') + '_request' handler = getattr(self, name, None) result = handler(packet) if callable(handler) else False if want_reply: if result: self._send_packet(MSG_CHANNEL_SUCCESS) else: self._send_packet(MSG_CHANNEL_FAILURE) if result and request in ('shell', 'exec', 'subsystem'): self._session.session_started() self.resume_reading() def _process_response(self, pkttype, packet): """Process a success or failure response""" # pylint: disable=unused-argument if self._send_state not in {'open', 'eof_pending', 'eof_sent', 'close_pending', 'close_sent'}: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Channel not open') packet.check_end() if self._request_waiters: waiter = self._request_waiters.pop(0) if not waiter.cancelled(): waiter.set_result(pkttype == MSG_CHANNEL_SUCCESS) else: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Unexpected channel response') packet_handlers = { MSG_CHANNEL_WINDOW_ADJUST: _process_window_adjust, MSG_CHANNEL_DATA: _process_data, MSG_CHANNEL_EXTENDED_DATA: _process_extended_data, MSG_CHANNEL_EOF: _process_eof, MSG_CHANNEL_CLOSE: _process_close, MSG_CHANNEL_REQUEST: _process_request, MSG_CHANNEL_SUCCESS: _process_response, MSG_CHANNEL_FAILURE: _process_response } @asyncio.coroutine def _open(self, chantype, *args): """Make a request to open the channel""" if self._send_state != 'closed': raise OSError('Channel already open') self._open_waiter = asyncio.Future(loop=self._loop) self._conn.send_packet(Byte(MSG_CHANNEL_OPEN), String(chantype), UInt32(self._recv_chan), UInt32(self._recv_window), UInt32(self._recv_pktsize), *args) self._send_state = 'open_sent' return (yield from self._open_waiter) def _send_packet(self, pkttype, *args): """Send a packet on the channel""" if self._send_chan is None: raise OSError('Channel not open') self._conn.send_packet(Byte(pkttype), UInt32(self._send_chan), *args) def _send_request(self, request, *args, want_reply=False): """Send a channel request""" self._send_packet(MSG_CHANNEL_REQUEST, String(request), Boolean(want_reply), *args) @asyncio.coroutine def _make_request(self, request, *args): """Make a channel request and wait for the response""" waiter = asyncio.Future(loop=self._loop) self._request_waiters.append(waiter) self._send_request(request, *args, want_reply=True) return (yield from waiter) def abort(self): """Forcibly close the channel This method can be called to forcibly close the channel, after which no more data can be sent or received. Any unsent buffered data and any incoming data in flight will be discarded. """ if self._send_state not in {'close_sent', 'closed'}: self._send_packet(MSG_CHANNEL_CLOSE) self._send_state = 'close_sent' def close(self): """Cleanly close the channel This method can be called to cleanly close the channel, after which no more data can be sent or received. Any unsent buffered data will be flushed asynchronously before the channel is closed. """ if self._send_state not in {'close_pending', 'close_sent', 'closed'}: self._send_state = 'close_pending' self._flush_send_buf() @asyncio.coroutine def wait_closed(self): """Wait for this channel to close This method is a coroutine which can be called to block until this channel has finished closing. """ if self._session: waiter = asyncio.Future(loop=self._loop) self._close_waiters.append(waiter) yield from waiter def get_extra_info(self, name, default=None): """Get additional information about the channel This method returns extra information about the channel once it is established. Supported values include ``'connection'`` to return the SSH connection this channel is running over plus all of the values supported on that connection. For TCP channels, the values ``'local_peername'`` and ``'remote_peername'`` are added to return the local and remote host and port information for the tunneled TCP connection. See :meth:`get_extra_info() ` on :class:`SSHClientConnection` for more information. """ return self._extra.get(name, self._conn.get_extra_info(name, default) if self._conn else default) def can_write_eof(self): """Return whether the channel supports :meth:`write_eof` This method always returns ``True``. """ # pylint: disable=no-self-use return True def get_write_buffer_size(self): """Return the current size of the channel's output buffer This method returns how many bytes are currently in the channel's output buffer waiting to be written. """ return self._send_buf_len def set_write_buffer_limits(self, high=None, low=None): """Set the high- and low-water limits for write flow control This method sets the limits used when deciding when to call the ``pause_writing()`` and ``resume_writing()`` methods on SSH sessions. Writing will be paused when the write buffer size exceeds the high-water mark, and resumed when the write buffer size equals or drops below the low-water mark. """ if high is None: high = 4*low if low is not None else 65536 if low is None: low = high // 4 if not 0 <= low <= high: raise ValueError('high (%r) must be >= low (%r) must be >= 0' % (high, low)) self._send_high_water = high self._send_low_water = low self._pause_resume_writing() def write(self, data, datatype=None): """Write data on the channel This method can be called to send data on the channel. If an encoding was specified when the channel was created, the data should be provided as a string and will be converted using that encoding. Otherwise, the data should be provided as bytes. An extended data type can optionally be provided. For instance, this is used from a :class:`SSHServerSession` to write data to ``stderr``. :param data: The data to send on the channel :param integer datatype: (optional) The extended data type of the data, from :ref:`extended data types ` :type data: string or bytes :raises: :exc:`OSError` if the channel isn't open for sending or the extended data type is not valid for this type of channel """ if self._send_state != 'open': raise BrokenPipeError('Channel not open for sending') if datatype is not None and datatype not in self._write_datatypes: raise OSError('Invalid extended data type') if len(data) == 0: return if self._encoding: data = data.encode(self._encoding) self._send_buf.append((bytearray(data), datatype)) self._send_buf_len += len(data) self._flush_send_buf() def writelines(self, list_of_data, datatype=None): """Write a list of data bytes on the channel This method can be called to write a list (or any iterable) of data bytes to the channel. It is functionality equivalent to calling :meth:`write` on each element in the list. :param list_of_data: The data to send on the channel :param integer datatype: (optional) The extended data type of the data, from :ref:`extended data types ` :type list_of_data: iterable of ``string`` or ``bytes`` objects :raises: :exc:`OSError` if the channel isn't open for sending or the extended data type is not valid for this type of channel """ sep = '' if self._encoding else b'' return self.write(sep.join(list_of_data), datatype) def write_eof(self): """Write EOF on the channel This method sends an end-of-file indication on the channel, after which no more data can be sent. The channel remains open, though, and data may still be sent in the other direction. :raises: :exc:`OSError` if the channel isn't open for sending """ if self._send_state != 'open': raise BrokenPipeError('Channel not open for sending') self._send_state = 'eof_pending' self._flush_send_buf() def pause_reading(self): """Pause delivery of incoming data This method is used to temporarily suspend delivery of incoming channel data. After this call, incoming data will no longer be delivered until :meth:`resume_reading` is called. Data will be buffered locally up to the configured SSH channel window size, but window updates will no longer be sent, eventually causing back pressure on the remote system. .. note:: Channel close notifications are not suspended by this call. If the remote system closes the channel while delivery is suspended, the channel will be closed even though some buffered data may not have been delivered. """ self._recv_paused = True def resume_reading(self): """Resume delivery of incoming data This method can be called to resume delivery of incoming data which was suspended by a call to :meth:`pause_reading`. As soon as this method is called, any buffered data will be delivered immediately. A pending end-of-file notication may also be delivered if one was queued while reading was paused. """ if self._recv_paused: self._recv_paused = False while self._recv_buf and not self._recv_paused: self._deliver_data(*self._recv_buf.pop(0)) class SSHClientChannel(SSHChannel): """SSH client channel""" _read_datatypes = {EXTENDED_DATA_STDERR} def __init__(self, conn, loop, encoding, window, max_pktsize): super().__init__(conn, loop, encoding, window, max_pktsize) self._exit_status = None self._exit_signal = None @asyncio.coroutine def create(self, session_factory, command, subsystem, env, term_type, term_size, term_modes): """Create an SSH client session""" packet = yield from self._open(b'session') # Client sessions should have no extra data in the open confirmation packet.check_end() self._session = session_factory() self._session.connection_made(self) for name, value in env.items(): name = str(name).encode('utf-8') value = str(value).encode('utf-8') self._send_request(b'env', String(name), String(value)) if term_type: term_type = term_type.encode('ascii') if len(term_size) == 4: width, height, pixwidth, pixheight = term_size elif len(term_size) == 2: width, height = term_size pixwidth = pixheight = 0 elif not term_size: width = height = pixwidth = pixheight = 0 else: raise ValueError('If set, terminal size must be a tuple of ' '2 or 4 integers') modes = b'' for mode, value in term_modes.items(): if mode <= PTY_OP_END or mode >= PTY_OP_RESERVED: raise ValueError('Invalid pty mode: %s' % mode) modes += Byte(mode) + UInt32(value) modes += Byte(PTY_OP_END) if not (yield from self._make_request(b'pty-req', String(term_type), UInt32(width), UInt32(height), UInt32(pixwidth), UInt32(pixheight), String(modes))): self.close() raise ChannelOpenError(OPEN_REQUEST_PTY_FAILED, 'PTY request failed') if command: result = yield from self._make_request(b'exec', String(command)) elif subsystem: result = yield from self._make_request(b'subsystem', String(subsystem)) else: result = yield from self._make_request(b'shell') if not result: self.close() raise ChannelOpenError(OPEN_REQUEST_SESSION_FAILED, 'Session request failed') if not self._session: raise ChannelOpenError(OPEN_REQUEST_SESSION_FAILED, 'Channel closed during session startup') self._session.session_started() self.resume_reading() return self, self._session def _process_xon_xoff_request(self, packet): """Process a request to set up XON/XOFF processing""" client_can_do = packet.get_boolean() packet.check_end() self._session.xon_xoff_requested(client_can_do) return True def _process_exit_status_request(self, packet): """Process a request to deliver exit status""" status = packet.get_uint32() & 0xff packet.check_end() self._exit_status = status self._session.exit_status_received(status) return True def _process_exit_signal_request(self, packet): """Process a request to deliver an exit signal""" signal = packet.get_string() core_dumped = packet.get_boolean() msg = packet.get_string() lang = packet.get_string() packet.check_end() try: signal = signal.decode('ascii') msg = msg.decode('utf-8') lang = lang.decode('ascii') except UnicodeDecodeError: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid exit signal request') from None self._exit_signal = (signal, core_dumped, msg, lang) self._session.exit_signal_received(signal, core_dumped, msg, lang) return True def get_exit_status(self): """Return the session's exit status This method returns the exit status of the session if one has been sent. If an exit signal was received, this method returns -1 and the exit signal information can be collected by calling :meth:`get_exit_signal`. If neither has been sent, this method returns ``None``. """ if self._exit_status is not None: return self._exit_status elif self._exit_signal: return -1 else: return None def get_exit_signal(self): """Return the session's exit signal, if one was sent This method returns information about the exit signal sent on this session. If an exit signal was sent, a tuple is returned containing the signal name, a boolean for whether a core dump occurred, a message associated with the signal, and the language the message was in. If no exit signal was sent, ``None`` is returned. """ return self._exit_signal def change_terminal_size(self, width, height, pixwidth=0, pixheight=0): """Change the terminal window size for this session This method changes the width and height of the terminal associated with this session. :param integer width: The width of the terminal in characters :param integer height: The height of the terminal in characters :param integer pixwidth: (optional) The width of the terminal in pixels :param integer pixheight: (optional) The height of the terminal in pixels """ self._send_request(b'window-change', UInt32(width), UInt32(height), UInt32(pixwidth), UInt32(pixheight)) def send_break(self, msec): """Send a break to the remote process This method requests that the server perform a break operation on the remote process or service as described in :rfc:`4335`. :param integer msec: The duration of the break in milliseconds :raises: :exc:`OSError` if the channel is not open """ self._send_request(b'break', UInt32(msec)) def send_signal(self, signal): """Send a signal to the remote process This method can be called to deliver a signal to the remote process or service. Signal names should be as described in section 6.10 of :rfc:`4254#section-6.10`. :param string signal: The signal to deliver :raises: :exc:`OSError` if the channel is not open """ signal = signal.encode('ascii') self._send_request(b'signal', String(signal)) def terminate(self): """Terminate the remote process This method can be called to terminate the remote process or service by sending it a ``TERM`` signal. :raises: :exc:`OSError` if the channel is not open """ self.send_signal('TERM') def kill(self): """Forcibly kill the remote process This method can be called to forcibly stop the remote process or service by sending it a ``KILL`` signal. :raises: :exc:`OSError` if the channel is not open """ self.send_signal('KILL') class SSHServerChannel(SSHChannel): """SSH server channel""" _write_datatypes = {EXTENDED_DATA_STDERR} def __init__(self, conn, loop, encoding, window, max_pktsize): """Initialize an SSH server channel""" super().__init__(conn, loop, encoding, window, max_pktsize) self._env = self._conn.get_key_option('environment', {}) self._command = None self._subsystem = None self._term_type = None self._term_size = (0, 0, 0, 0) self._term_modes = {} def _process_pty_req_request(self, packet): """Process a request to open a pseudo-terminal""" term_type = packet.get_string() width = packet.get_uint32() height = packet.get_uint32() pixwidth = packet.get_uint32() pixheight = packet.get_uint32() modes = packet.get_string() packet.check_end() try: self._term_type = term_type.decode('ascii') except UnicodeDecodeError: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid pty request') from None if not self._conn.check_key_permission('pty') or \ not self._conn.check_certificate_permission('pty'): return False self._term_size = (width, height, pixwidth, pixheight) idx = 0 while idx < len(modes): mode = modes[idx] idx += 1 if mode == PTY_OP_END or mode >= PTY_OP_RESERVED: break if idx+4 <= len(modes): self._term_modes[mode] = int.from_bytes(modes[idx:idx+4], 'big') idx += 4 else: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid pty modes string') return self._session.pty_requested(self._term_type, self._term_size, self._term_modes) def _process_env_request(self, packet): """Process a request to set an environment variable""" name = packet.get_string() value = packet.get_string() packet.check_end() try: name = name.decode('utf-8') value = value.decode('utf-8') except UnicodeDecodeError: return False self._env[name] = value return True def _start_session(self, command=None, subsystem=None): """Tell the session what type of channel is being requested""" forced_command = self._conn.get_certificate_option('force-command') if forced_command is None: forced_command = self._conn.get_key_option('command') if forced_command is not None: command = forced_command if command is not None: self._command = command result = self._session.exec_requested(command) elif subsystem is not None: self._subsystem = subsystem result = self._session.subsystem_requested(subsystem) else: result = self._session.shell_requested() return result def _process_shell_request(self, packet): """Process a request to open a shell""" packet.check_end() return self._start_session() def _process_exec_request(self, packet): """Process a request to execute a command""" command = packet.get_string() packet.check_end() try: command = command.decode('utf-8') except UnicodeDecodeError: return False return self._start_session(command=command) def _process_subsystem_request(self, packet): """Process a request to open a subsystem""" subsystem = packet.get_string() packet.check_end() try: subsystem = subsystem.decode('ascii') except UnicodeDecodeError: return False return self._start_session(subsystem=subsystem) def _process_window_change_request(self, packet): """Process a request to change the window size""" width = packet.get_uint32() height = packet.get_uint32() pixwidth = packet.get_uint32() pixheight = packet.get_uint32() packet.check_end() self._term_size = (width, height, pixwidth, pixheight) self._session.terminal_size_changed(width, height, pixwidth, pixheight) return True def _process_signal_request(self, packet): """Process a request to send a signal""" signal = packet.get_string() packet.check_end() try: signal = signal.decode('ascii') except UnicodeDecodeError: return False self._session.signal_received(signal) return True def _process_break_request(self, packet): """Process a request to send a break""" msec = packet.get_uint32() packet.check_end() return self._session.break_received(msec) def start_sftp_server(self, sftp_factory): """Start an SFTP server for this session This method can be used by an existing :class:`SSHServerSession` to replace itself with an SFTP server session. Calls to this method should be made from :meth:`session_started ` before any data is read or written. Once called, no further calls will be made on the original session. .. note:: The :meth:`connection_lost ` method will not be called on the original server session when this is used. :param callable sftp_server: A callable which returns an :class:`SFTPServer` object that will be created to handle SFTP requests on this channel. """ # Reset the encoding to allow the transfer of binary data self._encoding = None # Replace the session with an SFTPServerSession self._session = SFTPServerSession(sftp_factory(self._conn)) self._session.connection_made(self) self._session.session_started() def get_environment(self): """Return the environment for this session This method returns the environment set by the client when the session was opened. Calls to this method should only be made after :meth:`session_started ` has been called on the :class:`SSHServerSession`. :returns: A dictionary containing the environment variables set by the client """ return self._env def get_command(self): """Return the command the client requested to execute, if any This method returns the command the client requested to execute when the session was opened, if any. If the client did not request that a command be executed, this method will return ``None``. Calls to this method should only be made after :meth:`session_started ` has been called on the :class:`SSHServerSession`. When using the stream-based API, calls to this can be made at any time after the handler function has started up. """ return self._command def get_subsystem(self): """Return the subsystem the client requested to open, if any This method returns the subsystem the client requested to open when the session was opened, if any. If the client did not request that a subsystem be opened, this method will return ``None``. Calls to this method should only be made after :meth:`session_started ` has been called on the :class:`SSHServerSession`. When using the stream-based API, calls to this can be made at any time after the handler function has started up. """ return self._subsystem def get_terminal_type(self): """Return the terminal type for this session This method returns the terminal type set by the client when the session was opened. If the client didn't request a pseudo-terminal, this method will return ``None``. Calls to this method should only be made after :meth:`session_started ` has been called on the :class:`SSHServerSession`. When using the stream-based API, calls to this can be made at any time after the handler function has started up. :returns: A string containing the terminal type or ``None`` if no pseudo-terminal was requested """ return self._term_type def get_terminal_size(self): """Return terminal size information for this session This method returns the latest terminal size information set by the client. If the client didn't set any terminal size information, all values returned will be zero. Calls to this method should only be made after :meth:`session_started ` has been called on the :class:`SSHServerSession`. When using the stream-based API, calls to this can be made at any time after the handler function has started up. Also see :meth:`terminal_size_changed() ` or the :exc:`TerminalSizeChanged` exception for how to get notified when the terminal size changes. :returns: A tuple of four integers containing the width and height of the terminal in characters and the width and height of the terminal in pixels """ return self._term_size def get_terminal_mode(self, mode): """Return the requested TTY mode for this session This method looks up the value of a POSIX terminal mode set by the client when the session was opened. If the client didn't request a pseudo-terminal or didn't set the requested TTY mode opcode, this method will return ``None``. Calls to this method should only be made after :meth:`session_started ` has been called on the :class:`SSHServerSession`. When using the stream-based API, calls to this can be made at any time after the handler function has started up. :param integer mode: POSIX terminal mode taken from :ref:`POSIX terminal modes ` to look up :returns: An integer containing the value of the requested POSIX terminal mode or ``None`` if the requested mode was not set """ return self._term_modes.get(mode) def set_xon_xoff(self, client_can_do): """Set whether the client should enable XON/XOFF flow control This method can be called to tell the client whether or not to enable XON/XOFF flow control, indicating that it should intercept Control-S and Control-Q coming from its local terminal to pause and resume output, respectively. Applications should set client_can_do to ``True`` to enable this functionality or to ``False`` to tell the client to forward Control-S and Control-Q through as normal input. :param boolean client_can_do: Whether or not the client should enable XON/XOFF flow control """ self._send_request(b'xon-xoff', Boolean(client_can_do)) def write_stderr(self, data): """Write output to stderr This method can be called to send output to the client which is intended to be displayed on stderr. If an encoding was specified when the channel was created, the data should be provided as a string and will be converted using that encoding. Otherwise, the data should be provided as bytes. :param data: The data to send to stderr :type data: string or bytes :raises: :exc:`OSError` if the channel isn't open for sending """ self.write(data, EXTENDED_DATA_STDERR) def writelines_stderr(self, list_of_data): """Write a list of data bytes to stderr This method can be called to write a list (or any iterable) of data bytes to the channel. It is functionality equivalent to calling :meth:`write_stderr` on each element in the list. """ self.writelines(list_of_data, EXTENDED_DATA_STDERR) def exit(self, status): """Send exit status and close the channel This method can be called to report an exit status for the process back to the client and close the channel. A zero exit status is generally returned when the operation was successful. After reporting the status, the channel is closed. :param integer status: The exit status to report to the client :raises: :exc:`OSError` if the channel isn't open """ if self._send_state not in {'open', 'eof_pending', 'eof_sent'}: raise OSError('Channel not open') self._send_request(b'exit-status', UInt32(status & 0xff)) self.close() def exit_with_signal(self, signal, core_dumped=False, msg='', lang=DEFAULT_LANG): """Send exit signal and close the channel This method can be called to report that the process terminated abnormslly with a signal. A more detailed error message may also provided, along with an indication of whether or not the process dumped core. After reporting the signal, the channel is closed. :param string signal: The signal which caused the process to exit :param boolean core_dumped: (optional) Whether or not the process dumped core :param msg: (optional) Details about what error occurred :param lang: (optional) The language the error message is in :raises: :exc:`OSError` if the channel isn't open """ if self._send_state not in {'open', 'eof_pending', 'eof_sent'}: raise OSError('Channel not open') signal = signal.encode('ascii') msg = msg.encode('utf-8') lang = lang.encode('ascii') self._send_request(b'exit-signal', String(signal), Boolean(core_dumped), String(msg), String(lang)) self.close() class SSHTCPChannel(SSHChannel): """SSH TCP channel""" @asyncio.coroutine def _finish_open_request(self, session): """Finish processing a TCP channel open request""" yield from super()._finish_open_request(session) if self._session: self._session.session_started() self.resume_reading() @asyncio.coroutine def _open_tcp(self, session_factory, chantype, host, port, orig_host, orig_port): """Open a TCP channel""" self._extra['local_peername'] = (orig_host, orig_port) self._extra['remote_peername'] = (host, port) host = host.encode('utf-8') orig_host = orig_host.encode('utf-8') packet = yield from super()._open(chantype, String(host), UInt32(port), String(orig_host), UInt32(orig_port)) # TCP sessions should have no extra data in the open confirmation packet.check_end() self._session = session_factory() self._session.connection_made(self) self._session.session_started() self.resume_reading() return self, self._session @asyncio.coroutine def connect(self, session_factory, host, port, orig_host, orig_port): """Create a new outbound TCP session""" return (yield from self._open_tcp(session_factory, b'direct-tcpip', host, port, orig_host, orig_port)) @asyncio.coroutine def accept(self, session_factory, host, port, orig_host, orig_port): """Create a new forwarded TCP session""" return (yield from self._open_tcp(session_factory, b'forwarded-tcpip', host, port, orig_host, orig_port)) def set_inbound_peer_names(self, dest_host, dest_port, orig_host, orig_port): """Set local and remote peer names for inbound connections""" self._extra['local_peername'] = (dest_host, dest_port) self._extra['remote_peername'] = (orig_host, orig_port) asyncssh-1.3.0/asyncssh/cipher.py000066400000000000000000000054621260630620200170310ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Symmetric key encryption handlers""" from .crypto import lookup_cipher _enc_algs = [] _enc_params = {} _enc_ciphers = {} def register_encryption_alg(alg, cipher_name, mode_name, key_size, initial_bytes): """Register an encryption algorithm""" cipher = lookup_cipher(cipher_name, mode_name) if cipher: # pragma: no branch _enc_algs.append(alg) _enc_params[alg] = (key_size, cipher.iv_size, cipher.block_size, cipher.mode_name) _enc_ciphers[alg] = (cipher, initial_bytes) def get_encryption_algs(): """Return a list of available encryption algorithms""" return _enc_algs def get_encryption_params(alg): """Get parameters of an encryption algorithm This function returns the key, iv, and block sizes of an encryption algorithm. """ return _enc_params[alg] def get_cipher(alg, key, iv=None): """Return an instance of a cipher This function returns a cipher object initialized with the specified key and iv that can be used for data encryption and decryption. """ cipher, initial_bytes = _enc_ciphers[alg] return cipher.new(key, iv, initial_bytes) # pylint: disable=bad-whitespace register_encryption_alg(b'chacha20-poly1305@openssh.com', 'chacha20-poly1305', 'chacha', 64, 0) register_encryption_alg(b'aes256-ctr', 'aes', 'ctr', 32, 0) register_encryption_alg(b'aes192-ctr', 'aes', 'ctr', 24, 0) register_encryption_alg(b'aes128-ctr', 'aes', 'ctr', 16, 0) register_encryption_alg(b'aes256-gcm@openssh.com', 'aes', 'gcm', 32, 0) register_encryption_alg(b'aes128-gcm@openssh.com', 'aes', 'gcm', 16, 0) register_encryption_alg(b'aes256-cbc', 'aes', 'cbc', 32, 0) register_encryption_alg(b'aes192-cbc', 'aes', 'cbc', 24, 0) register_encryption_alg(b'aes128-cbc', 'aes', 'cbc', 16, 0) register_encryption_alg(b'3des-cbc', 'des3', 'cbc', 24, 0) register_encryption_alg(b'blowfish-cbc', 'blowfish', 'cbc', 16, 0) register_encryption_alg(b'cast128-cbc', 'cast', 'cbc', 16, 0) register_encryption_alg(b'arcfour256', 'arc4', None, 32, 1536) register_encryption_alg(b'arcfour128', 'arc4', None, 16, 1536) register_encryption_alg(b'arcfour', 'arc4', None, 16, 0) asyncssh-1.3.0/asyncssh/compression.py000066400000000000000000000044121260630620200201120ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """SSH compression handlers""" import zlib _cmp_algs = [] _cmp_params = {} _cmp_compressors = {} _cmp_decompressors = {} def _none(): """Compressor/decompressor for no compression.""" return None class _ZLibCompress: """Wrapper class to force a sync flush when compressing""" def __init__(self): self._comp = zlib.compressobj() def compress(self, data): """Compress data using zlib compression with sync flush""" return self._comp.compress(data) + self._comp.flush(zlib.Z_SYNC_FLUSH) def register_compression_alg(alg, compressor, decompressor, after_auth): """Register a compression algorithm""" _cmp_algs.append(alg) _cmp_params[alg] = after_auth _cmp_compressors[alg] = compressor _cmp_decompressors[alg] = decompressor def get_compression_algs(): """Return a list of available compression algorithms""" return _cmp_algs def get_compression_params(alg): """Get parameters of a compression algorithm This function returns whether or not a compression algorithm should be delayed until after authentication completes. """ return _cmp_params[alg] def get_compressor(alg): """Return an instance of a compressor This function returns an object that can be used for data compression. """ return _cmp_compressors[alg]() def get_decompressor(alg): """Return an instance of a decompressor This function returns an object that can be used for data decompression. """ return _cmp_decompressors[alg]() # pylint: disable=bad-whitespace register_compression_alg(b'zlib@openssh.com', _ZLibCompress, zlib.decompressobj, True) register_compression_alg(b'zlib', _ZLibCompress, zlib.decompressobj, False) register_compression_alg(b'none', _none, _none, False) asyncssh-1.3.0/asyncssh/connection.py000066400000000000000000005037421260630620200177220ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """SSH connection handlers""" import asyncio import getpass import os import socket import time from collections import OrderedDict from .auth import lookup_client_auth from .auth import get_server_auth_methods, lookup_server_auth from .auth_keys import read_authorized_keys from .channel import SSHClientChannel, SSHServerChannel, SSHTCPChannel from .cipher import get_encryption_algs, get_encryption_params, get_cipher from .compression import get_compression_algs, get_compression_params from .compression import get_compressor, get_decompressor from .constants import DEFAULT_LANG from .constants import DISC_BY_APPLICATION, DISC_CONNECTION_LOST from .constants import DISC_KEY_EXCHANGE_FAILED, DISC_HOST_KEY_NOT_VERIFYABLE from .constants import DISC_MAC_ERROR, DISC_NO_MORE_AUTH_METHODS_AVAILABLE from .constants import DISC_PROTOCOL_ERROR, DISC_SERVICE_NOT_AVAILABLE from .constants import EXTENDED_DATA_STDERR from .constants import MSG_DISCONNECT, MSG_IGNORE, MSG_UNIMPLEMENTED from .constants import MSG_DEBUG, MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT from .constants import MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_CONFIRMATION from .constants import MSG_CHANNEL_OPEN_FAILURE, MSG_CHANNEL_WINDOW_ADJUST from .constants import MSG_CHANNEL_DATA, MSG_CHANNEL_EXTENDED_DATA from .constants import MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MSG_CHANNEL_REQUEST from .constants import MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE from .constants import MSG_KEXINIT, MSG_NEWKEYS, MSG_KEX_FIRST, MSG_KEX_LAST from .constants import MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE from .constants import MSG_USERAUTH_SUCCESS, MSG_USERAUTH_BANNER from .constants import MSG_USERAUTH_FIRST, MSG_USERAUTH_LAST from .constants import MSG_GLOBAL_REQUEST, MSG_REQUEST_SUCCESS from .constants import MSG_REQUEST_FAILURE from .constants import OPEN_ADMINISTRATIVELY_PROHIBITED, OPEN_CONNECT_FAILED from .constants import OPEN_UNKNOWN_CHANNEL_TYPE from .forward import SSHPortForwarder, SSHLocalPortForwarder from .forward import SSHRemotePortForwarder from .kex import get_kex_algs, get_kex from .known_hosts import match_known_hosts from .listener import SSHClientListener, SSHForwardListener from .mac import get_mac_algs, get_mac_params, get_mac from .misc import ChannelOpenError, DisconnectError, ip_address from .packet import Boolean, Byte, NameList, String, UInt32, UInt64 from .packet import PacketDecodeError, SSHPacket, SSHPacketHandler from .public_key import CERT_TYPE_HOST, CERT_TYPE_USER from .public_key import get_public_key_algs, get_certificate_algs from .public_key import decode_ssh_public_key, decode_ssh_certificate from .public_key import import_private_key, import_public_key from .public_key import read_private_key, read_public_key from .public_key import read_private_key_list, read_public_key_list from .public_key import import_certificate, read_certificate from .public_key import KeyImportError from .saslprep import saslprep, SASLPrepError from .sftp import SFTPClient, SFTPServer, SFTPClientSession from .stream import SSHClientStreamSession, SSHServerStreamSession from .stream import SSHTCPStreamSession, SSHReader, SSHWriter # SSH default port _DEFAULT_PORT = 22 # SSH service names _USERAUTH_SERVICE = b'ssh-userauth' _CONNECTION_SERVICE = b'ssh-connection' # Default file names in .ssh directory to read private keys from _DEFAULT_KEY_FILES = ('id_ed25519', 'id_ecdsa', 'id_rsa', 'id_dsa') # Default rekey parameters _DEFAULT_REKEY_BYTES = 1 << 30 # 1 GiB _DEFAULT_REKEY_SECONDS = 3600 # 1 hour # Default channel parameters _DEFAULT_WINDOW = 2*1024*1024 # 2 MiB _DEFAULT_MAX_PKTSIZE = 32768 # 32 kiB def _load_private_key(key): """Load a private key This function loads a private key and an optional certificate. The key argument can be either a key reference or a tuple with a reference to a key and a reference to a matching certificate. Key references can either be the name of a file to load the key from, a byte string to import as a private key, or an already loaded :class:`SSHKey` private key. Certificate references can be the name of a file to load the certificate from, a byte string to import as a certificate, an already loaded :class:`SSHCertificate`, or ``None`` if no certificate should be associated with the key. When a filename is provided in as a reference outside of a tuple, an attempt is made to load a private key from that file and a certificate from a file constructed by appending '-cert.pub' to the end of the filename. This function returns a tuple of a :class:`SSHKey` private key and either an :class:`SSHCertificate` or ``None`` depending on whether an associated certificate was loaded. """ if isinstance(key, str): cert = key + '-cert.pub' ignore_missing_cert = True elif isinstance(key, tuple): key, cert = key ignore_missing_cert = False else: cert = None if isinstance(key, str): key = read_private_key(key) elif isinstance(key, bytes): key = import_private_key(key) if isinstance(cert, str): try: cert = read_certificate(cert) except OSError: if ignore_missing_cert: cert = None else: raise elif isinstance(cert, bytes): cert = import_certificate(cert) if cert and key.get_ssh_public_key() != cert.key.get_ssh_public_key(): raise ValueError('Certificate key mismatch') return key, cert def _load_public_key(key): """Load a public key This function loads a public key. The key argument can be the name of a file to load the key from, a byte string to import the key from, or an already loaded :class:`SSHKey` public key. """ if isinstance(key, str): key = read_public_key(key) elif isinstance(key, bytes): key = import_public_key(key) return key def _load_private_key_list(keylist): """Load list of private keys and optional associated certificates This function loads a collection of private keys, each with an optional certificate. The keylist argument can be either a filename to load private keys from (without any certificates) or a list of values representing keys and certificates as described in ::func::`_load_private_key`. This function returns a list of tuples of a :class:`SSHKey` private key and either an :class:`SSHCertificate` or ``None`` depending on whether an associated certificate was loaded. """ if isinstance(keylist, str): keys = read_private_key_list(keylist) return [(key, None) for key in keys] else: return [_load_private_key(key) for key in keylist] def _load_public_key_list(keylist): """Load public key list This function loads a collection of public keys. The keylist argument can be either a filename to load keys from or a list of values representing keys as described in :func:`_load_public_key`. It returns a list of loaded :class:`SSHKey` public keys. """ if isinstance(keylist, str): return read_public_key_list(keylist) else: return [_load_public_key(key) for key in keylist] def _load_authorized_keys(authorized_keys): """Load authorized keys list This function loads authorized client keys. The authorized_keys argument can be either a filename to load keys from or an already imported :class:`SSHAuthorizedKeys` object containing the authorized keys and their associated options. """ if isinstance(authorized_keys, str): return read_authorized_keys(authorized_keys) else: return authorized_keys def _select_algs(alg_type, algs, possible_algs, none_value=None): """Select a set of allowed algorithms""" if algs == (): return possible_algs elif algs: result = [] for alg_str in algs: alg = alg_str.encode('ascii') if alg not in possible_algs: raise ValueError('%s is not a valid %s algorithm' % (alg_str, alg_type)) result.append(alg) return result elif none_value: return [none_value] else: raise ValueError('No %s algorithms selected' % alg_type) class SSHConnection(SSHPacketHandler): """Parent class for SSH connections""" def __init__(self, protocol_factory, loop, kex_algs, encryption_algs, mac_algs, compression_algs, rekey_bytes, rekey_seconds, server): self._protocol_factory = protocol_factory self._loop = loop self._transport = None self._peer_addr = None self._owner = None self._extra = {} self._server = server self._inpbuf = b'' self._packet = b'' self._pktlen = 0 self._client_version = b'' self._server_version = b'' self._client_kexinit = b'' self._server_kexinit = b'' self._session_id = None self._send_seq = 0 self._send_cipher = None self._send_blocksize = 8 self._send_mac = None self._send_mode = None self._compressor = None self._compress_after_auth = False self._deferred_packets = [] self._recv_handler = self._recv_version self._recv_seq = 0 self._recv_cipher = None self._recv_blocksize = 8 self._recv_mac = None self._recv_macsize = 0 self._recv_mode = None self._decompressor = None self._decompress_after_auth = None self._next_recv_cipher = None self._next_recv_blocksize = 0 self._next_recv_mac = None self._next_recv_macsize = 0 self._next_recv_mode = None self._next_decompressor = None self._next_decompress_after_auth = None self._kex = None self._kexinit_sent = False self._kex_complete = False self._ignore_first_kex = False self._rekey_bytes = rekey_bytes self._rekey_bytes_sent = 0 self._rekey_seconds = rekey_seconds self._rekey_time = time.time() + rekey_seconds self._enc_alg_cs = None self._enc_alg_sc = None self._mac_alg_cs = None self._mac_alg_sc = None self._cmp_alg_cs = None self._cmp_alg_sc = None self._next_service = None self._auth = None self._auth_in_progress = False self._auth_complete = False self._auth_methods = [b'none'] self._auth_waiter = None self._username = None self._channels = {} self._next_recv_chan = 0 self._global_request_queue = [] self._global_request_waiters = [] self._local_listeners = {} self._disconnected = False self._kex_algs = _select_algs('key exchange', kex_algs, get_kex_algs()) self._enc_algs = _select_algs('encryption', encryption_algs, get_encryption_algs()) self._mac_algs = _select_algs('MAC', mac_algs, get_mac_algs()) self._cmp_algs = _select_algs('compression', compression_algs, get_compression_algs(), b'none') self._server_host_key_algs = [] def __enter__(self): """Allow SSHConnection to be used as a context manager""" return self def __exit__(self, *exc_info): """Automatically close the connection when used as a context manager""" try: self.close() except RuntimeError as exc: # There's a race in some cases between the close call here # and the code which shuts down the event loop. Since the # loop.is_closed() method is only in Python 3.4.2 and later, # catch and ignore the RuntimeError for now if this happens. if exc.args[0] == 'Event loop is closed': pass else: raise def _cleanup(self, exc): """Clean up this connection""" if self._auth: self._auth.cancel() self._auth = None if self._channels: for chan in list(self._channels.values()): chan.process_connection_close(exc) self._channels = {} if self._local_listeners: for listener in self._local_listeners.values(): listener.close() self._local_listeners = {} if self._owner: self._owner.connection_lost(exc) self._owner = None self._inpbuf = b'' self._recv_handler = None def _force_close(self, exc): """Force this connection to close immediately""" if not self._transport: return self._transport.abort() self._transport = None self._loop.call_soon(self._cleanup, exc) def is_client(self): """Return if this is a client connection""" return not self._server def is_server(self): """Return if this is a server connection""" return self._server def get_owner(self): """Return the SSHClient or SSHServer which owns this connection""" return self._owner def get_hash_prefix(self): """Return the bytes used in calculating unique connection hashes This methods returns a packetized version of the client and server version and kexinit strings which is needed to perform key exchange hashes. """ return b''.join((String(self._client_version), String(self._server_version), String(self._client_kexinit), String(self._server_kexinit))) def connection_made(self, transport): """Handle a newly opened connection""" self._transport = transport peername = transport.get_extra_info('peername') self._peer_addr = peername[0] if peername else None self._owner = self._protocol_factory() self._protocol_factory = None try: self._connection_made() self._owner.connection_made(self) self._send_version() except DisconnectError as exc: self._loop.call_soon(self.connection_lost, exc) def connection_lost(self, exc=None): """Handle the closing of a connection""" if exc is None and self._transport: exc = DisconnectError(DISC_CONNECTION_LOST, 'Connection lost') self._force_close(exc) def data_received(self, data): """Handle incoming data on the connection""" if data: self._inpbuf += data try: while self._inpbuf and self._recv_handler(): pass except DisconnectError as exc: self._force_close(exc) def eof_received(self): """Handle an incoming end of file on the connection""" self.connection_lost(None) def pause_writing(self): """Handle a request from the transport to pause writing data""" # Do nothing with this for now pass def resume_writing(self): """Handle a request from the transport to resume writing data""" # Do nothing with this for now pass def add_channel(self, chan): """Add a new channel, returning its channel number""" while self._next_recv_chan in self._channels: self._next_recv_chan = (self._next_recv_chan + 1) & 0xffffffff recv_chan = self._next_recv_chan self._next_recv_chan = (self._next_recv_chan + 1) & 0xffffffff self._channels[recv_chan] = chan return recv_chan def remove_channel(self, recv_chan): """Remove the channel with the specified channel number""" del self._channels[recv_chan] def _choose_alg(self, alg_type, local_algs, remote_algs): """Choose a common algorithm from the client & server lists This method returns the earliest algorithm on the client's list which is supported by the server. """ if self.is_client(): client_algs, server_algs = local_algs, remote_algs else: client_algs, server_algs = remote_algs, local_algs for alg in client_algs: if alg in server_algs: return alg raise DisconnectError(DISC_KEY_EXCHANGE_FAILED, 'No matching %s algorithm found' % alg_type) def _send(self, data): """Send data to the SSH connection""" if self._transport: self._transport.write(data) def _send_version(self): """Start the SSH handshake""" from .version import __version__ version = b'SSH-2.0-AsyncSSH_' + __version__.encode('ascii') if self.is_client(): self._client_version = version self._extra.update(client_version=version.decode('ascii')) else: self._server_version = version self._extra.update(server_version=version.decode('ascii')) self._send(version + b'\r\n') def _recv_version(self): """Receive and parse the remote SSH version""" idx = self._inpbuf.find(b'\n') if idx < 0: return False version = self._inpbuf[:idx] if version.endswith(b'\r'): version = version[:-1] self._inpbuf = self._inpbuf[idx+1:] if (version.startswith(b'SSH-2.0-') or (self.is_client() and version.startswith(b'SSH-1.99-'))): # Accept version 2.0, or 1.99 if we're a client if self.is_server(): self._client_version = version self._extra.update(client_version=version.decode('ascii')) else: self._server_version = version self._extra.update(server_version=version.decode('ascii')) self._send_kexinit() self._kexinit_sent = True self._recv_handler = self._recv_pkthdr elif self.is_client() and not version.startswith(b'SSH-'): # As a client, ignore the line if it doesn't appear to be a version pass else: # Otherwise, reject the unknown version self._force_close(DisconnectError(DISC_PROTOCOL_ERROR, 'Unknown SSH version')) return False return True def _recv_pkthdr(self): """Receive and parse an SSH packet header""" if len(self._inpbuf) < self._recv_blocksize: return False self._packet = self._inpbuf[:self._recv_blocksize] self._inpbuf = self._inpbuf[self._recv_blocksize:] pktlen = self._packet[:4] if self._recv_cipher: if self._recv_mode == 'chacha': nonce = UInt64(self._recv_seq) pktlen = self._recv_cipher.crypt_len(pktlen, nonce) elif self._recv_mode not in ('gcm', 'etm'): self._packet = self._recv_cipher.decrypt(self._packet) pktlen = self._packet[:4] self._pktlen = int.from_bytes(pktlen, 'big') self._recv_handler = self._recv_packet return True def _recv_packet(self): """Receive the remainder of an SSH packet and process it""" rem = 4 + self._pktlen + self._recv_macsize - self._recv_blocksize if len(self._inpbuf) < rem: return False rest = self._inpbuf[:rem-self._recv_macsize] if self._recv_mode in ('chacha', 'gcm'): self._packet += rest mac = self._inpbuf[rem-self._recv_macsize:rem] hdr = self._packet[:4] self._packet = self._packet[4:] if self._recv_mode == 'chacha': nonce = UInt64(self._recv_seq) self._packet = \ self._recv_cipher.verify_and_decrypt(hdr, self._packet, nonce, mac) else: self._packet = \ self._recv_cipher.verify_and_decrypt(hdr, self._packet, mac) if not self._packet: raise DisconnectError(DISC_MAC_ERROR, 'MAC verification failed') payload = self._packet[1:-self._packet[0]] elif self._recv_mode == 'etm': self._packet += rest mac = self._inpbuf[rem-self._recv_macsize:rem] if self._recv_mac: if not self._recv_mac.verify(UInt32(self._recv_seq) + self._packet, mac): raise DisconnectError(DISC_MAC_ERROR, 'MAC verification failed') self._packet = self._packet[4:] if self._recv_cipher: self._packet = self._recv_cipher.decrypt(self._packet) payload = self._packet[1:-self._packet[0]] else: if self._recv_cipher: rest = self._recv_cipher.decrypt(rest) self._packet += rest mac = self._inpbuf[rem-self._recv_macsize:rem] if self._recv_mac: if not self._recv_mac.verify(UInt32(self._recv_seq) + self._packet, mac): raise DisconnectError(DISC_MAC_ERROR, 'MAC verification failed') payload = self._packet[5:-self._packet[4]] self._inpbuf = self._inpbuf[rem:] if self._decompressor and (self._auth_complete or not self._decompress_after_auth): payload = self._decompressor.decompress(payload) try: packet = SSHPacket(payload) pkttype = packet.get_byte() if self._kex and MSG_KEX_FIRST <= pkttype <= MSG_KEX_LAST: if self._ignore_first_kex: self._ignore_first_kex = False processed = True else: processed = self._kex.process_packet(pkttype, packet) elif (self._auth and MSG_USERAUTH_FIRST <= pkttype <= MSG_USERAUTH_LAST): processed = self._auth.process_packet(pkttype, packet) else: processed = self.process_packet(pkttype, packet) except PacketDecodeError as exc: raise DisconnectError(DISC_PROTOCOL_ERROR, str(exc)) if not processed: self.send_packet(Byte(MSG_UNIMPLEMENTED), UInt32(self._recv_seq)) if self._transport: self._recv_seq = (self._recv_seq + 1) & 0xffffffff self._recv_handler = self._recv_pkthdr return True def send_packet(self, *args): """Send an SSH packet""" payload = b''.join(args) pkttype = payload[0] if (self._auth_complete and self._kex_complete and (self._rekey_bytes_sent >= self._rekey_bytes or time.monotonic() >= self._rekey_time)): self._send_kexinit() self._kexinit_sent = True if (((pkttype in {MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT} or pkttype > MSG_KEX_LAST) and not self._kex_complete) or (pkttype == MSG_USERAUTH_BANNER and not self._auth_in_progress) or (pkttype > MSG_USERAUTH_LAST and not self._auth_complete)): self._deferred_packets.append(payload) return # If we're encrypting and we have no data outstanding, insert an # ignore packet into the stream if self._send_cipher and payload[0] != MSG_IGNORE: self.send_packet(Byte(MSG_IGNORE), String(b'')) if self._compressor and (self._auth_complete or not self._compress_after_auth): payload = self._compressor.compress(payload) hdrlen = 1 if self._send_mode in ('chacha', 'gcm', 'etm') else 5 padlen = -(hdrlen + len(payload)) % self._send_blocksize if padlen < 4: padlen += self._send_blocksize packet = Byte(padlen) + payload + os.urandom(padlen) pktlen = len(packet) hdr = UInt32(pktlen) if self._send_mode == 'chacha': nonce = UInt64(self._send_seq) hdr = self._send_cipher.crypt_len(hdr, nonce) packet, mac = self._send_cipher.encrypt_and_sign(hdr, packet, nonce) packet = hdr + packet elif self._send_mode == 'gcm': packet, mac = self._send_cipher.encrypt_and_sign(hdr, packet) packet = hdr + packet elif self._send_mode == 'etm': if self._send_cipher: packet = self._send_cipher.encrypt(packet) packet = hdr + packet if self._send_mac: mac = self._send_mac.sign(UInt32(self._send_seq) + packet) else: mac = b'' else: packet = hdr + packet if self._send_mac: mac = self._send_mac.sign(UInt32(self._send_seq) + packet) else: mac = b'' if self._send_cipher: packet = self._send_cipher.encrypt(packet) self._send(packet + mac) self._send_seq = (self._send_seq + 1) & 0xffffffff if self._kex_complete: self._rekey_bytes_sent += pktlen def _send_deferred_packets(self): """Send packets deferred due to key exchange or auth""" deferred_packets = self._deferred_packets self._deferred_packets = [] for packet in deferred_packets: self.send_packet(packet) def _send_kexinit(self): """Start a key exchange""" self._kex_complete = False self._rekey_bytes_sent = 0 self._rekey_time = time.monotonic() + self._rekey_seconds cookie = os.urandom(16) kex_algs = NameList(self._kex_algs) host_key_algs = NameList(self._server_host_key_algs) enc_algs = NameList(self._enc_algs) mac_algs = NameList(self._mac_algs) cmp_algs = NameList(self._cmp_algs) langs = NameList([]) packet = b''.join((Byte(MSG_KEXINIT), cookie, kex_algs, host_key_algs, enc_algs, enc_algs, mac_algs, mac_algs, cmp_algs, cmp_algs, langs, langs, Boolean(False), UInt32(0))) if self.is_server(): self._server_kexinit = packet else: self._client_kexinit = packet self.send_packet(packet) def send_newkeys(self, k, h): """Finish a key exchange and send a new keys message""" if not self._session_id: self._session_id = h enc_keysize_cs, enc_ivsize_cs, enc_blocksize_cs, mode_cs = \ get_encryption_params(self._enc_alg_cs) enc_keysize_sc, enc_ivsize_sc, enc_blocksize_sc, mode_sc = \ get_encryption_params(self._enc_alg_sc) if mode_cs in ('chacha', 'gcm'): mac_keysize_cs, mac_hashsize_cs = 0, 16 else: mac_keysize_cs, mac_hashsize_cs, etm_cs = \ get_mac_params(self._mac_alg_cs) if etm_cs: mode_cs = 'etm' if mode_sc in ('chacha', 'gcm'): mac_keysize_sc, mac_hashsize_sc = 0, 16 else: mac_keysize_sc, mac_hashsize_sc, etm_sc = \ get_mac_params(self._mac_alg_sc) if etm_sc: mode_sc = 'etm' cmp_after_auth_cs = get_compression_params(self._cmp_alg_cs) cmp_after_auth_sc = get_compression_params(self._cmp_alg_sc) iv_cs = self._kex.compute_key(k, h, b'A', self._session_id, enc_ivsize_cs) iv_sc = self._kex.compute_key(k, h, b'B', self._session_id, enc_ivsize_sc) enc_key_cs = self._kex.compute_key(k, h, b'C', self._session_id, enc_keysize_cs) enc_key_sc = self._kex.compute_key(k, h, b'D', self._session_id, enc_keysize_sc) mac_key_cs = self._kex.compute_key(k, h, b'E', self._session_id, mac_keysize_cs) mac_key_sc = self._kex.compute_key(k, h, b'F', self._session_id, mac_keysize_sc) self._kex = None next_cipher_cs = get_cipher(self._enc_alg_cs, enc_key_cs, iv_cs) next_cipher_sc = get_cipher(self._enc_alg_sc, enc_key_sc, iv_sc) if mode_cs in ('chacha', 'gcm'): self._mac_alg_cs = self._enc_alg_cs next_mac_cs = None else: next_mac_cs = get_mac(self._mac_alg_cs, mac_key_cs) if mode_sc in ('chacha', 'gcm'): self._mac_alg_sc = self._enc_alg_sc next_mac_sc = None else: next_mac_sc = get_mac(self._mac_alg_sc, mac_key_sc) self.send_packet(Byte(MSG_NEWKEYS)) if self.is_client(): self._send_cipher = next_cipher_cs self._send_blocksize = max(8, enc_blocksize_cs) self._send_mac = next_mac_cs self._send_mode = mode_cs self._compressor = get_compressor(self._cmp_alg_cs) self._compress_after_auth = cmp_after_auth_cs self._next_recv_cipher = next_cipher_sc self._next_recv_blocksize = max(8, enc_blocksize_sc) self._next_recv_mac = next_mac_sc self._next_recv_macsize = mac_hashsize_sc self._next_recv_mode = mode_sc self._next_decompressor = get_decompressor(self._cmp_alg_sc) self._next_decompress_after_auth = cmp_after_auth_sc self._extra.update( send_cipher=self._enc_alg_cs.decode('ascii'), send_mac=self._mac_alg_cs.decode('ascii'), send_compression=self._cmp_alg_cs.decode('ascii'), recv_cipher=self._enc_alg_sc.decode('ascii'), recv_mac=self._mac_alg_sc.decode('ascii'), recv_compression=self._cmp_alg_sc.decode('ascii')) else: self._send_cipher = next_cipher_sc self._send_blocksize = max(8, enc_blocksize_sc) self._send_mac = next_mac_sc self._send_mode = mode_sc self._compressor = get_compressor(self._cmp_alg_sc) self._compress_after_auth = cmp_after_auth_sc self._next_recv_cipher = next_cipher_cs self._next_recv_blocksize = max(8, enc_blocksize_cs) self._next_recv_mac = next_mac_cs self._next_recv_macsize = mac_hashsize_cs self._next_recv_mode = mode_cs self._next_decompressor = get_decompressor(self._cmp_alg_cs) self._next_decompress_after_auth = cmp_after_auth_cs self._extra.update( send_cipher=self._enc_alg_sc.decode('ascii'), send_mac=self._mac_alg_sc.decode('ascii'), send_compression=self._cmp_alg_sc.decode('ascii'), recv_cipher=self._enc_alg_cs.decode('ascii'), recv_mac=self._mac_alg_cs.decode('ascii'), recv_compression=self._cmp_alg_cs.decode('ascii')) self._next_service = _USERAUTH_SERVICE self._kex_complete = True self._send_deferred_packets() def send_service_request(self, service): """Send a service request""" self._next_service = service self.send_packet(Byte(MSG_SERVICE_REQUEST), String(service)) def send_userauth_request(self, method, *args, key=None): """Send a user authentication request""" packet = b''.join((Byte(MSG_USERAUTH_REQUEST), String(self._username), String(_CONNECTION_SERVICE), String(method)) + args) if key: packet += String(key.sign(String(self._session_id) + packet)) self.send_packet(packet) def send_userauth_failure(self, partial_success): """Send a user authentication failure response""" self._auth = None self.send_packet(Byte(MSG_USERAUTH_FAILURE), NameList(get_server_auth_methods(self)), Boolean(partial_success)) def send_userauth_success(self): """Send a user authentication success response""" self.send_packet(Byte(MSG_USERAUTH_SUCCESS)) self._auth = None self._auth_in_progress = False self._auth_complete = True self._extra.update(username=self._username) self._send_deferred_packets() def send_channel_open_confirmation(self, send_chan, recv_chan, recv_window, recv_pktsize, *result_args): """Send a channel open confirmation""" self.send_packet(Byte(MSG_CHANNEL_OPEN_CONFIRMATION), UInt32(send_chan), UInt32(recv_chan), UInt32(recv_window), UInt32(recv_pktsize), *result_args) def send_channel_open_failure(self, send_chan, code, reason, lang): """Send a channel open failure""" reason = reason.encode('utf-8') lang = lang.encode('ascii') self.send_packet(Byte(MSG_CHANNEL_OPEN_FAILURE), UInt32(send_chan), UInt32(code), String(reason), String(lang)) @asyncio.coroutine def _make_global_request(self, request, *args): """Send a global request and wait for the response""" waiter = asyncio.Future(loop=self._loop) self._global_request_waiters.append(waiter) self.send_packet(Byte(MSG_GLOBAL_REQUEST), String(request), Boolean(True), *args) return (yield from waiter) def _report_global_response(self, result): """Report back the response to a previously issued global request""" _, _, want_reply = self._global_request_queue.pop(0) if want_reply: if result: response = b'' if result is True else result self.send_packet(Byte(MSG_REQUEST_SUCCESS), response) else: self.send_packet(Byte(MSG_REQUEST_FAILURE)) if self._global_request_queue: self._service_next_global_request() def _service_next_global_request(self): """Process next item on global request queue""" handler, packet, _ = self._global_request_queue[0] if callable(handler): handler(packet) else: self._report_global_response(False) @asyncio.coroutine def _create_tcp_listener(self, listen_host, listen_port): """Create a listener for TCP/IP port forwarding""" if listen_host == '': listen_host = None addrinfo = yield from self._loop.getaddrinfo(listen_host, listen_port, family=socket.AF_UNSPEC, type=socket.SOCK_STREAM, flags=socket.AI_PASSIVE) if not addrinfo: raise OSError('getaddrinfo() returned empty list') sockets = [] for family, socktype, proto, _, sa in addrinfo: try: sock = socket.socket(family, socktype, proto) except OSError: continue sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) if family == socket.AF_INET6: sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, True) if sa[1] == 0: sa = sa[:1] + (listen_port,) + sa[2:] try: sock.bind(sa) except OSError as exc: for sock in sockets: sock.close() raise OSError(exc.errno, 'error while attempting to bind on ' 'address %r: %s' % (sa, exc.strerror)) from None if listen_port == 0: listen_port = sock.getsockname()[1] sockets.append(sock) return listen_port, sockets @asyncio.coroutine def _create_forward_listener(self, listen_port, sockets, factory): """Create an SSHForwardListener for a set of listening sockets""" servers = [] for sock in sockets: server = yield from self._loop.create_server(factory, sock=sock) servers.append(server) return SSHForwardListener(listen_port, servers) def _connection_made(self): """Handle the opening of a new connection""" raise NotImplementedError def _process_disconnect(self, pkttype, packet): """Process a disconnect message""" # pylint: disable=unused-argument code = packet.get_uint32() reason = packet.get_string() lang = packet.get_string() packet.check_end() try: reason = reason.decode('utf-8') lang = lang.decode('ascii') except UnicodeDecodeError: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid disconnect message') from None if code != DISC_BY_APPLICATION: exc = DisconnectError(code, reason, lang) else: exc = None self._force_close(exc) def _process_ignore(self, pkttype, packet): """Process an ignore message""" # pylint: disable=no-self-use,unused-argument _ = packet.get_string() # data packet.check_end() # Do nothing def _process_unimplemented(self, pkttype, packet): """Process an unimplemented message response""" # pylint: disable=no-self-use,unused-argument _ = packet.get_uint32() # seq packet.check_end() # Ignore this def _process_debug(self, pkttype, packet): """Process a debug message""" # pylint: disable=unused-argument always_display = packet.get_boolean() msg = packet.get_string() lang = packet.get_string() packet.check_end() try: msg = msg.decode('utf-8') lang = lang.decode('ascii') except UnicodeDecodeError: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid debug message') from None self._owner.debug_msg_received(msg, lang, always_display) def _process_service_request(self, pkttype, packet): """Process a service request""" # pylint: disable=unused-argument service = packet.get_string() packet.check_end() if service == self._next_service: self._next_service = None self.send_packet(Byte(MSG_SERVICE_ACCEPT), String(service)) if self.is_server() and service == _USERAUTH_SERVICE: self._auth_in_progress = True self._send_deferred_packets() else: raise DisconnectError(DISC_SERVICE_NOT_AVAILABLE, 'Unexpected service request received') def _process_service_accept(self, pkttype, packet): """Process a service accept response""" # pylint: disable=unused-argument service = packet.get_string() packet.check_end() if service == self._next_service: self._next_service = None if self.is_client() and service == _USERAUTH_SERVICE: self._auth_in_progress = True # This method is only in SSHClientConnection # pylint: disable=no-member self.try_next_auth() else: raise DisconnectError(DISC_SERVICE_NOT_AVAILABLE, 'Unexpected service accept received') def _process_kexinit(self, pkttype, packet): """Process a key exchange request""" # pylint: disable=unused-argument if self._kex: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Key exchange already in progress') _ = packet.get_bytes(16) # cookie kex_algs = packet.get_namelist() server_host_key_algs = packet.get_namelist() enc_algs_cs = packet.get_namelist() enc_algs_sc = packet.get_namelist() mac_algs_cs = packet.get_namelist() mac_algs_sc = packet.get_namelist() cmp_algs_cs = packet.get_namelist() cmp_algs_sc = packet.get_namelist() _ = packet.get_namelist() # lang_cs _ = packet.get_namelist() # lang_sc first_kex_follows = packet.get_boolean() _ = packet.get_uint32() # reserved packet.check_end() if self.is_server(): self._client_kexinit = packet.get_consumed_payload() # This method is only in SSHServerConnection # pylint: disable=no-member if not self._choose_server_host_key(server_host_key_algs): raise DisconnectError(DISC_KEY_EXCHANGE_FAILED, 'Unable to ' 'find compatible server host key') else: self._server_kexinit = packet.get_consumed_payload() if self._kexinit_sent: self._kexinit_sent = False else: self._send_kexinit() kex_alg = self._choose_alg('key exchange', self._kex_algs, kex_algs) self._kex = get_kex(self, kex_alg) self._ignore_first_kex = (first_kex_follows and self._kex.algorithm != kex_algs[0]) self._enc_alg_cs = self._choose_alg('encryption', self._enc_algs, enc_algs_cs) self._enc_alg_sc = self._choose_alg('encryption', self._enc_algs, enc_algs_sc) self._mac_alg_cs = self._choose_alg('MAC', self._mac_algs, mac_algs_cs) self._mac_alg_sc = self._choose_alg('MAC', self._mac_algs, mac_algs_sc) self._cmp_alg_cs = self._choose_alg('compression', self._cmp_algs, cmp_algs_cs) self._cmp_alg_sc = self._choose_alg('compression', self._cmp_algs, cmp_algs_sc) def _process_newkeys(self, pkttype, packet): """Process a new keys message, finishing a key exchange""" # pylint: disable=unused-argument packet.check_end() if self._next_recv_cipher: self._recv_cipher = self._next_recv_cipher self._recv_blocksize = self._next_recv_blocksize self._recv_mac = self._next_recv_mac self._recv_mode = self._next_recv_mode self._recv_macsize = self._next_recv_macsize self._decompressor = self._next_decompressor self._decompress_after_auth = self._next_decompress_after_auth self._next_recv_cipher = None else: raise DisconnectError(DISC_PROTOCOL_ERROR, 'New keys not negotiated') if self.is_client() and not (self._auth_in_progress or self._auth_complete): self.send_service_request(_USERAUTH_SERVICE) def _process_userauth_request(self, pkttype, packet): """Process a user authentication request""" # pylint: disable=unused-argument username = packet.get_string() service = packet.get_string() method = packet.get_string() if service != _CONNECTION_SERVICE: raise DisconnectError(DISC_SERVICE_NOT_AVAILABLE, 'Unexpected service in auth request') try: username = saslprep(username.decode('utf-8')) except (UnicodeDecodeError, SASLPrepError): raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid auth request message') from None if self.is_client(): raise DisconnectError(DISC_PROTOCOL_ERROR, 'Unexpected userauth request') elif self._auth_complete: # Silent ignore requests if we're already authenticated pass else: if username != self._username: self._username = username if not self._owner.begin_auth(username): self.send_userauth_success() return if self._auth: self._auth.cancel() self._auth = lookup_server_auth(self, self._username, method, packet) def _process_userauth_failure(self, pkttype, packet): """Process a user authentication failure response""" # pylint: disable=unused-argument self._auth_methods = packet.get_namelist() partial_success = packet.get_boolean() packet.check_end() if self.is_client() and self._auth: if partial_success: self._auth.auth_succeeded() else: self._auth.auth_failed() # This method is only in SSHClientConnection # pylint: disable=no-member self.try_next_auth() else: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Unexpected userauth response') def _process_userauth_success(self, pkttype, packet): """Process a user authentication success response""" # pylint: disable=unused-argument packet.check_end() if self.is_client() and self._auth: self._auth.cancel() self._auth = None self._auth_in_progress = False self._auth_complete = True self._extra.update(username=self._username) self._send_deferred_packets() self._owner.auth_completed() if self._auth_waiter: if not self._auth_waiter.cancelled(): self._auth_waiter.set_result(None) self._auth_waiter = None else: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Unexpected userauth response') def _process_userauth_banner(self, pkttype, packet): """Process a user authentication banner message""" # pylint: disable=unused-argument msg = packet.get_string() lang = packet.get_string() packet.check_end() try: msg = msg.decode('utf-8') lang = lang.decode('ascii') except UnicodeDecodeError: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid userauth banner') from None if self.is_client(): self._owner.auth_banner_received(msg, lang) else: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Unexpected userauth banner') def _process_global_request(self, pkttype, packet): """Process a global request""" # pylint: disable=unused-argument request = packet.get_string() want_reply = packet.get_boolean() try: request = request.decode('ascii') except UnicodeDecodeError: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid global request') from None name = '_process_' + request.replace('-', '_') + '_global_request' handler = getattr(self, name, None) self._global_request_queue.append((handler, packet, want_reply)) if len(self._global_request_queue) == 1: self._service_next_global_request() def _process_global_response(self, pkttype, packet): """Process a global response""" # pylint: disable=unused-argument if self._global_request_waiters: waiter = self._global_request_waiters.pop(0) if not waiter.cancelled(): waiter.set_result((pkttype, packet)) else: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Unexpected global response') def _process_channel_open(self, pkttype, packet): """Process a channel open request""" # pylint: disable=unused-argument chantype = packet.get_string() send_chan = packet.get_uint32() send_window = packet.get_uint32() send_pktsize = packet.get_uint32() try: chantype = chantype.decode('ascii') except UnicodeDecodeError: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid channel open request') from None try: name = '_process_' + chantype.replace('-', '_') + '_open' handler = getattr(self, name, None) if callable(handler): chan, session = handler(packet) chan.process_open(send_chan, send_window, send_pktsize, session) else: raise ChannelOpenError(OPEN_UNKNOWN_CHANNEL_TYPE, 'Unknown channel type') except ChannelOpenError as exc: self.send_channel_open_failure(send_chan, exc.code, exc.reason, exc.lang) def _process_channel_open_confirmation(self, pkttype, packet): """Process a channel open confirmation response""" # pylint: disable=unused-argument recv_chan = packet.get_uint32() send_chan = packet.get_uint32() send_window = packet.get_uint32() send_pktsize = packet.get_uint32() chan = self._channels.get(recv_chan) if chan: chan.process_open_confirmation(send_chan, send_window, send_pktsize, packet) else: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid channel number') def _process_channel_open_failure(self, pkttype, packet): """Process a channel open failure response""" # pylint: disable=unused-argument recv_chan = packet.get_uint32() code = packet.get_uint32() reason = packet.get_string() lang = packet.get_string() packet.check_end() try: reason = reason.decode('utf-8') lang = lang.decode('ascii') except UnicodeDecodeError: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid channel open failure') from None chan = self._channels.get(recv_chan) if chan: chan.process_open_failure(code, reason, lang) else: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid channel number') def _process_channel_msg(self, pkttype, packet): """Process a channel-specific message""" recv_chan = packet.get_uint32() chan = self._channels.get(recv_chan) if chan: chan.process_packet(pkttype, packet) else: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid channel number') packet_handlers = { MSG_DISCONNECT: _process_disconnect, MSG_IGNORE: _process_ignore, MSG_UNIMPLEMENTED: _process_unimplemented, MSG_DEBUG: _process_debug, MSG_SERVICE_REQUEST: _process_service_request, MSG_SERVICE_ACCEPT: _process_service_accept, MSG_KEXINIT: _process_kexinit, MSG_NEWKEYS: _process_newkeys, MSG_USERAUTH_REQUEST: _process_userauth_request, MSG_USERAUTH_FAILURE: _process_userauth_failure, MSG_USERAUTH_SUCCESS: _process_userauth_success, MSG_USERAUTH_BANNER: _process_userauth_banner, MSG_GLOBAL_REQUEST: _process_global_request, MSG_REQUEST_SUCCESS: _process_global_response, MSG_REQUEST_FAILURE: _process_global_response, MSG_CHANNEL_OPEN: _process_channel_open, MSG_CHANNEL_OPEN_CONFIRMATION: _process_channel_open_confirmation, MSG_CHANNEL_OPEN_FAILURE: _process_channel_open_failure, MSG_CHANNEL_WINDOW_ADJUST: _process_channel_msg, MSG_CHANNEL_DATA: _process_channel_msg, MSG_CHANNEL_EXTENDED_DATA: _process_channel_msg, MSG_CHANNEL_EOF: _process_channel_msg, MSG_CHANNEL_CLOSE: _process_channel_msg, MSG_CHANNEL_REQUEST: _process_channel_msg, MSG_CHANNEL_SUCCESS: _process_channel_msg, MSG_CHANNEL_FAILURE: _process_channel_msg } def abort(self): """Forcibly close the SSH connection This method closes the SSH connection immediately, without waiting for pending operations to complete and wihtout sending an explicit SSH disconnect message. Buffered data waiting to be sent will be lost and no more data will be received. When the the connection is closed, :meth:`connection_lost() ` on the associated :class:`SSHClient` object will be called with the value ``None``. """ self._force_close(None) def close(self): """Cleanly close the SSH connection This method calls :meth:`disconnect` with the reason set to indicate that the connection was closed explicitly by the application. """ self.disconnect(DISC_BY_APPLICATION, 'Disconnected by application') def disconnect(self, code, reason, lang=DEFAULT_LANG): """Disconnect the SSH connection This method sends a disconnect message and closes the SSH connection after buffered data waiting to be written has been sent. No more data will be received. When the connection is fully closed, :meth:`connection_lost() ` on the associated :class:`SSHClient` or :class:`SSHServer` object will be called with the value ``None``. :param integer code: The reason for the disconnect, from :ref:`disconnect reason codes ` :param string reason: A human readable reason for the disconnect :param string lang: The language the reason is in """ if not self._transport: return for chan in list(self._channels.values()): chan.close() reason = reason.encode('utf-8') lang = lang.encode('ascii') self.send_packet(Byte(MSG_DISCONNECT), UInt32(code), String(reason), String(lang)) self._transport.close() self._transport = None def get_extra_info(self, name, default=None): """Get additional information about the connection This method returns extra information about the connection once it is established. Supported values include everything supported by a socket transport plus: | username | client_version | server_version | send_cipher | send_mac | send_compression | recv_cipher | recv_mac | recv_compression See :meth:`get_extra_info() ` in :class:`asyncio.BaseTransport` for more information. """ return self._extra.get(name, self._transport.get_extra_info(name, default) if self._transport else default) def send_debug(self, msg, lang=DEFAULT_LANG, always_display=False): """Send a debug message on this connection This method can be called to send a debug message to the other end of the connection. :param string msg: The debug message to send :param string lang: The language the message is in :param boolean always_display: Whether or not to display the message """ msg = msg.encode('utf-8') lang = lang.encode('ascii') self.send_packet(Byte(MSG_DEBUG), Boolean(always_display), String(msg), String(lang)) @asyncio.coroutine def forward_connection(self, dest_host, dest_port): """Forward a tunneled SSH connection This method is a coroutine which can be returned by a ``session_factory`` to forward connections tunneled over SSH to the specified destination host and port. :param string dest_host: The hostname or address to forward the connections to :param integer dest_port: The port number to forward the connections to :returns: coroutine that returns an :class:`SSHTCPSession` """ try: def protocol_factory(): """Return an SSH port forwarder tied to this connection""" return SSHPortForwarder(self, self._loop) _, peer = yield from self._loop.create_connection(protocol_factory, dest_host, dest_port) except OSError as exc: raise ChannelOpenError(OPEN_CONNECT_FAILED, str(exc)) from None return SSHRemotePortForwarder(self, self._loop, peer) class SSHClientConnection(SSHConnection): """SSH client connection This class represents an SSH client connection. Once authentication is successful on a connection, new client sessions can be opened by calling :meth:`create_session`. Direct TCP connections can be opened by calling :meth:`create_connection`. Remote listeners for forwarded TCP connections can be opened by calling :meth:`create_server`. TCP port forwarding can be set up by calling :meth:`forward_local_port` or :meth:`forward_remote_port`. """ def __init__(self, client_factory, loop, host, port, known_hosts, username, client_keys, password, kex_algs, encryption_algs, mac_algs, compression_algs, rekey_bytes, rekey_seconds, auth_waiter): super().__init__(client_factory, loop, kex_algs, encryption_algs, mac_algs, compression_algs, rekey_bytes, rekey_seconds, server=False) self._host = host self._port = port if port != _DEFAULT_PORT else None self._known_hosts = known_hosts self._server_host_keys = set() self._server_ca_keys = set() self._revoked_server_keys = set() if username is None: username = getpass.getuser() self._username = saslprep(username) if client_keys: self._client_keys = _load_private_key_list(client_keys) else: self._client_keys = [] if client_keys is (): for file in _DEFAULT_KEY_FILES: try: file = os.path.join(os.environ['HOME'], '.ssh', file) self._client_keys.append(_load_private_key(file)) except OSError: pass self._password = password self._kbdint_password_auth = False self._remote_listeners = {} self._dynamic_remote_listeners = {} self._auth_waiter = auth_waiter def _connection_made(self): """Handle the opening of a new connection""" if self._known_hosts is None: self._server_host_keys = None self._server_ca_keys = None self._revoked_server_keys = None self._server_host_key_algs = (get_public_key_algs() + get_certificate_algs()) else: if not self._known_hosts: self._known_hosts = os.path.join(os.environ['HOME'], '.ssh', 'known_hosts') if isinstance(self._known_hosts, (str, bytes)): server_host_keys, server_ca_keys, revoked_server_keys = \ match_known_hosts(self._known_hosts, self._host, self._peer_addr, self._port) else: server_host_keys, server_ca_keys, revoked_server_keys = \ self._known_hosts server_host_keys = _load_public_key_list(server_host_keys) server_ca_keys = _load_public_key_list(server_ca_keys) revoked_server_keys = \ _load_public_key_list(revoked_server_keys) self._server_host_keys = set() self._server_host_key_algs = [] self._server_ca_keys = set(server_ca_keys) if server_ca_keys: self._server_host_key_algs.extend(get_certificate_algs()) self._revoked_server_keys = set(revoked_server_keys) for key in server_host_keys: self._server_host_keys.add(key) if key.algorithm not in self._server_host_key_algs: self._server_host_key_algs.append(key.algorithm) if not self._server_host_key_algs: raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE, 'No trusted server host keys available') def _cleanup(self, exc): """Clean up this client connection""" if self._remote_listeners: for listener in self._remote_listeners.values(): listener.close() self._remote_listeners = {} self._dynamic_remote_listeners = {} if self._auth_waiter: self._auth_waiter.set_exception(exc) self._auth_waiter = None super()._cleanup(exc) def validate_server_host_key(self, data): """Validate and return the server's host key""" try: cert = decode_ssh_certificate(data) except KeyImportError: pass else: if self._revoked_server_keys is not None and \ cert.signing_key in self._revoked_server_keys: raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE, 'Revoked server CA key') if self._server_ca_keys is not None and \ cert.signing_key not in self._server_ca_keys: raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE, 'Untrusted server CA key') try: cert.validate(CERT_TYPE_HOST, self._host) except ValueError as exc: raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE, str(exc)) from None return cert.key try: key = decode_ssh_public_key(data) except KeyImportError: pass else: if self._revoked_server_keys is not None and \ key in self._revoked_server_keys: raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE, 'Revoked server host key') if self._server_host_keys is not None and \ key not in self._server_host_keys: raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE, 'Untrusted server host key') return key raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE, 'Unable to decode server host key') def try_next_auth(self): """Attempt client authentication using the next compatible method""" if self._auth: self._auth.cancel() self._auth = None while self._auth_methods: method = self._auth_methods.pop(0) self._auth = lookup_client_auth(self, method) if self._auth: return self._force_close(DisconnectError(DISC_NO_MORE_AUTH_METHODS_AVAILABLE, 'Permission denied')) @asyncio.coroutine def public_key_auth_requested(self): """Return a client key to authenticate with""" if self._client_keys: key, cert = self._client_keys.pop(0) else: result = self._owner.public_key_auth_requested() if asyncio.iscoroutine(result): result = yield from result key, cert = _load_private_key(result) if cert: self._client_keys.insert(0, (key, None)) return cert.algorithm, key, cert.data elif key: return key.algorithm, key, key.get_ssh_public_key() else: return None, None, None @asyncio.coroutine def password_auth_requested(self): """Return a password to authenticate with""" # Only allow password auth if the connection supports encryption # and a MAC. if (not self._send_cipher or (not self._send_mac and self._send_mode not in ('chacha', 'gcm'))): return None if self._password: result = self._password self._password = None else: result = self._owner.password_auth_requested() if asyncio.iscoroutine(result): result = yield from result return result @asyncio.coroutine def password_change_requested(self): """Return a password to authenticate with and what to change it to""" result = self._owner.password_change_requested() if asyncio.iscoroutine(result): result = yield from result return result def password_changed(self): """Report a successful password change""" self._owner.password_changed() def password_change_failed(self): """Report a failed password change""" self._owner.password_change_failed() @asyncio.coroutine def kbdint_auth_requested(self): """Return the list of supported keyboard-interactive auth methods If keyboard-interactive auth is not supported in the client but a password was provided when the connection was opened, this will allow sending the password via keyboard-interactive auth. """ # Only allow keyboard interactive auth if the connection supports # encryption and a MAC. if (not self._send_cipher or (not self._send_mac and self._send_mode not in ('chacha', 'gcm'))): return None result = self._owner.kbdint_auth_requested() if asyncio.iscoroutine(result): result = yield from result if result is None and self._password is not None: self._kbdint_password_auth = True result = '' return result @asyncio.coroutine def kbdint_challenge_received(self, name, instructions, lang, prompts): """Return responses to a keyboard-interactive auth challenge""" if self._kbdint_password_auth: if len(prompts) == 0: # Silently drop any empty challenges used to print messages result = [] elif len(prompts) == 1 and 'password' in prompts[0][0].lower(): password = self.password_auth_requested() result = [password] if password is not None else None else: result = None else: result = self._owner.kbdint_challenge_received(name, instructions, lang, prompts) if asyncio.iscoroutine(result): result = yield from result return result def _process_session_open(self, packet): """Process an inbound session open request These requests are disallowed on an SSH client. """ # pylint: disable=no-self-use,unused-argument raise ChannelOpenError(OPEN_ADMINISTRATIVELY_PROHIBITED, 'Session open forbidden on client') def _process_direct_tcpip_open(self, packet): """Process an inbound direct TCP/IP channel open request These requests are disallowed on an SSH client. """ # pylint: disable=no-self-use,unused-argument raise ChannelOpenError(OPEN_ADMINISTRATIVELY_PROHIBITED, 'Direct TCP/IP open forbidden on client') def _process_forwarded_tcpip_open(self, packet): """Process an inbound forwarded TCP/IP channel open request""" dest_host = packet.get_string() dest_port = packet.get_uint32() orig_host = packet.get_string() orig_port = packet.get_uint32() packet.check_end() try: dest_host = dest_host.decode('utf-8') orig_host = orig_host.decode('utf-8') except UnicodeDecodeError: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid channel open request') from None # Some buggy servers send back a port of ``0`` instead of the actual # listening port when reporting connections which arrive on a listener # set up on a dynamic port. This lookup attempts to work around that. listener = (self._remote_listeners.get((dest_host, dest_port)) or self._dynamic_remote_listeners.get(dest_host)) if listener: return listener.process_connection(orig_host, orig_port) else: raise ChannelOpenError(OPEN_CONNECT_FAILED, 'No such listener') @asyncio.coroutine def close_client_listener(self, listener, listen_host, listen_port): """Close a remote TCP/IP listener""" yield from self._make_global_request( b'cancel-tcpip-forward', String(listen_host.encode('utf-8')), UInt32(listen_port)) if self._dynamic_remote_listeners[listen_host] == listener: del self._dynamic_remote_listeners[listen_host] del self._remote_listeners[listen_host, listen_port] @asyncio.coroutine def create_session(self, session_factory, command=None, *, subsystem=None, env={}, term_type=None, term_size=None, term_modes={}, encoding='utf-8', window=_DEFAULT_WINDOW, max_pktsize=_DEFAULT_MAX_PKTSIZE): """Create an SSH client session This method is a coroutine which can be called to create an SSH client session used to execute a command, start a subsystem such as sftp, or if no command or subsystem is specific run an interactive shell. Optional arguments allow terminal and environment information to be provided. By default, this class expects string data in its send and receive functions, which it encodes on the SSH connection in UTF-8 (ISO 10646) format. An optional encoding argument can be passed in to select a different encoding, or ``None`` can be passed in if the application wishes to send and receive raw bytes. Other optional arguments include the SSH receive window size and max packet size which default to 2 MB and 32 KB, respectively. :param callable session_factory: A callable which returns an :class:`SSHClientSession` object that will be created to handle activity on this session :param string command: (optional) The remote command to execute. By default, an interactive shell is started if no command or subsystem is provided. :param string subsystem: (optional) The name of a remote subsystem to start up :param dictionary env: (optional) The set of environment variables to set for this session. Keys and values passed in here will be converted to Unicode strings encoded as UTF-8 (ISO 10646) for transmission. .. note:: Many SSH servers restrict which environment variables a client is allowed to set. The server's configuration may need to be edited before environment variables can be successfully set in the remote environment. :param string term_type: (optional) The terminal type to set for this session. If this is not set, a pseudo-terminal will not be requested for this session. :param term_size: (optional) The terminal width and height in characters and optionally the width and height in pixels :param term_modes: (optional) POSIX terminal modes to set for this session, where keys are taken from :ref:`POSIX terminal modes ` with values defined in section 8 of :rfc:`4254#section-8`. :param string encoding: (optional) The Unicode encoding to use for data exchanged on the connection :param integer window: (optional) The receive window size for this session :param integer max_pktsize: (optional) The maximum packet size for this session :type term_size: *tuple of 2 or 4 integers* :returns: an :class:`SSHClientChannel` and :class:`SSHClientSession` """ chan = SSHClientChannel(self, self._loop, encoding, window, max_pktsize) return (yield from chan.create(session_factory, command, subsystem, env, term_type, term_size, term_modes)) @asyncio.coroutine def open_session(self, *args, **kwargs): """Open an SSH client session This method is a coroutine wrapper around :meth:`create_session` designed to provide a "high-level" stream interface for creating an SSH client session. Instead of taking a ``session_factory`` argument for constructing an object which will handle activity on the session via callbacks, it returns an :class:`SSHWriter` and two :class:`SSHReader` objects representing stdin, stdout, and stderr which can be used to perform I/O on the session. With the exception of ``session_factory``, all of the arguments to :meth:`create_session` are supported and have the same meaning. """ chan, session = yield from self.create_session(SSHClientStreamSession, *args, **kwargs) return (SSHWriter(session, chan), SSHReader(session, chan), SSHReader(session, chan, EXTENDED_DATA_STDERR)) @asyncio.coroutine def create_connection(self, session_factory, dest_host, dest_port, orig_host='', orig_port=0, *, encoding=None, window=_DEFAULT_WINDOW, max_pktsize=_DEFAULT_MAX_PKTSIZE): """Create an SSH TCP direct connection This method is a coroutine which can be called to request that the server open a new outbound TCP connection to the specified destination host and port. If the connection is successfully opened, a new SSH channel will be opened with data being handled by a :class:`SSHTCPSession` object created by ``session_factory``. Optional arguments include the host and port of the original client opening the connection when performing TCP port forwarding. By default, this class expects data to be sent and received as raw bytes. However, an optional encoding argument can be passed in to select the encoding to use, allowing the application send and receive string data. Other optional arguments include the SSH receive window size and max packet size which default to 2 MB and 32 KB, respectively. :param callable session_factory: A callable which returns an :class:`SSHClientSession` object that will be created to handle activity on this session :param string dest_host: The hostname or address to connect to :param integer dest_port: The port number to connect to :param string orig_host: (optional) The hostname or address of the client requesting the connection :param integer orig_port: (optional) The port number of the client requesting the connection :param string encoding: (optional) The Unicode encoding to use for data exchanged on the connection :param integer window: (optional) The receive window size for this session :param integer max_pktsize: (optional) The maximum packet size for this session :returns: an :class:`SSHTCPChannel` and :class:`SSHTCPSession` :raises: :exc:`ChannelOpenError` if the connection can't be opened """ chan = SSHTCPChannel(self, self._loop, encoding, window, max_pktsize) return (yield from chan.connect(session_factory, dest_host, dest_port, orig_host, orig_port)) @asyncio.coroutine def open_connection(self, *args, **kwargs): """Open an SSH TCP direct connection This method is a coroutine wrapper around :meth:`create_connection` designed to provide a "high-level" stream interface for creating an SSH TCP direct connection. Instead of taking a ``session_factory`` argument for constructing an object which will handle activity on the session via callbacks, it returns :class:`SSHReader` and :class:`SSHWriter` objects which can be used to perform I/O on the connection. With the exception of ``session_factory``, all of the arguments to :meth:`create_connection` are supported and have the same meaning here. :returns: an :class:`SSHReader` and :class:`SSHWriter` :raises: :exc:`ChannelOpenError` if the connection can't be opened """ chan, session = yield from self.create_connection(SSHTCPStreamSession, *args, **kwargs) return SSHReader(session, chan), SSHWriter(session, chan) @asyncio.coroutine def create_server(self, session_factory, listen_host, listen_port, *, encoding=None, window=_DEFAULT_WINDOW, max_pktsize=_DEFAULT_MAX_PKTSIZE): """Create a remote SSH TCP listener This method is a coroutine which can be called to request that the server listen on the specified remote address and port for incoming TCP connections. If the request is successful, the return value is an :class:`SSHListener` object which can be used later to shut down the listener. If the request fails, ``None`` is returned. :param session_factory: A callable or coroutine which takes arguments of the original host and port of the client and decides whether to accept the connection or not, either returning an :class:`SSHTCPSession` object used to handle activity on that connection or raising :exc:`ChannelOpenError` to indicate that the connection should not be accepted :param string listen_host: The hostname or address on the remote host to listen on :param integer listen_port: The port number on the remote host to listen on :param string encoding: (optional) The Unicode encoding to use for data exchanged on the connection :param integer window: (optional) The receive window size for this session :param integer max_pktsize: (optional) The maximum packet size for this session :type session_factory: callable or coroutine :returns: :class:`SSHListener` or ``None`` if the listener can't be opened """ listen_host = listen_host.lower() pkttype, packet = \ yield from self._make_global_request( b'tcpip-forward', String(listen_host.encode('utf-8')), UInt32(listen_port)) if pkttype == MSG_REQUEST_SUCCESS: if listen_port == 0: listen_port = packet.get_uint32() dynamic = True else: dynamic = False packet.check_end() listener = SSHClientListener(self, self._loop, session_factory, listen_host, listen_port, encoding, window, max_pktsize) if dynamic: self._dynamic_remote_listeners[listen_host] = listener self._remote_listeners[listen_host, listen_port] = listener return listener else: packet.check_end() return None @asyncio.coroutine def start_server(self, handler_factory, *args, **kwargs): """Start a remote SSH TCP listener This method is a coroutine wrapper around :meth:`create_server` designed to provide a "high-level" stream interface for creating remote SSH TCP listeners. Instead of taking a ``session_factory`` argument for constructing an object which will handle activity on the session via callbacks, it takes a ``handler_factory`` which returns a callable or coroutine that will be passed :class:`SSHReader` and :class:`SSHWriter` objects which can be used to perform I/O on each new connection which arrives. Like :meth:`create_server`, ``handler_factory`` can also raise :exc:`ChannelOpenError` if the connection should not be accepted. With the exception of ``handler_factory`` replacing ``session_factory``, all of the arguments to :meth:`create_server` are supported and have the same meaning here. :param handler_factory: A callable or coroutine which takes arguments of the original host and port of the client and decides whether to accept the connection or not, either returning a callback or coroutine used to handle activity on that connection or raising :exc:`ChannelOpenError` to indicate that the connection should not be accepted :type handler_factory: callable or coroutine :returns: :class:`SSHListener` or ``None`` if the listener can't be opened """ def session_factory(orig_host, orig_port): """Return a TCP stream session handler""" return SSHTCPStreamSession(handler_factory(orig_host, orig_port)) return (yield from self.create_server(session_factory, *args, **kwargs)) @asyncio.coroutine def forward_local_port(self, listen_host, listen_port, dest_host, dest_port): """Set up local port forwarding This method is a coroutine which attempts to set up port forwarding from a local listening port to a remote host and port via the SSH connection. If the request is successful, the return value is an :class:`SSHListener` object which can be used later to shut down the port forwarding. :param string listen_host: The hostname or address on the local host to listen on :param integer listen_port: The port number on the local host to listen on :param string dest_host: The hostname or address to forward the connections to :param integer dest_port: The port number to forward the connections to :returns: :class:`SSHListener` :raises: :exc:`OSError` if the listener can't be opened """ def factory(): """Return a local port forwarder""" return SSHLocalPortForwarder(self, self._loop, self.create_connection, dest_host, dest_port) listen_port, sockets = \ yield from self._create_tcp_listener(listen_host, listen_port) return (yield from self._create_forward_listener(listen_port, sockets, factory)) @asyncio.coroutine def forward_remote_port(self, listen_host, listen_port, dest_host, dest_port): """Set up remote port forwarding This method is a coroutine which attempts to set up port forwarding from a remote listening port to a local host and port via the SSH connection. If the request is successful, the return value is an :class:`SSHListener` object which can be used later to shut down the port forwarding. If the request fails, ``None`` is returned. :param string listen_host: The hostname or address on the remote host to listen on :param integer listen_port: The port number on the remote host to listen on :param string dest_host: The hostname or address to forward connections to :param integer dest_port: The port number to forward connections to :returns: :class:`SSHListener` or ``None`` if the listener can't be opened """ def session_factory(orig_host, orig_port): """Return an SSHTCPConnection used to do remote port forwarding""" # pylint: disable=unused-argument return self.forward_connection(dest_host, dest_port) return self.create_server(session_factory, listen_host, listen_port) @asyncio.coroutine def start_sftp_client(self, path_encoding='utf-8', path_errors='strict'): """Start an SFTP client This method is a coroutine which attempts to start a secure file transfer session. If it succeeds, it returns an :class:`SFTPClient` object which can be used to copy and access files on the remote host. An optional Unicode encoding can be specified for sending and receiving pathnames, defaulting to UTF-8 with strict error checking. If an encoding of ``None`` is specified, pathnames will be left as bytes rather than being converted to & from strings. :param string path_encoding: The Unicode encoding to apply when sending and receiving remote pathnames :param string path_errors: The error handling strategy to apply on encode/decode errors :returns: :class:`SFTPClient` :raises: :exc:`SFTPError` if the session can't be opened """ def session_factory(): """Return an SFTP client session handler""" return SFTPClientSession(self._loop, version_waiter) version_waiter = asyncio.Future(loop=self._loop) _, session = yield from self.create_session(session_factory, subsystem='sftp', encoding=None) yield from version_waiter return SFTPClient(session, path_encoding, path_errors) class SSHServerConnection(SSHConnection): """SSH server connection This class represents an SSH server connection. During authentication, :meth:`send_auth_banner` can be called to send an authentication banner to the client. Once authenticated, :class:`SSHServer` objects wishing to create session objects with non-default channel properties can call :meth:`create_server_channel` from their :meth:`session_requested() ` method and return a tuple of the :class:`SSHServerChannel` object returned from that and either an :class:`SSHServerSession` object or a coroutine which returns an :class:`SSHServerSession`. Similarly, :class:`SSHServer` objects wishing to create TCP connection objects with non-default channel properties can call :meth:`create_tcp_channel` from their :meth:`connection_requested() ` method and return a tuple of the :class:`SSHTCPChannel` object returned from that and either an :class:`SSHTCPSession` object or a coroutine which returns an :class:`SSHTCPSession`. """ def __init__(self, server_factory, loop, server_host_keys, authorized_client_keys, kex_algs, encryption_algs, mac_algs, compression_algs, allow_pty, session_factory, session_encoding, sftp_factory, window, max_pktsize, rekey_bytes, rekey_seconds): super().__init__(server_factory, loop, kex_algs, encryption_algs, mac_algs, compression_algs, rekey_bytes, rekey_seconds, server=True) self._allow_pty = allow_pty self._session_factory = session_factory self._session_encoding = session_encoding self._sftp_factory = sftp_factory self._window = window self._max_pktsize = max_pktsize server_host_keys = _load_private_key_list(server_host_keys) self._server_host_keys = OrderedDict() for key, cert in server_host_keys: if key.algorithm in self._server_host_keys: raise ValueError('Multiple keys of type %s found' % key.algorithm.decode('ascii')) self._server_host_keys[key.algorithm] = (key, key.get_ssh_public_key()) if cert: if cert.algorithm in self._server_host_keys: raise ValueError('Multiple keys of type %s found' % cert.algorithm.decode('ascii')) self._server_host_keys[cert.algorithm] = (key, cert.data) if not self._server_host_keys: raise ValueError('No server host keys provided') self._server_host_key_algs = self._server_host_keys.keys() self._client_keys = _load_authorized_keys(authorized_client_keys) self._server_host_key = None self._key_options = {} self._cert_options = None self._kbdint_password_auth = False def _connection_made(self): """Handle the opening of a new connection""" pass def _choose_server_host_key(self, peer_host_key_algs): """Choose the server host key to use Given a list of host key algorithms supported by the client, select the first compatible server host key we have and return whether or not we were able to find a match. """ for alg in peer_host_key_algs: if alg in self._server_host_keys: self._server_host_key = self._server_host_keys[alg] return True return False def get_server_host_key(self): """Return the chosen server host key This method returns the chosen server host private key and a corresponding public key or certificate which contains it. """ return self._server_host_key @asyncio.coroutine def _validate_client_certificate(self, username, key_data): """Validate a client certificate for the specified user""" try: cert = decode_ssh_certificate(key_data) except KeyImportError: return None options = None if self._client_keys: options = self._client_keys.validate(cert.signing_key, self._peer_addr, cert.principals, ca=True) if options is None: result = self._owner.validate_ca_key(username, cert.signing_key) if asyncio.iscoroutine(result): result = yield from result if not result: return None options = {} self._key_options = options if self.get_key_option('principals'): username = None try: cert.validate(CERT_TYPE_USER, username) except ValueError: return None allowed_addresses = self.get_certificate_option('source-address') if allowed_addresses: ip = ip_address(self._peer_addr) if not any(ip in network for network in allowed_addresses): return None self._cert_options = cert.options return cert.key @asyncio.coroutine def _validate_client_public_key(self, username, key_data): """Validate a client public key for the specified user""" try: key = decode_ssh_public_key(key_data) except KeyImportError: return None options = None if self._client_keys: options = self._client_keys.validate(key, self._peer_addr) if options is None: result = self._owner.validate_public_key(username, key) if asyncio.iscoroutine(result): result = yield from result if not result: return None options = {} self._key_options = options return key def public_key_auth_supported(self): """Return whether or not public key authentication is supported""" return (bool(self._client_keys) or self._owner.public_key_auth_supported()) @asyncio.coroutine def validate_public_key(self, username, key_data, msg, signature): """Validate the public key or certificate for the specified user This method validates that the public key or certificate provided is allowed for the specified user. If msg and signature are provided, the key is used to also validate the message signature. It returns ``True`` when the key is allowed and the signature (if present) is valid. Otherwise, it returns ``False``. """ key = ((yield from self._validate_client_certificate(username, key_data)) or (yield from self._validate_client_public_key(username, key_data))) if key is None: return False elif msg: return key.verify(String(self._session_id) + msg, signature) else: return True def password_auth_supported(self): """Return whether or not password authentication is supported""" return self._owner.password_auth_supported() @asyncio.coroutine def validate_password(self, username, password): """Return whether password is valid for this user""" result = self._owner.validate_password(username, password) if asyncio.iscoroutine(result): result = yield from result return result def kbdint_auth_supported(self): """Return whether or not keyboard-interactive authentication is supported""" if self._owner.kbdint_auth_supported(): return True elif self._owner.password_auth_supported(): self._kbdint_password_auth = True return True else: return False @asyncio.coroutine def get_kbdint_challenge(self, username, lang, submethods): """Return a keyboard-interactive auth challenge""" if self._kbdint_password_auth: result = ('', '', DEFAULT_LANG, (('Password:', False),)) else: result = self._owner.get_kbdint_challenge(username, lang, submethods) if asyncio.iscoroutine(result): result = yield from result return result @asyncio.coroutine def validate_kbdint_response(self, username, responses): """Return whether the keyboard-interactive response is valid for this user""" if self._kbdint_password_auth: if len(responses) != 1: return False result = self._owner.validate_password(username, responses[0]) else: result = self._owner.validate_kbdint_response(username, responses) if asyncio.iscoroutine(result): result = yield from result return result def _process_session_open(self, packet): """Process an incoming session open request""" packet.check_end() if self._session_factory or self._sftp_factory: chan = self.create_server_channel(self._session_encoding, self._window, self._max_pktsize) session = SSHServerStreamSession(self._allow_pty, self._session_factory, self._sftp_factory) else: result = self._owner.session_requested() if not result: raise ChannelOpenError(OPEN_CONNECT_FAILED, 'Session refused') if isinstance(result, tuple): chan, result = result else: chan = self.create_server_channel(self._session_encoding, self._window, self._max_pktsize) if callable(result): session = SSHServerStreamSession(self._allow_pty, result, None) else: session = result return chan, session def _process_direct_tcpip_open(self, packet): """Process an incoming direct TCP/IP open request""" dest_host = packet.get_string() dest_port = packet.get_uint32() orig_host = packet.get_string() orig_port = packet.get_uint32() packet.check_end() try: dest_host = dest_host.decode('utf-8') orig_host = orig_host.decode('utf-8') except UnicodeDecodeError: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid channel open request') from None if not self.check_key_permission('port-forwarding') or \ not self.check_certificate_permission('port-forwarding'): raise ChannelOpenError(OPEN_ADMINISTRATIVELY_PROHIBITED, 'Port forwarding not permitted') permitted_opens = self.get_key_option('permitopen') if permitted_opens and \ (dest_host, dest_port) not in permitted_opens and \ (dest_host, None) not in permitted_opens: raise ChannelOpenError(OPEN_ADMINISTRATIVELY_PROHIBITED, 'Port forwarding not permitted to %s ' 'port %s' % (dest_host, dest_port)) result = self._owner.connection_requested(dest_host, dest_port, orig_host, orig_port) if not result: raise ChannelOpenError(OPEN_CONNECT_FAILED, 'Connection refused') if result is True: result = self.forward_connection(dest_host, dest_port) if isinstance(result, tuple): chan, result = result else: chan = self.create_tcp_channel() if callable(result): session = SSHTCPStreamSession(result) else: session = result chan.set_inbound_peer_names(dest_host, dest_port, orig_host, orig_port) return chan, session def _process_tcpip_forward_global_request(self, packet): """Process an incoming TCP/IP port forwarding request""" listen_host = packet.get_string() listen_port = packet.get_uint32() packet.check_end() try: listen_host = listen_host.decode('utf-8').lower() except UnicodeDecodeError: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid TCP forward request') from None if not self.check_key_permission('port-forwarding') or \ not self.check_certificate_permission('port-forwarding'): self._report_global_response(False) return result = self._owner.server_requested(listen_host, listen_port) if not result: self._report_global_response(False) return if result is True: result = self._create_default_forwarder(listen_host, listen_port) asyncio.async(self._finish_forward(result, listen_host, listen_port), loop=self._loop) @asyncio.coroutine def _create_default_forwarder(self, listen_host, listen_port): """Create a TCP listener which does port forwarding""" def factory(): """Return a local port forwarder""" return SSHLocalPortForwarder(self, self._loop, self.create_connection, listen_host, listen_port) listen_port, sockets = \ yield from self._create_tcp_listener(listen_host, listen_port) return (yield from self._create_forward_listener(listen_port, sockets, factory)) @asyncio.coroutine def _finish_forward(self, listener, listen_host, listen_port): """Finish processing a port forwarding request""" if asyncio.iscoroutine(listener): try: listener = yield from listener except OSError: listener = None if listener: if listen_port == 0: listen_port = listener.get_port() result = UInt32(listen_port) else: result = True self._local_listeners[listen_host, listen_port] = listener self._report_global_response(result) else: self._report_global_response(False) def _process_cancel_tcpip_forward_global_request(self, packet): """Process a request to cancel TCP/IP port forwarding""" listen_host = packet.get_string() listen_port = packet.get_uint32() packet.check_end() try: listen_host = listen_host.decode('utf-8').lower() except UnicodeDecodeError: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid TCP forward request') from None listener = self._local_listeners.pop((listen_host, listen_port)) if not listener: raise DisconnectError(DISC_PROTOCOL_ERROR, 'No such listener') listener.close() def send_auth_banner(self, msg, lang=DEFAULT_LANG): """Send an authentication banner to the client This method can be called to send an authentication banner to the client, displaying information while authentication is in progress. It is an error to call this method after the authentication is complete. :param string msg: The message to display :param string lang: The language the message is in :raises: :exc:`OSError` if authentication is already completed """ if self._auth_complete: raise OSError('Authentication already completed') msg = msg.encode('utf-8') lang = lang.encode('ascii') self.send_packet(Byte(MSG_USERAUTH_BANNER), String(msg), String(lang)) def set_authorized_keys(self, authorized_keys): """Set the keys trusted for client public key authentication This method can be called to set the trusted user and CA keys for client public key authentication. It should generally be called from the :meth:`begin_auth ` method of :class:`SSHServer` to set the appropriate keys for the user attempting to authenticate. :param authorized_keys: The keys to trust for client public key authentication :type authorized_keys: *see* :ref:`SpecifyingAuthorizedKeys` """ self._client_keys = _load_authorized_keys(authorized_keys) def get_key_option(self, option, default=None): """Return option from authorized_keys If a client key or certificate was presented during authentication, this method returns the value of the requested option in the corresponding authorized_keys entry if it was set. Otherwise, it returns the default value provided. The following standard options are supported: | command (string) | environment (dictionary of name/value pairs) | from (list of host patterns) | permitopen (list of host/port tuples) | principals (list of usernames) Non-standard options are also supported and will return the value ``True`` if the option is present without a value or return a list of strings containing the values associated with each occurrence of that option name. If the option is not present, the specified default value is returned. :param string option: The name of the option to look up. :param default: The default value to return if the option is not present. :returns: The value of the option in authorized_keys, if set """ if self._key_options is not None: return self._key_options.get(option, default) else: return default def check_key_permission(self, permission): """Check permissions in authorized_keys If a client key or certificate was presented during authentication, this method returns whether the specified permission is allowed by the corresponding authorized_keys entry. By default, all permissions are granted, but they can be revoked by specifying an option starting with 'no-' without a value. The following standard options are supported: | X11-forwarding | agent-forwarding | port-forwarding | pty | user-rc AsyncSSH internally enforces port-forwarding and pty permissions but ignores the other values since it does not implement those features. Non-standard permissions can also be checked, as long as the option follows the convention of starting with 'no-'. :param string permission: The name of the permission to check (without the 'no-'). :returns: A boolean indicating if the permission is granted. """ if self._key_options is not None: return not self._key_options.get('no-' + permission, False) else: return True def get_certificate_option(self, option, default=None): """Return option from user certificate If a user certificate was presented during authentication, this method returns the value of the requested option in the certificate if it was set. Otherwise, it returns the default value provided. The following options are supported: | force-command (string) | source-address (list of CIDR-style IP network addresses) :param string option: The name of the option to look up. :param default: The default value to return if the option is not present. :returns: The value of the option in the user certificate, if set """ if self._cert_options is not None: return self._cert_options.get(option, default) else: return default def check_certificate_permission(self, permission): """Check permissions in user certificate If a user certificate was presented during authentication, this method returns whether the specified permission was granted in the certificate. Otherwise, it acts as if all permissions are granted and returns ``True``. The following permissions are supported: | X11-forwarding | agent-forwarding | port-forwarding | pty | user-rc AsyncSSH internally enforces port-forwarding and pty permissions but ignores the other values since it does not implement those features. :param string permission: The name of the permission to check (without the 'permit-'). :returns: A boolean indicating if the permission is granted. """ if self._cert_options is not None: return self._cert_options.get('permit-' + permission, False) else: return True def create_server_channel(self, encoding='utf-8', window=_DEFAULT_WINDOW, max_pktsize=_DEFAULT_MAX_PKTSIZE): """Create an SSH server channel for a new SSH session This method can be called by :meth:`session_requested() ` to create an :class:`SSHServerChannel` with the desired encoding, window, and max packet size for a newly created SSH server session. :param string encoding: (optional) The Unicode encoding to use for data exchanged on the session, defaulting to UTF-8 (ISO 10646) format. If ``None`` is passed in, the application can send and receive raw bytes. :param integer window: (optional) The receive window size for this session :param integer max_pktsize: (optional) The maximum packet size for this session :returns: :class:`SSHServerChannel` """ return SSHServerChannel(self, self._loop, encoding, window, max_pktsize) def create_tcp_channel(self, encoding=None, window=_DEFAULT_WINDOW, max_pktsize=_DEFAULT_MAX_PKTSIZE): """Create an SSH TCP channel for a new direct TCP connection This method can be called by :meth:`connection_requested() ` to create an :class:`SSHTCPChannel` with the desired encoding, window, and max packet size for a newly created SSH direct connection. :param string encoding: (optional) The Unicode encoding to use for data exchanged on the connection. This defaults to ``None``, allowing the application to send and receive raw bytes. :param integer window: (optional) The receive window size for this session :param integer max_pktsize: (optional) The maximum packet size for this session :returns: :class:`SSHTCPChannel` """ return SSHTCPChannel(self, self._loop, encoding, window, max_pktsize) @asyncio.coroutine def create_connection(self, session_factory, listen_host, listen_port, orig_host='', orig_port=0, *, encoding=None, window=_DEFAULT_WINDOW, max_pktsize=_DEFAULT_MAX_PKTSIZE): """Create an SSH TCP forwarded connection This method is a coroutine which can be called to notify the client about a new inbound TCP connection arriving on the specified listening host and port. If the connection is successfully opened, a new SSH channel will be opened with data being handled by a :class:`SSHTCPSession` object created by ``session_factory``. Optional arguments include the host and port of the original client opening the connection when performing TCP port forwarding. By default, this class expects data to be sent and received as raw bytes. However, an optional encoding argument can be passed in to select the encoding to use, allowing the application send and receive string data. Other optional arguments include the SSH receive window size and max packet size which default to 2 MB and 32 KB, respectively. :param callable session_factory: A callable which returns an :class:`SSHClientSession` object that will be created to handle activity on this session :param string listen_host: The hostname or address of the listener receiving the connection :param integer listen_port: The port number of the listener receiving the connection :param string orig_host: (optional) The hostname or address of the client requesting the connection :param integer orig_port: (optional) The port number of the client requesting the connection :param string encoding: (optional) The Unicode encoding to use for data exchanged on the connection :param integer window: (optional) The receive window size for this session :param integer max_pktsize: (optional) The maximum packet size for this session :returns: an :class:`SSHTCPChannel` and :class:`SSHTCPSession` """ chan = SSHTCPChannel(self, self._loop, encoding, window, max_pktsize) return (yield from chan.accept(session_factory, listen_host, listen_port, orig_host, orig_port)) @asyncio.coroutine def open_connection(self, handler_factory, *args, **kwargs): """Open an SSH TCP forwarded connection This method is a coroutine wrapper around :meth:`create_connection` designed to provide a "high-level" stream interface for creating an SSH TCP forwarded connection. Instead of taking a ``session_factory`` argument for constructing an object which will handle activity on the session via callbacks, it returns :class:`SSHReader` and :class:`SSHWriter` objects which can be used to perform I/O on the connection. With the exception of ``session_factory``, all of the arguments to :meth:`create_connection` are supported and have the same meaning here. :returns: an :class:`SSHReader` and :class:`SSHWriter` """ def session_factory(): """Return a TCP stream session handler""" return SSHTCPStreamSession(handler_factory) chan, session = yield from self.create_connection(session_factory, *args, **kwargs) return SSHReader(session, chan), SSHWriter(session, chan) class SSHClient: """SSH client protocol handler Applications should subclass this when implementing an SSH client. The functions listed below should be overridden to define application-specific behavior. In particular, the method :meth:`auth_completed` should be defined to open the desired SSH channels on this connection once authentication has been completed. For simple password or public key based authentication, nothing needs to be defined here if the password or client keys are passed in when the connection is created. However, to prompt interactively or otherwise dynamically select these values, the methods :meth:`password_auth_requested` and/or :meth:`public_key_auth_requested` can be defined. Keyboard-interactive authentication is also supported via :meth:`kbdint_auth_requested` and :meth:`kbdint_challenge_received`. If the server sends an authentication banner, the method :meth:`auth_banner_received` will be called. If the server requires a password change, the method :meth:`password_change_requested` will be called, followed by either :meth:`password_changed` or :meth:`password_change_failed` depending on whether the password change is successful. """ # pylint: disable=no-self-use,unused-argument def connection_made(self, connection): """Called when a connection is made This method is called as soon as the TCP connection completes. The connection parameter should be stored if needed for later use. :param connection: The connection which was successfully opened :type connection: :class:`SSHClientConnection` """ def connection_lost(self, exc): """Called when a connection is lost or closed This method is called when a connection is closed. If the connection is shut down cleanly, *exc* will be ``None``. Otherwise, it will be an exception explaining the reason for the disconnect. :param exc: The exception which caused the connection to close, or ``None`` if the connection closed cleanly :type exc: :class:`Exception` """ def debug_msg_received(self, msg, lang, always_display): """A debug message was received on this connection This method is called when the other end of the connection sends a debug message. Applications should implement this method if they wish to process these debug messages. :param string msg: The debug message sent :param string lang: The language the message is in :param boolean always_display: Whether or not to display the message """ def auth_banner_received(self, msg, lang): """An incoming authentication banner was received This method is called when the server sends a banner to display during authentication. Applications should implement this method if they wish to do something with the banner. :param string msg: The message the server wanted to display :param string lang: The language the message is in """ def auth_completed(self): """Authentication was completed successfully This method is called when authentication has completed succesfully. Applications may use this method to create whatever client sessions and direct TCP/IP connections are needed and/or set up listeners for incoming TCP/IP connections coming from the server. """ # pylint: disable=no-self-use def public_key_auth_requested(self): """Public key authentication has been requested This method should return a private key corresponding to the user that authentication is being attempted for. This method may be called multiple times and can return a different key to try each time it is called. When there are no keys left to try, it should return ``None`` to indicate that some other authentication method should be tried. If client keys were provided when the connection was opened, they will be tried before this method is called. If blocking operations need to be performed to determine the key to authenticate with, this method may be defined as a coroutine. :returns: A key as described in :ref:`SpecifyingPrivateKeys` or ``None`` to move on to another authentication method """ return None def password_auth_requested(self): """Password authentication has been requested This method should return a string containing the password corresponding to the user that authentication is being attempted for. It may be called multiple times and can return a different password to try each time, but most servers have a limit on the number of attempts allowed. When there's no password left to try, this method should return ``None`` to indicate that some other authentication method should be tried. If a password was provided when the connection was opened, it will be tried before this method is called. If blocking operations need to be performed to determine the password to authenticate with, this method may be defined as a coroutine. :returns: A string containing the password to authenticate with or ``None`` to move on to another authentication method """ return None def password_change_requested(self, prompt, lang): """A password change has been requested This method is called when password authentication was attempted and the user's password was expired on the server. To request a password change, this method should return a tuple or two strings containing the old and new passwords. Otherwise, it should return ``NotImplemented``. If blocking operations need to be performed to determine the passwords to authenticate with, this method may be defined as a coroutine. By default, this method returns ``NotImplemented``. :param string prompt: The prompt requesting that the user enter a new password :param string lang: the language that the prompt is in :returns: A tuple of two strings containing the old and new passwords or ``NotImplemented`` if password changes aren't supported """ return NotImplemented def password_changed(self): """The requested password change was successful This method is called to indicate that a requested password change was successful. It is generally followed by a call to :meth:`auth_completed` since this means authentication was also successful. """ def password_change_failed(self): """The requested password change has failed This method is called to indicate that a requested password change failed, generally because the requested new password doesn't meet the password criteria on the remote system. After this method is called, other forms of authentication will automatically be attempted. """ def kbdint_auth_requested(self): """Keyboard-interactive authentication has been requested This method should return a string containing a comma-separated list of submethods that the server should use for keyboard-interactive authentication. An empty string can be returned to let the server pick the type of keyboard-interactive authentication to perform. If keyboard-interactive authentication is not supported, ``None`` should be returned. By default, keyboard-interactive authentication is supported if a password was provided when the :class:`SSHClient` was created and it hasn't been sent yet. If the challenge is not a password challenge, this authentication will fail. This method and the :meth:`kbdint_challenge_received` method can be overridden if other forms of challenge should be supported. If blocking operations need to be performed to determine the submethods to request, this method may be defined as a coroutine. :returns: A string containing the submethods the server should use for authentication or ``None`` to move on to another authentication method """ return None def kbdint_challenge_received(self, name, instruction, lang, prompts): """A keyboard-interactive auth challenge has been received This method is called when the server sends a keyboard-interactive authentication challenge. The return value should be a list of strings of the same length as the number of prompts provided if the challenge can be answered, or ``None`` to indicate that some other form of authentication should be attempted. If blocking operations need to be performed to determine the responses to authenticate with, this method may be defined as a coroutine. By default, this method will look for a challenge consisting of a single 'Password:' prompt, and call the method :meth:`password_auth_requested` to provide the response. It will also ignore challenges with no prompts (generally used to provide instructions). Any other form of challenge will cause this method to return ``None`` to move on to another authentication method. :param string name: The name of the challenge :param string instruction: Instructions to the user about how to respond to the challenge :param string lang: The language the challenge is in :param prompts: The challenges the user should respond to and whether or not the responses should be echoed when they are entered :type prompts: list of tuples of string and boolean :returns: List of string responses to the challenge or ``None`` to move on to another authentication method """ return None class SSHServer: """SSH server protocol handler Applications should subclass this when implementing an SSH server. At a minimum, one or more of the authentication handlers will need to be overridden to perform authentication, or :meth:`begin_auth` should be overridden to return ``False`` to indicate that no authentication is required. In addition, one or more of the :meth:`session_requested`, :meth:`connection_requested`, or :meth:`server_requested` methods will need to be overridden to handle requests to open sessions or direct TCP/IP connections or set up listeners for forwarded TCP/IP connections. """ # pylint: disable=no-self-use,unused-argument def connection_made(self, connection): """Called when a connection is made This method is called when a new TCP connection is accepted. The connection parameter should be stored if needed for later use. """ def connection_lost(self, exc): """Called when a connection is lost or closed This method is called when a connection is closed. If the connection is shut down cleanly, *exc* will be ``None``. Otherwise, it will be an exception explaining the reason for the disconnect. """ def debug_msg_received(self, msg, lang, always_display): """A debug message was received on this connection This method is called when the other end of the connection sends a debug message. Applications should implement this method if they wish to process these debug messages. :param string msg: The debug message sent :param string lang: The language the message is in :param boolean always_display: Whether or not to display the message """ def begin_auth(self, username): """Authentication has been requested by the client This method will be called when authentication is attempted for the specified user. Applications should use this method to prepare whatever state they need to complete the authentication, such as loading in the set of authorized keys for that user. If no authentication is required for this user, this method should return ``False`` to cause the authentication to immediately succeed. Otherwise, it should return ``True`` to indicate that authentication should proceed. :param string username: The name of the user being authenticated :returns: A boolean indicating whether authentication is required """ return True def public_key_auth_supported(self): """Return whether or not public key authentication is supported This method should return ``True`` if client public key authentication is supported. Applications wishing to support it must have this method return ``True`` and implement :meth:`validate_public_key` to return whether or not the key provided by the client is valid for the user being authenticated. By default, it returns ``False`` indicating the client public key authentication is not supported. :returns: A boolean indicating if public key authentication is supported or not """ return False def validate_public_key(self, username, key): """Return whether key is an authorized client key for this user Basic key-based client authentication can be supported by passing authorized keys in the ``authorized_client_keys`` argument of :func:`create_server`, or by calling :meth:`set_authorized_keys ` on the server connection from the :meth:`begin_auth` method. However, for more flexibility in matching on the allowed set of keys, this method can be implemented by the application to do the matching itself. It should return ``True`` if the specified key is a valid client key for the user being authenticated. This method may be called multiple times with different keys provided by the client. Applications should precompute as much as possible in the :meth:`begin_auth` method so that this function can quickly return whether the key provided is in the list. If blocking operations need to be performed to determine the validity of the key, this method may be defined as a coroutine. By default, this method returns ``False`` for all client keys. .. note:: This function only needs to report whether the public key provided is a valid client key for this user. If it is, AsyncSSH will verify that the client possesses the corresponding private key before allowing the authentication to succeed. :param string username: The user being authenticated :param key: The public key sent by the client :type key: :class:`SSHKey` *public key* :returns: A boolean indicating if the specified key is a valid client key for the user being authenticated """ return False def validate_ca_key(self, username, key): """Return whether key is an authorized CA key for this user Basic key-based client authentication can be supported by passing authorized keys in the ``authorized_client_keys`` argument of :func:`create_server`, or by calling :meth:`set_authorized_keys ` on the server connection from the :meth:`begin_auth` method. However, for more flexibility in matching on the allowed set of keys, this method can be implemented by the application to do the matching itself. It should return ``True`` if the specified key is a valid certificate authority key for the user being authenticated. This method may be called multiple times with different keys provided by the client. Applications should precompute as much as possible in the :meth:`begin_auth` method so that this function can quickly return whether the key provided is in the list. If blocking operations need to be performed to determine the validity of the key, this method may be defined as a coroutine. By default, this method returns ``False`` for all CA keys. .. note:: This function only needs to report whether the public key provided is a valid CA key for this user. If it is, AsyncSSH will verify that the certificate is valid, that the user is one of the valid principals for the certificate, and that the client possesses the private key corresponding to the public key in the certificate before allowing the authentication to succeed. :param string username: The user being authenticated :param key: The public key which signed the certificate sent by the client :type key: :class:`SSHKey` *public key* :returns: A boolean indicating if the specified key is a valid CA key for the user being authenticated """ return False def password_auth_supported(self): """Return whether or not password authentication is supported This method should return ``True`` if password authentication is supported. Applications wishing to support it must have this method return ``True`` and implement :meth:`validate_password` to return whether or not the password provided by the client is valid for the user being authenticated. By default, this method returns ``False`` indicating that password authentication is not supported. :returns: A boolean indicating if password authentication is supported or not """ return False def validate_password(self, username, password): """Return whether password is valid for this user This method should return ``True`` if the specified password is a valid password for the user being authenticated. It must be overridden by applications wishing to support password authentication. This method may be called multiple times with different passwords provided by the client. Applications may wish to limit the number of attempts which are allowed. This can be done by having :meth:`password_auth_supported` begin returning ``False`` after the maximum number of attempts is exceeded. If blocking operations need to be performed to determine the validity of the password, this method may be defined as a coroutine. By default, this method returns ``False`` for all passwords. :param string username: The user being authenticated :param string password: The password sent by the client :returns: A boolean indicating if the specified password is valid for the user being authenticated """ return False def kbdint_auth_supported(self): """Return whether or not keyboard-interactive authentication is supported This method should return ``True`` if keyboard-interactive authentication is supported. Applications wishing to support it must have this method return ``True`` and implement :meth:`get_kbdint_challenge` and :meth:`validate_kbdint_response` to generate the apporiate challenges and validate the responses for the user being authenticated. :returns: A boolean indicating if keyboard-interactive authentication is supported or not """ return False def get_kbdint_challenge(self, username, lang, submethods): """Return a keyboard-interactive auth challenge This method should return ``True`` if authentication should succeed without any challenge, ``False`` if authentication should fail without any challenge, or an auth challenge consisting of a challenge name, instructions, a language tag, and a list of tuples containing prompt strings and booleans indicating whether input should be echoed when a value is entered for that prompt. If blocking operations need to be performed to determine the challenge to issue, this method may be defined as a coroutine. :param string username: The user being authenticated :param string lang: The language requested by the client for the challenge :param string submethods: A comma-separated list of the types of challenges the client can support, or the empty string if the server should choose :returns: An authentication challenge as described above """ return False def validate_kbdint_response(self, username, responses): """Return whether the keyboard-interactive response is valid for this user This method should validate the keyboard-interactive responses provided and return ``True`` if authentication should succeed with no further challenge, ``False`` if authentication should fail, or an additional auth challenge in the same format returned by :meth:`get_kbdint_challenge`. Any series of challenges can be returned this way. To print a message in the middle of a sequence of challenges without prompting for additional data, a challenge can be returned with an empty list of prompts. After the client acknowledges this message, this function will be called again with an empty list of responses to continue the authentication. If blocking operations need to be performed to determine the validity of the response or the next challenge to issue, this method may be defined as a coroutine. :param string username: The user being authenticated :param responses: A list of responses to the last challenge :type responses: list of strings :returns: ``True``, ``False``, or the next challenge """ return False def session_requested(self): """Handle an incoming session request This method is called when a session open request is received from the client, indicating it wishes to open a channel to be used for running a shell, executing a command, or connecting to a subsystem. If the application wishes to accept the session, it must override this method to return either an :class:`SSHServerSession` object to use to process the data received on the channel or a tuple consisting of an :class:`SSHServerChannel` object created with :meth:`create_server_channel ` and an :class:`SSHServerSession`, if the application wishes to pass non-default arguments when creating the channel. If blocking operations need to be performed before the session can be created, a coroutine which returns an :class:`SSHServerSession` object can be returned instead of the session iself. This can be either returned directly or as a part of a tuple with an :class:`SSHServerChannel` object. To reject this request, this method should return ``False`` to send back a "Session refused" response or raise a :exc:`ChannelOpenError` exception with the reason for the failure. The details of what type of session the client wants to start will be delivered to methods on the :class:`SSHServerSession` object which is returned, along with other information such as environment variables, terminal type, size, and modes. By default, all session requests are rejected. :returns: One of the following: * An :class:`SSHServerSession` object or a coroutine which returns an :class:`SSHServerSession` * A tuple consisting of an :class:`SSHServerChannel` and the above * A callable or coroutine handler function which takes AsyncSSH stream objects for stdin, stdout, and stderr as arguments * A tuple consisting of an :class:`SSHServerChannel` and the above * ``False`` to refuse the request :raises: :exc:`ChannelOpenError` if the session shouldn't be accepted """ return False def connection_requested(self, dest_host, dest_port, orig_host, orig_port): """Handle a direct TCP/IP connection request This method is called when a direct TCP/IP connection request is received by the server. Applications wishing to accept such connections must override this method. To allow standard port forwarding of data on the connection to the requested destination host and port, this method should return ``True``. To reject this request, this method should return ``False`` to send back a "Connection refused" response or raise an :exc:`ChannelOpenError` exception with the reason for the failure. If the application wishes to process the data on the connection itself, this method should return either an :class:`SSHTCPSession` object which can be used to process the data received on the channel or a tuple consisting of of an :class:`SSHTCPChannel` object created with :meth:`create_tcp_channel() ` and an :class:`SSHTCPSession`, if the application wishes to pass non-default arguments when creating the channel. If blocking operations need to be performed before the session can be created, a coroutine which returns an :class:`SSHTCPSession` object can be returned instead of the session iself. This can be either returned directly or as a part of a tuple with an :class:`SSHTCPChannel` object. By default, all connection requests are rejected. :param string dest_host: The address the client wishes to connect to :param integer dest_port: The port the client wishes to connect to :param string orig_host: The address the connection was originated from :param integer orig_port: The port the connection was originated from :returns: One of the following: * An :class:`SSHTCPSession` object or a coroutine which returns an :class:`SSHTCPSession` * A tuple consisting of an :class:`SSHTCPChannel` and the above * A callable or coroutine handler function which takes AsyncSSH stream objects for reading and writing to the connection * A tuple consisting of an :class:`SSHTCPChannel` and the above * ``True`` to request standard port forwarding * ``False`` to refuse the connection :raises: :exc:`ChannelOpenError` if the connection shouldn't be accepted """ return False def server_requested(self, listen_host, listen_port): """Handle a request to listen on a TCP/IP address and port This method is called when a client makes a request to listen on an address and port for incoming TCP connections. The port to listen on may be ``0`` to request a dynamically allocated port. Applications wishing to allow TCP/IP connection forwarding must override this method. To set up standard port forwarding of connections received on this address and port, this method should return ``True``. If the application wishes to manage listening for incoming connections itself, this method should return an :class:`SSHListener` object that listens for new connections and calls :meth:`create_connection ` on each of them to forward them back to the client or returns ``None`` if the listener can't be set up. If blocking operations need to be performed to set up the listener, a coroutine which returns an :class:`SSHListener` can be returned instead of the listener itself. To reject this request, this method should return ``False``. By default, this method rejects all server requests. :param string listen_host: The address the server should listen on :param integer listen_port: The port the server should listen on, or the value ``0`` to request that the server dynamically allocate a port :returns: One of the following: * An :class:`SSHListener` object or a coroutine which returns an :class:`SSHListener` or ``False`` if the listener can't be opened * ``True`` to set up standard port forwarding * ``False`` to reject the request """ return False @asyncio.coroutine def create_connection(client_factory, host, port=_DEFAULT_PORT, *, loop=None, family=0, flags=0, local_addr=None, known_hosts=(), username=None, client_keys=(), password=None, kex_algs=(), encryption_algs=(), mac_algs=(), compression_algs=(), rekey_bytes=_DEFAULT_REKEY_BYTES, rekey_seconds=_DEFAULT_REKEY_SECONDS): """Create an SSH client connection This function is a coroutine which can be run to create an outbound SSH client connection to the specified host and port. When successful, the following steps occur: 1. The connection is established and an :class:`SSHClientConnection` object is created to represent it. 2. The ``client_factory`` is called without arguments and should return an :class:`SSHClient` object. 3. The client object is tied to the connection and its :meth:`connection_made() ` method is called. 4. The SSH handshake and authentication process is initiated, calling methods on the client object if needed. 5. When authentication completes successfully, the client's :meth:`auth_completed() ` method is called. 6. The coroutine returns the ``(connection, client)`` pair. At this point, the connection is ready for sessions to be opened or port forwarding to be set up. If an error occurs, it will be raised as an exception and the partially open connection and client objects will be cleaned up. .. note:: Unlike :func:`socket.create_connection`, asyncio calls to create a connection do not support a ``timeout`` parameter. However, asyncio calls can be wrapped in a call to :func:`asyncio.wait_for` or :func:`asyncio.wait` which takes a timeout and provides equivalent functionality. :param callable client_factory: A callable which returns an :class:`SSHClient` object that will be tied to the connection :param string host: The hostname or address to connect to :param integer port: (optional) The port number to connect to. If not specified, the default SSH port is used. :param loop: (optional) The event loop to use when creating the connection. If not specified, the default event loop is used. :param family: (optional) The address family to use when creating the socket. By default, the address family is automatically selected based on the host. :param flags: (optional) The flags to pass to getaddrinfo() when looking up the host address :param local_addr: (optional) The host and port to bind the socket to before connecting :param known_hosts: (optional) The list of keys which will be used to validate the server host key presented during the SSH handshake. If this is not specified, the keys will be looked up in the file :file:`.ssh/known_hosts`. If this is explicitly set to ``None``, server host key validation will be disabled. :param string username: (optional) Username to authenticate as on the server. If not specified, the currently logged in user on the local machine will be used. :param client_keys: (optional) A list of keys which will be used to authenticate this client via public key authentication. If no client keys are specified, an attempt will be made to load them from the files :file:`.ssh/id_ed25519`, :file:`.ssh/id_ecdsa`, :file:`.ssh/id_rsa`, and :file:`.ssh/id_dsa`, with optional certificates loaded from the files :file:`.ssh/id_ed25519-cert.pub`, :file:`.ssh/id_ecdsa-cert.pub`, :file:`.ssh/id_rsa-cert.pub`, and :file:`.ssh/id_dsa-cert.pub`. If this argument is explicitly set to ``None``, client public key authentication will not be performed. :param string password: (optional) The password to use for client password authentication or keyboard-interactive authentication which prompts for a password. If this is not specified, client password authentication will not be performed. :param kex_algs: (optional) A list of allowed key exchange algorithms in the SSH handshake, taken from :ref:`key exchange algorithms ` :param encryption_algs: (optional) A list of encryption algorithms to use during the SSH handshake, taken from :ref:`encryption algorithms ` :param mac_algs: (optional) A list of MAC algorithms to use during the SSH handshake, taken from :ref:`MAC algorithms ` :param compression_algs: (optional) A list of compression algorithms to use during the SSH handshake, taken from :ref:`compression algorithms `, or ``None`` to disable compression :param integer rekey_bytes: (optional) The number of bytes which can be sent before the SSH session key is renegotiated. This defaults to 1 GB. :param integer rekey_seconds: (optional) The maximum time in seconds before the SSH session key is renegotiated. This defaults to 1 hour. :type family: ``socket.AF_UNSPEC``, ``socket.AF_INET``, or ``socket.AF_INET6`` :type flags: flags to pass to :meth:`getaddrinfo() ` :type local_addr: tuple of string and integer :type known_hosts: *see* :ref:`SpecifyingKnownHosts` :type client_keys: *see* :ref:`SpecifyingPrivateKeys` :type kex_algs: list of strings :type encryption_algs: list of strings :type mac_algs: list of strings :type compression_algs: list of strings :returns: An :class:`SSHClientConnection` and :class:`SSHClient` """ def conn_factory(): """Return an SSH client connection handler""" return SSHClientConnection(client_factory, loop, host, port, known_hosts, username, client_keys, password, kex_algs, encryption_algs, mac_algs, compression_algs, rekey_bytes, rekey_seconds, auth_waiter) if not client_factory: client_factory = SSHClient if not loop: loop = asyncio.get_event_loop() auth_waiter = asyncio.Future(loop=loop) _, conn = yield from loop.create_connection(conn_factory, host, port, family=family, flags=flags, local_addr=local_addr) yield from auth_waiter return conn, conn.get_owner() @asyncio.coroutine def create_server(server_factory, host=None, port=_DEFAULT_PORT, *, loop=None, family=0, flags=socket.AI_PASSIVE, backlog=100, reuse_address=None, server_host_keys, authorized_client_keys=None, kex_algs=(), encryption_algs=(), mac_algs=(), compression_algs=(), allow_pty=True, session_factory=None, session_encoding='utf-8', sftp_factory=None, window=_DEFAULT_WINDOW, max_pktsize=_DEFAULT_MAX_PKTSIZE, rekey_bytes=_DEFAULT_REKEY_BYTES, rekey_seconds=_DEFAULT_REKEY_SECONDS): """Create an SSH server This function is a coroutine which can be run to create an SSH server bound to the specified host and port. The return value is an ``AbstractServer`` object which can be used later to shut down the server. :param callable server_factory: A callable which returns an :class:`SSHServer` object that will be created for each new inbound connection :param string host: (optional) The hostname or address to listen on. If not specified, listeners are created for all addresses. :param integer port: (optional) The port number to listen on. If not specified, the default SSH port is used. :param loop: (optional) The event loop to use when creating the server. If not specified, the default event loop is used. :param family: (optional) The address family to use when creating the server. By default, the address families are automatically selected based on the host. :param flags: (optional) The flags to pass to getaddrinfo() when looking up the host :param integer backlog: (optional) The maximum number of queued connections allowed on listeners :param boolean reuse_address: (optional) Whether or not to reuse a local socket in the TIME_WAIT state without waiting for its natural timeout to expire. If not specified, this will be automatically set to ``True`` on UNIX. :param server_host_keys: A list of private keys and optional certificates which can be used by the server as a host key. This argument must be specified. :param authorized_client_keys: (optional) A list of authorized user and CA public keys which should be trusted for certifcate-based client public key authentication. :param kex_algs: (optional) A list of allowed key exchange algorithms in the SSH handshake, taken from :ref:`key exchange algorithms ` :param encryption_algs: (optional) A list of encryption algorithms to use during the SSH handshake, taken from :ref:`encryption algorithms ` :param mac_algs: (optional) A list of MAC algorithms to use during the SSH handshake, taken from :ref:`MAC algorithms ` :param compression_algs: (optional) A list of compression algorithms to use during the SSH handshake, taken from :ref:`compression algorithms `, or ``None`` to disable compression :param boolean allow_pty: (optional) Whether or not to allow allocation of a pseudo-tty in sessions, defaulting to ``True`` :param callable session_factory: (optional) A callable or coroutine handler function which takes AsyncSSH stream objects for stdin, stdout, and stderr that will be called each time a new shell, exec, or subsytem other than SFTP is requested by the client. If not specified, sessions are rejected by default unless the :meth:`session_requested() ` method is overridden on the :class:`SSHServer` object returned by ``server_factory`` to make this decision. :param string session_encoding: (optional) The Unicode encoding to use for data exchanged on sessions on this server, defaulting to UTF-8 (ISO 10646) format. If ``None`` is passed in, the application can send and receive raw bytes. :param callable sftp_factory: (optional) A callable which returns an :class:`SFTPServer` object that will be created each time an SFTP session is requested by the client, or ``True`` to use the base :class:`SFTPServer` class to handle SFTP requests. If not specified, SFTP sessions are rejected by default. :param integer window: (optional) The receive window size for sessions on this server :param integer max_pktsize: (optional) The maximum packet size for sessions on this server :param integer rekey_bytes: (optional) The number of bytes which can be sent before the SSH session key is renegotiated, defaulting to 1 GB :param integer rekey_seconds: (optional) The maximum time in seconds before the SSH session key is renegotiated, defaulting to 1 hour :type family: ``socket.AF_UNSPEC``, ``socket.AF_INET``, or ``socket.AF_INET6`` :type flags: flags to pass to :meth:`getaddrinfo() ` :type server_host_keys: *see* :ref:`SpecifyingPrivateKeys` :type authorized_client_keys: *see* :ref:`SpecifyingAuthorizedKeys` :type kex_algs: list of strings :type encryption_algs: list of strings :type mac_algs: list of strings :type compression_algs: list of strings :returns: ``AbstractServer`` """ if not server_factory: server_factory = SSHServer if sftp_factory is True: sftp_factory = SFTPServer if not loop: loop = asyncio.get_event_loop() def conn_factory(): """Return an SSH server connection handler""" return SSHServerConnection(server_factory, loop, server_host_keys, authorized_client_keys, kex_algs, encryption_algs, mac_algs, compression_algs, allow_pty, session_factory, session_encoding, sftp_factory, window, max_pktsize, rekey_bytes, rekey_seconds) return (yield from loop.create_server(conn_factory, host, port, family=family, flags=flags, backlog=backlog, reuse_address=reuse_address)) @asyncio.coroutine def connect(host, port=_DEFAULT_PORT, **kwargs): """Make an SSH client connection This function is a coroutine wrapper around :func:`create_connection` which can be used when a custom SSHClient instance is not needed. It takes all the same arguments as :func:`create_connection` except for ``client_factory`` and returns only the :class:`SSHClientConnection` object rather than a tuple of an :class:`SSHClientConnection` and :class:`SSHClient`. When using this call, the following restrictions apply: 1. No callbacks are called when the connection is successfully opened, when it is closed, or when authentication completes. 2. Any authentication information must be provided as arguments to this call, as any authentication callbacks will deny other authentication attempts. Also, authentication banner information will be ignored. 3. Any debug messages sent by the server will be ignored. """ conn, _ = yield from create_connection(None, host, port, **kwargs) return conn @asyncio.coroutine def listen(host, port=_DEFAULT_PORT, *, server_host_keys, **kwargs): """Start an SSH server This function is a coroutine wrapper around :func:`create_server` which can be used when a custom SSHServer instance is not needed. It takes all the same arguments as :func:`create_server` except for ``server_factory``. When using this call, the following restrictions apply: 1. No callbacks are called when a new connection arrives, when a connection is closed, or when authentication completes. 2. Any authentication information must be provided as arguments to this call, as any authentication callbacks will deny other authentication attempts. Currently, this allows only public key authentication to be used, by passing in the ``authorized_client_keys`` argument. 3. Only handlers using the streams API are supported and the same handlers must be used for all clients. These handlers must be provided in the ``session_factory`` and/or ``sftp_factory`` arguments to this call. 4. Any debug messages sent by the client will be ignored. """ return (yield from create_server(None, host, port, server_host_keys=server_host_keys, **kwargs)) asyncssh-1.3.0/asyncssh/constants.py000066400000000000000000000165121260630620200175710ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """SSH constants""" # pylint: disable=bad-whitespace # Default language for error messages DEFAULT_LANG = 'en-US' # SSH message codes MSG_DISCONNECT = 1 MSG_IGNORE = 2 MSG_UNIMPLEMENTED = 3 MSG_DEBUG = 4 MSG_SERVICE_REQUEST = 5 MSG_SERVICE_ACCEPT = 6 MSG_KEXINIT = 20 MSG_NEWKEYS = 21 MSG_KEX_FIRST = 30 MSG_KEX_LAST = 49 MSG_USERAUTH_REQUEST = 50 MSG_USERAUTH_FAILURE = 51 MSG_USERAUTH_SUCCESS = 52 MSG_USERAUTH_BANNER = 53 MSG_USERAUTH_FIRST = 60 MSG_USERAUTH_LAST = 79 MSG_GLOBAL_REQUEST = 80 MSG_REQUEST_SUCCESS = 81 MSG_REQUEST_FAILURE = 82 MSG_CHANNEL_OPEN = 90 MSG_CHANNEL_OPEN_CONFIRMATION = 91 MSG_CHANNEL_OPEN_FAILURE = 92 MSG_CHANNEL_WINDOW_ADJUST = 93 MSG_CHANNEL_DATA = 94 MSG_CHANNEL_EXTENDED_DATA = 95 MSG_CHANNEL_EOF = 96 MSG_CHANNEL_CLOSE = 97 MSG_CHANNEL_REQUEST = 98 MSG_CHANNEL_SUCCESS = 99 MSG_CHANNEL_FAILURE = 100 # SSH disconnect reason codes DISC_HOST_NOT_ALLOWED_TO_CONNECT = 1 DISC_PROTOCOL_ERROR = 2 DISC_KEY_EXCHANGE_FAILED = 3 DISC_RESERVED = 4 DISC_MAC_ERROR = 5 DISC_COMPRESSION_ERROR = 6 DISC_SERVICE_NOT_AVAILABLE = 7 DISC_PROTOCOL_VERSION_NOT_SUPPORTED = 8 DISC_HOST_KEY_NOT_VERIFYABLE = 9 DISC_CONNECTION_LOST = 10 DISC_BY_APPLICATION = 11 DISC_TOO_MANY_CONNECTIONS = 12 DISC_AUTH_CANCELLED_BY_USER = 13 DISC_NO_MORE_AUTH_METHODS_AVAILABLE = 14 DISC_ILLEGAL_USER_NAME = 15 # SSH channel open failure reason codes OPEN_ADMINISTRATIVELY_PROHIBITED = 1 OPEN_CONNECT_FAILED = 2 OPEN_UNKNOWN_CHANNEL_TYPE = 3 OPEN_RESOURCE_SHORTAGE = 4 # Internal failure reason codes OPEN_REQUEST_PTY_FAILED = 0xfffffffe OPEN_REQUEST_SESSION_FAILED = 0xffffffff # SSH file transfer packet types FXP_INIT = 1 FXP_VERSION = 2 FXP_OPEN = 3 FXP_CLOSE = 4 FXP_READ = 5 FXP_WRITE = 6 FXP_LSTAT = 7 FXP_FSTAT = 8 FXP_SETSTAT = 9 FXP_FSETSTAT = 10 FXP_OPENDIR = 11 FXP_READDIR = 12 FXP_REMOVE = 13 FXP_MKDIR = 14 FXP_RMDIR = 15 FXP_REALPATH = 16 FXP_STAT = 17 FXP_RENAME = 18 FXP_READLINK = 19 FXP_SYMLINK = 20 FXP_STATUS = 101 FXP_HANDLE = 102 FXP_DATA = 103 FXP_NAME = 104 FXP_ATTRS = 105 FXP_EXTENDED = 200 FXP_EXTENDED_REPLY = 201 # SSH file transfer open flags FXF_READ = 0x00000001 FXF_WRITE = 0x00000002 FXF_APPEND = 0x00000004 FXF_CREAT = 0x00000008 FXF_TRUNC = 0x00000010 FXF_EXCL = 0x00000020 # SSH file transfer attribute flags FILEXFER_ATTR_SIZE = 0x00000001 FILEXFER_ATTR_UIDGID = 0x00000002 FILEXFER_ATTR_PERMISSIONS = 0x00000004 FILEXFER_ATTR_ACMODTIME = 0x00000008 FILEXFER_ATTR_EXTENDED = 0x80000000 FILEXFER_ATTR_UNDEFINED = 0x7ffffff0 # OpenSSH statvfs attribute flags FXE_STATVFS_ST_RDONLY = 0x1 FXE_STATVFS_ST_NOSUID = 0x2 # SSH file transfer error codes FX_OK = 0 FX_EOF = 1 FX_NO_SUCH_FILE = 2 FX_PERMISSION_DENIED = 3 FX_FAILURE = 4 FX_BAD_MESSAGE = 5 FX_NO_CONNECTION = 6 FX_CONNECTION_LOST = 7 FX_OP_UNSUPPORTED = 8 # SSH channel data type codes EXTENDED_DATA_STDERR = 1 # SSH pty mode opcodes PTY_OP_END = 0 PTY_VINTR = 1 PTY_VQUIT = 2 PTY_VERASE = 3 PTY_VKILL = 4 PTY_VEOF = 5 PTY_VEOL = 6 PTY_VEOL2 = 7 PTY_VSTART = 8 PTY_VSTOP = 9 PTY_VSUSP = 10 PTY_VDSUSP = 11 PTY_VREPRINT = 12 PTY_WERASE = 13 PTY_VLNEXT = 14 PTY_VFLUSH = 15 PTY_VSWTCH = 16 PTY_VSTATUS = 17 PTY_VDISCARD = 18 PTY_IGNPAR = 30 PTY_PARMRK = 31 PTY_INPCK = 32 PTY_ISTRIP = 33 PTY_INLCR = 34 PTY_IGNCR = 35 PTY_ICRNL = 36 PTY_IUCLC = 37 PTY_IXON = 38 PTY_IXANY = 39 PTY_IXOFF = 40 PTY_IMAXBEL = 41 PTY_ISIG = 50 PTY_ICANON = 51 PTY_XCASE = 52 PTY_ECHO = 53 PTY_ECHOE = 54 PTY_ECHOK = 55 PTY_ECHONL = 56 PTY_NOFLSH = 57 PTY_TOSTOP = 58 PTY_IEXTEN = 59 PTY_ECHOCTL = 60 PTY_ECHOKE = 61 PTY_PENDIN = 62 PTY_OPOST = 70 PTY_OLCUC = 71 PTY_ONLCR = 72 PTY_OCRNL = 73 PTY_ONOCR = 74 PTY_ONLRET = 75 PTY_CS7 = 90 PTY_CS8 = 91 PTY_PARENB = 92 PTY_PARODD = 93 PTY_OP_ISPEED = 128 PTY_OP_OSPEED = 129 PTY_OP_RESERVED = 160 asyncssh-1.3.0/asyncssh/crypto/000077500000000000000000000000001260630620200165165ustar00rootroot00000000000000asyncssh-1.3.0/asyncssh/crypto/__init__.py000066400000000000000000000022351260630620200206310ustar00rootroot00000000000000# Copyright (c) 2014-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """A shim for accessing cryptographic primitives needed by asyncssh""" from .cipher import register_cipher, lookup_cipher from .ec import decode_ec_point, encode_ec_point from .ec import get_ec_curve_params, lookup_ec_curve_by_params # Import PyCA versions of DSA, ECDSA, and RSA from .pyca.dsa import DSAPrivateKey, DSAPublicKey from .pyca.ec import ECDSAPrivateKey, ECDSAPublicKey from .pyca.rsa import RSAPrivateKey, RSAPublicKey # Import pyca module to get ciphers defined there registered from . import pyca # Import chacha20-poly1305 cipher if available from . import chacha # Import curve25519 DH if available try: from .curve25519 import Curve25519DH except ImportError: # pragma: no cover pass # Import native Python ECDH module from .ecdh import ECDH asyncssh-1.3.0/asyncssh/crypto/chacha.py000066400000000000000000000103301260630620200202740ustar00rootroot00000000000000# Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Chacha20-Poly1305 symmetric encryption handler""" import ctypes from .cipher import register_cipher class _Chacha20Poly1305Cipher: """Handler for Chacha20-Poly1305 symmetric encryption""" block_size = 1 iv_size = 0 def __init__(self, key): if len(key) != 2 * _CHACHA20_KEYBYTES: raise ValueError('Invalid chacha20-poly1305 key size') self._key = key[:_CHACHA20_KEYBYTES] self._adkey = key[_CHACHA20_KEYBYTES:] @classmethod def new(cls, key, iv=None, initial_bytes=0): """Construct a new chacha20-poly1305 cipher object""" # pylint: disable=unused-argument return cls(key) def _crypt(self, key, data, nonce, ctr=0): """Encrypt/decrypt a block of data""" # pylint: disable=no-self-use datalen = len(data) result = ctypes.create_string_buffer(datalen) datalen = ctypes.c_ulonglong(datalen) ctr = ctypes.c_ulonglong(ctr) if _chacha20_xor_ic(result, data, datalen, nonce, ctr, key) != 0: raise ValueError('Chacha encryption failed') # pragma: no cover return result.raw def _polykey(self, nonce): """Generate a poly1305 key""" polykey = ctypes.create_string_buffer(_POLY1305_KEYBYTES) polykeylen = ctypes.c_ulonglong(_POLY1305_KEYBYTES) if _chacha20(polykey, polykeylen, nonce, self._key) != 0: raise ValueError('Poly1305 key gen failed') # pragma: no cover return polykey def _compute_tag(self, data, nonce): """Compute a poly1305 tag for a block of data""" tag = ctypes.create_string_buffer(_POLY1305_BYTES) datalen = ctypes.c_ulonglong(len(data)) polykey = self._polykey(nonce) if _poly1305(tag, data, datalen, polykey) != 0: raise ValueError('Poly1305 tag gen failed') # pragma: no cover return tag.raw def _verify_tag(self, data, nonce, tag): """Verify a poly1305 tag on a block of data""" datalen = ctypes.c_ulonglong(len(data)) polykey = self._polykey(nonce) return _poly1305_verify(tag, data, datalen, polykey) == 0 def crypt_len(self, data, nonce): """Encrypt/decrypt an SSH packet length value""" if len(nonce) != _CHACHA20_NONCEBYTES: raise ValueError('Invalid chacha20-poly1305 nonce size') return self._crypt(self._adkey, data, nonce) def encrypt_and_sign(self, header, data, nonce): """Encrypt and sign a block of data""" if len(nonce) != _CHACHA20_NONCEBYTES: raise ValueError('Invalid chacha20-poly1305 nonce size') ciphertext = self._crypt(self._key, data, nonce, 1) tag = self._compute_tag(header + ciphertext, nonce) return ciphertext, tag def verify_and_decrypt(self, header, data, nonce, tag): """Verify the signature of and decrypt a block of data""" if len(nonce) != _CHACHA20_NONCEBYTES: raise ValueError('Invalid chacha20-poly1305 nonce size') if self._verify_tag(header + data, nonce, tag): plaintext = self._crypt(self._key, data, nonce, 1) else: plaintext = None return plaintext try: from libnacl import nacl _CHACHA20_KEYBYTES = nacl.crypto_stream_chacha20_keybytes() _CHACHA20_NONCEBYTES = nacl.crypto_stream_chacha20_noncebytes() _chacha20 = nacl.crypto_stream_chacha20 _chacha20_xor_ic = nacl.crypto_stream_chacha20_xor_ic _POLY1305_BYTES = nacl.crypto_onetimeauth_poly1305_bytes() _POLY1305_KEYBYTES = nacl.crypto_onetimeauth_poly1305_keybytes() _poly1305 = nacl.crypto_onetimeauth_poly1305 _poly1305_verify = nacl.crypto_onetimeauth_poly1305_verify except (ImportError, OSError, AttributeError): # pragma: no cover pass else: register_cipher('chacha20-poly1305', 'chacha', _Chacha20Poly1305Cipher) asyncssh-1.3.0/asyncssh/crypto/cipher.py000066400000000000000000000020051260630620200203370ustar00rootroot00000000000000# Copyright (c) 2014-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """A shim for accessing symmetric ciphers needed by asyncssh""" _ciphers = {} def register_cipher(cipher_name, mode_name, cipher): """Register a symmetric cipher If multiple modules try to register the same cipher and mode, the first one to register it is used. """ if (cipher_name, mode_name) not in _ciphers: # pragma: no branch cipher.cipher_name = cipher_name cipher.mode_name = mode_name _ciphers[cipher_name, mode_name] = cipher def lookup_cipher(cipher_name, mode_name): """Look up a symmetric cipher""" return _ciphers.get((cipher_name, mode_name)) asyncssh-1.3.0/asyncssh/crypto/curve25519.py000066400000000000000000000037131260630620200206260ustar00rootroot00000000000000# Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Curve25519 key exchange handler primitives""" import ctypes import os _found = None try: from libnacl import nacl _CURVE25519_BYTES = nacl.crypto_scalarmult_curve25519_bytes() _CURVE25519_SCALARBYTES = nacl.crypto_scalarmult_curve25519_scalarbytes() _curve25519 = nacl.crypto_scalarmult_curve25519 _curve25519_base = nacl.crypto_scalarmult_curve25519_base except (ImportError, OSError, AttributeError): # pragma: no cover pass else: class Curve25519DH: """Curve25519 Diffie Hellman implementation""" def __init__(self): self._private = os.urandom(_CURVE25519_SCALARBYTES) def get_public(self): """Return the public key to send in the handshake""" public = ctypes.create_string_buffer(_CURVE25519_BYTES) if _curve25519_base(public, self._private) != 0: # This error is never returned by libsodium raise ValueError('Curve25519 failed') # pragma: no cover return public.raw def get_shared(self, peer_public): """Return the shared key from the peer's public key""" if len(peer_public) != _CURVE25519_BYTES: raise AssertionError('Invalid curve25519 public key size') shared = ctypes.create_string_buffer(_CURVE25519_BYTES) if _curve25519(shared, self._private, peer_public) != 0: # This error is never returned by libsodium raise ValueError('Curve25519 failed') # pragma: no cover return int.from_bytes(shared.raw, 'big') asyncssh-1.3.0/asyncssh/crypto/ec.py000066400000000000000000000210231260630620200174550ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Elliptic curve public key encryption primitives""" from ..misc import mod_inverse _curve_params = {} _curve_param_map = {} # Short variable names are used here, matching names in the spec # pylint: disable=invalid-name class PrimeCurve: """An elliptic curve over a prime finite field F(p)""" def __init__(self, p, a, b): self.p = p self.a = a self.b = b self.keylen = (p.bit_length() + 7) // 8 def __eq__(self, other): return (isinstance(other, self.__class__) and self.p == other.p and self.a % self.p == other.a % other.p and self.b % self.p == other.b % other.p) def __hash__(self): return hash((self.p, self.a % self.p, self.b % self.p)) class PrimePoint: """A point on an elliptic curve over a prime finite field F(p)""" def __init__(self, curve, x, y): self.curve = curve self.x = x self.y = y def __eq__(self, other): return (isinstance(other, self.__class__) and self.curve == other.curve and self.x == other.x and self.y == other.y) def __hash__(self): return hash((self.curve, self.x, self.y)) def __bool__(self): return self.curve is not None def __neg__(self): """Negate an elliptic curve point""" if self.y: return PrimePoint(self.curve, self.x, self.curve.p - self.y) else: return self def __add__(self, other): """Add two elliptic curve points""" if self.curve is None: return other elif other.curve is None: return self elif self.curve != other.curve: raise ValueError('Can\'t add points from different curves') p = self.curve.p if self.x == other.x: if (self.y + other.y) % p == 0: return _INFINITY else: l = ((3 * self.x * self.x + self.curve.a) * mod_inverse(2 * self.y, p)) % p else: l = ((other.y - self.y) * mod_inverse(other.x - self.x, p)) % p x = (l * l - self.x - other.x) % p y = (l * (self.x - x) - self.y) % p return PrimePoint(self.curve, x, y) def __sub__(self, other): """Subtract one elliptic curve point from another""" return self + (-other) def __rmul__(self, k): """Multiply an elliptic curve point by a scalar value""" result = _INFINITY P = self while k: if k & 1: if k & 2: result -= P while k & 2: k >>= 1 P += P k |= 2 else: result += P k >>= 1 P += P return result @classmethod def construct(cls, curve, x, y): """Construct an elliptic curve point from a curve and x, y values""" if x is None and y is None: return _INFINITY elif (0 <= x < curve.p and 0 <= y < curve.p and (y*y - (x*x*x + curve.a*x + curve.b)) % curve.p == 0): return cls(curve, x, y) else: raise ValueError('Point not on curve') @classmethod def decode(cls, curve, data): """Decode an octet string into an elliptic curve point""" return cls.construct(curve, *decode_ec_point(curve.keylen, data)) def encode(self): """Encode an elliptic curve point as an octet string""" return encode_ec_point(self.curve.keylen, self.x, self.y) # Define the point "infinity" which exists on all elliptic curves _INFINITY = PrimePoint(None, None, None) def register_prime_curve(curve_id, p, a, b, Gx, Gy, n): """Register an elliptic curve prime domain This function registers an elliptic curve prime domain by specifying the SSH identifier for the curve, the OID used to identify the curve in PKCS#1 and PKCS#8 private and public keys, and the set of parameters describing the curve, generator point, and order. """ if p % 2 == 0 or (4*a*a*a + 27*b*b) % p == 0: raise ValueError('Invalid curve parameters') G = PrimePoint.construct(PrimeCurve(p, a, b), Gx, Gy) if n * G: raise ValueError('Invalid order for curve %s' % curve_id.decode()) pb = p % n for b in range(100): if pb == 1: raise ValueError('Invalid prime for curve %s' % curve_id.decode()) pb = (pb * p) % n _curve_params[curve_id] = (G, n) _curve_param_map[G, n] = curve_id def decode_ec_point(keylen, data): """Decode an octet string into an elliptic curve point""" if data == b'\x00': return None, None elif data.startswith(b'\x04'): if len(data) == 2*keylen + 1: return (int.from_bytes(data[1:keylen+1], 'big'), int.from_bytes(data[keylen+1:], 'big')) else: raise ValueError('Invalid elliptic curve point data length') else: raise ValueError('Unsupported elliptic curve point type') def encode_ec_point(keylen, x, y): """Encode an elliptic curve point as an octet string""" if x is None: return b'\x00' else: return b'\x04' + x.to_bytes(keylen, 'big') + y.to_bytes(keylen, 'big') def get_ec_curve_params(curve_id): """Return the parameters for a named elliptic curve This function looks up an elliptic curve by name and returns the curve's generator point and order. """ try: return _curve_params[curve_id] except KeyError: raise ValueError('Unknown EC curve %s' % curve_id.decode()) def lookup_ec_curve_by_params(p, a, b, point, n): """Look up an elliptic curve by its parameters This function looks up an elliptic curve by its parameters and returns the curve's name. """ try: G = PrimePoint.decode(PrimeCurve(p, a, b), point) return _curve_param_map[G, n] except (KeyError, ValueError): raise ValueError('Unknown elliptic curve parameters') # pylint: disable=line-too-long register_prime_curve(b'nistp521', 6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057151, -3, 0x051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00, 0xc6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66, 0x11839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650, 6864797660130609714981900799081393217269435300143305409394463459185543183397655394245057746333217197532963996371363321113864768612440380340372808892707005449) register_prime_curve(b'nistp384', 39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112319, -3, 0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef, 0xaa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7, 0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f, 39402006196394479212279040100143613805079739270465446667946905279627659399113263569398956308152294913554433653942643) register_prime_curve(b'nistp256', 115792089210356248762697446949407573530086143415290314195533631308867097853951, -3, 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b, 0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296, 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5, 115792089210356248762697446949407573529996955224135760342422259061068512044369) asyncssh-1.3.0/asyncssh/crypto/ecdh.py000066400000000000000000000025221260630620200177740ustar00rootroot00000000000000# Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Elliptic curve Diffie-Hellman key exchange handler primitives""" from ..misc import randrange from .ec import get_ec_curve_params, PrimePoint # Short variable names are used here, matching names in the spec # pylint: disable=invalid-name class ECDH: """Elliptic curve Diffie-Hellman implementation""" def __init__(self, curve_id): G, n = get_ec_curve_params(curve_id) while True: self._d = randrange(2, n) self._Q = self._d * G if self._Q: # pragma: no branch break def get_public(self): """Return the public key to send in the handshake""" return self._Q.encode() def get_shared(self, peer_public): """Return the shared key from the peer's public key""" P = self._d * PrimePoint.decode(self._Q.curve, peer_public) if not P: # pragma: no cover raise ValueError('ECDH multiplication failed') return P.x asyncssh-1.3.0/asyncssh/crypto/pyca/000077500000000000000000000000001260630620200174525ustar00rootroot00000000000000asyncssh-1.3.0/asyncssh/crypto/pyca/__init__.py000066400000000000000000000007531260630620200215700ustar00rootroot00000000000000# Copyright (c) 2014-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """A shim around PyCA for accessing cryptographic primitives""" from . import cipher asyncssh-1.3.0/asyncssh/crypto/pyca/cipher.py000066400000000000000000000112311260630620200212740ustar00rootroot00000000000000# Copyright (c) 2014-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """A shim around PyCA for symmetric encryption""" from ..cipher import register_cipher from cryptography.exceptions import InvalidTag from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.hazmat.primitives.ciphers.algorithms import AES, ARC4 from cryptography.hazmat.primitives.ciphers.algorithms import Blowfish, CAST5 from cryptography.hazmat.primitives.ciphers.algorithms import TripleDES from cryptography.hazmat.primitives.ciphers.modes import CBC, CTR, GCM # pylint: disable=bad-whitespace _ciphers = {'aes': (AES, {'cbc': CBC, 'ctr': CTR, 'gcm': GCM}), 'arc4': (ARC4, {None: None}), 'blowfish': (Blowfish, {'cbc': CBC}), 'cast': (CAST5, {'cbc': CBC}), 'des': (TripleDES, {'cbc': CBC}), 'des3': (TripleDES, {'cbc': CBC})} # pylint: enable=bad-whitespace class GCMShim: """Shim for PyCA AES-GCM ciphers""" def __init__(self, cipher, block_size, key, iv): self._cipher = cipher self._key = key self._iv = iv self.block_size = block_size def _update_iv(self): """Update the IV after each encrypt/decrypt operation""" invocation = int.from_bytes(self._iv[4:], 'big') invocation = (invocation + 1) & 0xffffffffffffffff self._iv = self._iv[:4] + invocation.to_bytes(8, 'big') def encrypt_and_sign(self, header, data): """Encrypt and sign a block of data""" encryptor = Cipher(self._cipher(self._key), GCM(self._iv), default_backend()).encryptor() if header: encryptor.authenticate_additional_data(header) ciphertext = encryptor.update(data) + encryptor.finalize() self._update_iv() return ciphertext, encryptor.tag def verify_and_decrypt(self, header, data, tag): """Verify the signature of and decrypt a block of data""" decryptor = Cipher(self._cipher(self._key), GCM(self._iv, tag), default_backend()).decryptor() if header: decryptor.authenticate_additional_data(header) try: plaintext = decryptor.update(data) + decryptor.finalize() except InvalidTag: plaintext = None self._update_iv() return plaintext class CipherShim: """Shim for other PyCA ciphers""" def __init__(self, cipher, mode, block_size, key, iv, initial_bytes): if mode: mode = mode(iv) self._cipher = Cipher(cipher(key), mode, default_backend()) self._initial_bytes = initial_bytes self._encryptor = None self._decryptor = None self.block_size = block_size self.mode_name = None # set by register_cipher() def encrypt(self, data): """Encrypt a block of data""" if not self._encryptor: self._encryptor = self._cipher.encryptor() if self._initial_bytes: self._encryptor.update(self._initial_bytes * b'\0') return self._encryptor.update(data) def decrypt(self, data): """Decrypt a block of data""" if not self._decryptor: self._decryptor = self._cipher.decryptor() if self._initial_bytes: self._decryptor.update(self._initial_bytes * b'\0') return self._decryptor.update(data) class CipherFactory: """A factory which returns shims for PyCA symmetric encryption""" def __init__(self, cipher, mode): self._cipher = cipher self._mode = mode self.block_size = 1 if cipher == ARC4 else cipher.block_size // 8 self.iv_size = 12 if mode == GCM else self.block_size def new(self, key, iv=None, initial_bytes=0): """Construct a new symmetric cipher object""" if self._mode == GCM: return GCMShim(self._cipher, self.block_size, key, iv) else: return CipherShim(self._cipher, self._mode, self.block_size, key, iv, initial_bytes) for _cipher_name, (_cipher, _modes) in _ciphers.items(): for _mode_name, _mode in _modes.items(): register_cipher(_cipher_name, _mode_name, CipherFactory(_cipher, _mode)) asyncssh-1.3.0/asyncssh/crypto/pyca/dsa.py000066400000000000000000000047311260630620200206000ustar00rootroot00000000000000# Copyright (c) 2014-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """A shim around PyCA for DSA public and private keys""" from ...asn1 import der_encode, der_decode from cryptography.exceptions import InvalidSignature from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.hashes import SHA1 from cryptography.hazmat.primitives.asymmetric import dsa # Short variable names are used here, matching names in the spec # pylint: disable=invalid-name class _DSAKey: """Base class for shim around PyCA for DSA keys""" def __init__(self, p, q, g, y, x=None): self._params = dsa.DSAParameterNumbers(p, q, g) self._pub = dsa.DSAPublicNumbers(y, self._params) if x: self._priv = dsa.DSAPrivateNumbers(x, self._pub) self._priv_key = self._priv.private_key(default_backend()) else: self._priv = None self._pub_key = self._pub.public_key(default_backend()) @property def p(self): """Return the DSA public modulus""" return self._params.p @property def q(self): """Return the DSA sub-group order""" return self._params.q @property def g(self): """Return the DSA generator""" return self._params.g @property def y(self): """Return the DSA public value""" return self._pub.y @property def x(self): """Return the DSA private value""" return self._priv.x if self._priv else None class DSAPrivateKey(_DSAKey): """A shim around PyCA for DSA private keys""" def sign(self, data): """Sign a block of data""" signer = self._priv_key.signer(SHA1()) signer.update(data) return der_decode(signer.finalize()) class DSAPublicKey(_DSAKey): """A shim around PyCA for DSA public keys""" def verify(self, data, sig): """Verify the signature on a block of data""" verifier = self._pub_key.verifier(der_encode(sig), SHA1()) verifier.update(data) try: verifier.verify() return True except InvalidSignature: return False asyncssh-1.3.0/asyncssh/crypto/pyca/ec.py000066400000000000000000000070031260630620200204130ustar00rootroot00000000000000# Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """A shim around PyCA for elliptic curve keys and key exchange""" from ...asn1 import der_encode, der_decode from ..ec import decode_ec_point, encode_ec_point from cryptography.exceptions import InvalidSignature from cryptography.hazmat.backends.openssl import backend from cryptography.hazmat.primitives.hashes import SHA256, SHA384, SHA512 from cryptography.hazmat.primitives.asymmetric import ec # Short variable names are used here, matching names in the spec # pylint: disable=invalid-name _curves = {b'nistp256': (ec.SECP256R1, SHA256), b'nistp384': (ec.SECP384R1, SHA384), b'nistp521': (ec.SECP521R1, SHA512)} class _ECKey: """Base class for shim around PyCA for EC keys""" def __init__(self, curve_id, public_value=None, private_value=None): try: curve, hash_alg = _curves[curve_id] except KeyError: # pragma: no cover, other curves not registered raise ValueError('Unknown EC curve %s' % curve_id.decode()) from None self._curve_id = curve_id self._keylen = (curve.key_size + 7) // 8 self._hash_alg = hash_alg x, y = decode_ec_point(self._keylen, public_value) self._pub = ec.EllipticCurvePublicNumbers(x, y, curve()) if private_value: self._priv = ec.EllipticCurvePrivateNumbers(private_value, self._pub) self._priv_key = self._priv.private_key(backend) else: self._priv = None self._pub_key = self._pub.public_key(backend) @property def curve_id(self): """Return the EC curve name""" return self._curve_id @property def x(self): """Return the EC public x coordinate""" return self._pub.x @property def y(self): """Return the EC public y coordinate""" return self._pub.y @property def d(self): """Return the EC private value as an integer""" return self._priv.private_value if self._priv else None @property def public_value(self): """Return the EC public point value encoded as a byte string""" return encode_ec_point(self._keylen, self.x, self.y) @property def private_value(self): """Return the EC private value encoded as a byte string""" return self.d.to_bytes(self._keylen, 'big') if self.d else None class ECDSAPrivateKey(_ECKey): """A shim around PyCA for ECDSA private keys""" def sign(self, data): """Sign a block of data""" signer = self._priv_key.signer(ec.ECDSA(self._hash_alg())) signer.update(data) return der_decode(signer.finalize()) class ECDSAPublicKey(_ECKey): """A shim around PyCA for ECDSA public keys""" def verify(self, data, sig): """Verify the signature on a block of data""" verifier = self._pub_key.verifier(der_encode(sig), ec.ECDSA(self._hash_alg())) verifier.update(data) try: verifier.verify() return True except InvalidSignature: return False asyncssh-1.3.0/asyncssh/crypto/pyca/rsa.py000066400000000000000000000057661260630620200206270ustar00rootroot00000000000000# Copyright (c) 2014-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """A shim around PyCA for RSA public and private keys""" from cryptography.exceptions import InvalidSignature from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 from cryptography.hazmat.primitives.hashes import SHA1 from cryptography.hazmat.primitives.asymmetric import rsa # Short variable names are used here, matching names in the spec # pylint: disable=invalid-name class _RSAKey: """Base class for shum around PyCA for RSA keys""" def __init__(self, n, e, d=None, p=None, q=None, dmp1=None, dmq1=None, iqmp=None): self._pub = rsa.RSAPublicNumbers(e, n) if d: self._priv = rsa.RSAPrivateNumbers(p, q, d, dmp1, dmq1, iqmp, self._pub) self._priv_key = self._priv.private_key(default_backend()) else: self._priv = None self._pub_key = self._pub.public_key(default_backend()) @property def n(self): """Return the RSA public modulus""" return self._pub.n @property def e(self): """Return the RSA public exponent""" return self._pub.e @property def d(self): """Return the RSA private exponent""" return self._priv.d if self._priv else None @property def p(self): """Return the RSA first private prime""" return self._priv.p if self._priv else None @property def q(self): """Return the RSA second private prime""" return self._priv.q if self._priv else None @property def dmp1(self): """Return d modulo p-1""" return self._priv.dmp1 if self._priv else None @property def dmq1(self): """Return q modulo p-1""" return self._priv.dmq1 if self._priv else None @property def iqmp(self): """Return the inverse of q modulo p""" return self._priv.iqmp if self._priv else None class RSAPrivateKey(_RSAKey): """A shim around PyCA for RSA private keys""" def sign(self, data): """Sign a block of data""" signer = self._priv_key.signer(PKCS1v15(), SHA1()) signer.update(data) return signer.finalize() class RSAPublicKey(_RSAKey): """A shim around PyCA for RSA public keys""" def verify(self, data, sig): """Verify the signature on a block of data""" verifier = self._pub_key.verifier(sig, PKCS1v15(), SHA1()) verifier.update(data) try: verifier.verify() return True except InvalidSignature: return False asyncssh-1.3.0/asyncssh/dh.py000066400000000000000000000234511260630620200161500ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """SSH Diffie-Hellman key exchange handlers""" from hashlib import sha1, sha256 from .constants import DISC_KEY_EXCHANGE_FAILED, DISC_PROTOCOL_ERROR from .kex import Kex, register_kex_alg from .misc import DisconnectError, randrange from .packet import Byte, MPInt, String, UInt32 # pylint: disable=bad-whitespace,line-too-long # SSH KEX DH message values MSG_KEXDH_INIT = 30 MSG_KEXDH_REPLY = 31 # SSH KEX DH group exchange message values MSG_KEX_DH_GEX_REQUEST_OLD = 30 MSG_KEX_DH_GEX_GROUP = 31 MSG_KEX_DH_GEX_INIT = 32 MSG_KEX_DH_GEX_REPLY = 33 MSG_KEX_DH_GEX_REQUEST = 34 # SSH KEX group exchange key sizes KEX_DH_GEX_MIN_SIZE = 1024 KEX_DH_GEX_PREFERRED_SIZE = 2048 KEX_DH_GEX_MAX_SIZE = 8192 # SSH Diffie-Hellman group 1 parameters _group1_g = 2 _group1_p = 0xffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece65381ffffffffffffffff # SSH Diffie-Hellman group 14 parameters _group14_g = 2 _group14_p = 0xffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff # pylint: enable=bad-whitespace,line-too-long # Short variable names are used here, matching names in the spec # pylint: disable=invalid-name class _KexDHBase(Kex): """Abstract base class for Diffie-Hellman key exchange""" _replytype = None def __init__(self, alg, conn, hash_alg): super().__init__(alg, conn, hash_alg) self._g = None self._p = None self._q = None self._x = None self._e = None self._f = None def _compute_hash(self, host_key_data, k): """Abstract method for computing connection hash""" # Provided by subclass raise NotImplementedError def _send_init(self, pkttype): """Send a DH init message""" self._x = randrange(2, self._q) self._e = pow(self._g, self._x, self._p) self._conn.send_packet(Byte(pkttype), MPInt(self._e)) def _send_reply(self, pkttype): """Send a DH reply message""" if not 1 <= self._e < self._p: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Kex DH e out of range') y = randrange(2, self._q) self._f = pow(self._g, y, self._p) k = pow(self._e, y, self._p) if k < 1: # pragma: no cover, shouldn't be possible with valid p raise DisconnectError(DISC_PROTOCOL_ERROR, 'Kex DH k out of range') host_key, host_key_data = self._conn.get_server_host_key() h = self._compute_hash(host_key_data, k) sig = host_key.sign(h) self._conn.send_packet(Byte(pkttype), String(host_key_data), MPInt(self._f), String(sig)) self._conn.send_newkeys(k, h) def _verify_reply(self, host_key_data, sig): """Verify a DH reply message""" if not 1 <= self._f < self._p: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Kex DH f out of range') host_key = self._conn.validate_server_host_key(host_key_data) k = pow(self._f, self._x, self._p) if k < 1: # pragma: no cover, shouldn't be possible with valid p raise DisconnectError(DISC_PROTOCOL_ERROR, 'Kex DH k out of range') h = self._compute_hash(host_key_data, k) if not host_key.verify(h, sig): raise DisconnectError(DISC_KEY_EXCHANGE_FAILED, 'Key exchange hash mismatch') self._conn.send_newkeys(k, h) def _process_init(self, pkttype, packet): """Process a DH init message""" # pylint: disable=unused-argument if self._conn.is_client() or not self._p: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Unexpected kex init msg') self._e = packet.get_mpint() packet.check_end() self._send_reply(self._replytype) def _process_reply(self, pkttype, packet): """Process a DH reply message""" # pylint: disable=unused-argument if self._conn.is_server() or not self._p: raise DisconnectError(DISC_PROTOCOL_ERROR, 'Unexpected kex reply msg') host_key = packet.get_string() self._f = packet.get_mpint() sig = packet.get_string() packet.check_end() self._verify_reply(host_key, sig) class _KexDH(_KexDHBase): """Handler for Diffie-Hellman key exchange""" _replytype = MSG_KEXDH_REPLY def __init__(self, alg, conn, hash_alg, g, p): super().__init__(alg, conn, hash_alg) self._g = g self._p = p self._q = (p - 1) // 2 if conn.is_client(): self._send_init(MSG_KEXDH_INIT) def _compute_hash(self, host_key_data, k): """Compute a hash of key information associated with the connection""" hash_obj = self._hash_alg() hash_obj.update(self._conn.get_hash_prefix()) hash_obj.update(String(host_key_data)) hash_obj.update(MPInt(self._e)) hash_obj.update(MPInt(self._f)) hash_obj.update(MPInt(k)) return hash_obj.digest() packet_handlers = { MSG_KEXDH_INIT: _KexDHBase._process_init, MSG_KEXDH_REPLY: _KexDHBase._process_reply } class _KexDHGex(_KexDHBase): """Handler for Diffie-Hellman group exchange""" _replytype = MSG_KEX_DH_GEX_REPLY def __init__(self, alg, conn, hash_alg, old=False, preferred_size=0): super().__init__(alg, conn, hash_alg) if conn.is_client(): if old: # Send old request message for unit test self._request = UInt32(preferred_size) conn.send_packet(Byte(MSG_KEX_DH_GEX_REQUEST_OLD), self._request) else: self._request = (UInt32(KEX_DH_GEX_MIN_SIZE) + UInt32(KEX_DH_GEX_PREFERRED_SIZE) + UInt32(KEX_DH_GEX_MAX_SIZE)) conn.send_packet(Byte(MSG_KEX_DH_GEX_REQUEST), self._request) def _compute_hash(self, host_key_data, k): """Compute a hash of key information associated with the connection""" hash_obj = self._hash_alg() hash_obj.update(self._conn.get_hash_prefix()) hash_obj.update(String(host_key_data)) hash_obj.update(self._request) hash_obj.update(MPInt(self._p)) hash_obj.update(MPInt(self._g)) hash_obj.update(MPInt(self._e)) hash_obj.update(MPInt(self._f)) hash_obj.update(MPInt(k)) return hash_obj.digest() def _process_request(self, pkttype, packet): """Process a DH gex request message""" if self._conn.is_client(): raise DisconnectError(DISC_PROTOCOL_ERROR, 'Unexpected kex request msg') self._request = packet.get_remaining_payload() # The min/max sizes will be needed to fully implement DHGex # pylint: disable=unused-variable if pkttype == MSG_KEX_DH_GEX_REQUEST_OLD: min_size = KEX_DH_GEX_MIN_SIZE requested_size = packet.get_uint32() max_size = KEX_DH_GEX_MAX_SIZE else: min_size = packet.get_uint32() requested_size = packet.get_uint32() max_size = packet.get_uint32() packet.check_end() # TODO: For now, just select between group1 and group14 primes # based on the requested group size if requested_size <= 1024: self._p, self._g = _group1_p, _group1_g else: self._p, self._g = _group14_p, _group14_g self._q = (self._p - 1) // 2 self._conn.send_packet(Byte(MSG_KEX_DH_GEX_GROUP), MPInt(self._p), MPInt(self._g)) def _process_group(self, pkttype, packet): """Process a DH gex group message""" # pylint: disable=unused-argument if self._conn.is_server(): raise DisconnectError(DISC_PROTOCOL_ERROR, 'Unexpected kex group msg') self._p = packet.get_mpint() self._g = packet.get_mpint() packet.check_end() self._q = (self._p - 1) // 2 self._send_init(MSG_KEX_DH_GEX_INIT) packet_handlers = { MSG_KEX_DH_GEX_REQUEST_OLD: _process_request, MSG_KEX_DH_GEX_GROUP: _process_group, MSG_KEX_DH_GEX_INIT: _KexDHBase._process_init, MSG_KEX_DH_GEX_REPLY: _KexDHBase._process_reply, MSG_KEX_DH_GEX_REQUEST: _process_request } # pylint: disable=bad-whitespace register_kex_alg(b'diffie-hellman-group-exchange-sha256', _KexDHGex, sha256) register_kex_alg(b'diffie-hellman-group-exchange-sha1', _KexDHGex, sha1) register_kex_alg(b'diffie-hellman-group14-sha1', _KexDH, sha1, _group14_g, _group14_p) register_kex_alg(b'diffie-hellman-group1-sha1', _KexDH, sha1, _group1_g, _group1_p) asyncssh-1.3.0/asyncssh/dsa.py000066400000000000000000000152311260630620200163210ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """DSA public key encryption handler""" from .asn1 import ASN1DecodeError, ObjectIdentifier, der_encode, der_decode from .crypto import DSAPrivateKey, DSAPublicKey from .misc import all_ints from .packet import MPInt, String, PacketDecodeError, SSHPacket from .public_key import SSHKey, SSHCertificateV00, SSHCertificateV01 from .public_key import KeyExportError from .public_key import register_public_key_alg, register_certificate_alg # Short variable names are used here, matching names in the spec # pylint: disable=invalid-name class _DSAKey(SSHKey): """Handler for DSA public key encryption""" algorithm = b'ssh-dss' pem_name = b'DSA' pkcs8_oid = ObjectIdentifier('1.2.840.10040.4.1') def __init__(self, key): self._key = key def __eq__(self, other): # This isn't protected access - both objects are _DSAKey instances # pylint: disable=protected-access return (isinstance(other, self.__class__) and self._key.p == other._key.p and self._key.q == other._key.q and self._key.g == other._key.g and self._key.y == other._key.y and self._key.x == other._key.x) def __hash__(self): return hash((self._key.p, self._key.q, self._key.g, self._key.y, self._key.x)) @classmethod def make_private(cls, *args): """Construct a DSA private key""" return cls(DSAPrivateKey(*args)) @classmethod def make_public(cls, *args): """Construct a DSA public key""" return cls(DSAPublicKey(*args)) @classmethod def decode_pkcs1_private(cls, key_data): """Decode a PKCS#1 format DSA private key""" if (isinstance(key_data, tuple) and len(key_data) == 6 and all_ints(key_data) and key_data[0] == 0): return key_data[1:] else: return None @classmethod def decode_pkcs1_public(cls, key_data): """Decode a PKCS#1 format DSA public key""" if (isinstance(key_data, tuple) and len(key_data) == 4 and all_ints(key_data)): y, p, q, g = key_data return p, q, g, y else: return None @classmethod def decode_pkcs8_private(cls, alg_params, data): """Decode a PKCS#8 format DSA private key""" try: x = der_decode(data) except ASN1DecodeError: return None if (isinstance(alg_params, tuple) and len(alg_params) == 3 and all_ints(alg_params) and isinstance(x, int)): p, q, g = alg_params y = pow(g, x, p) return p, q, g, y, x else: return None @classmethod def decode_pkcs8_public(cls, alg_params, data): """Decode a PKCS#8 format DSA public key""" try: y = der_decode(data) except ASN1DecodeError: return None if (isinstance(alg_params, tuple) and len(alg_params) == 3 and all_ints(alg_params) and isinstance(y, int)): p, q, g = alg_params return p, q, g, y else: return None @classmethod def decode_ssh_private(cls, packet): """Decode an SSH format DSA private key""" p = packet.get_mpint() q = packet.get_mpint() g = packet.get_mpint() y = packet.get_mpint() x = packet.get_mpint() return p, q, g, y, x @classmethod def decode_ssh_public(cls, packet): """Decode an SSH format DSA public key""" p = packet.get_mpint() q = packet.get_mpint() g = packet.get_mpint() y = packet.get_mpint() return p, q, g, y def encode_pkcs1_private(self): """Encode a PKCS#1 format DSA private key""" if not self._key.x: raise KeyExportError('Key is not private') return (0, self._key.p, self._key.q, self._key.g, self._key.y, self._key.x) def encode_pkcs1_public(self): """Encode a PKCS#1 format DSA public key""" return (self._key.y, self._key.p, self._key.q, self._key.g) def encode_pkcs8_private(self): """Encode a PKCS#8 format DSA private key""" if not self._key.x: raise KeyExportError('Key is not private') return (self._key.p, self._key.q, self._key.g), der_encode(self._key.x) def encode_pkcs8_public(self): """Encode a PKCS#8 format DSA public key""" return (self._key.p, self._key.q, self._key.g), der_encode(self._key.y) def encode_ssh_private(self): """Encode an SSH format DSA private key""" if not self._key.x: raise KeyExportError('Key is not private') return b''.join((MPInt(self._key.p), MPInt(self._key.q), MPInt(self._key.g), MPInt(self._key.y), MPInt(self._key.x))) def encode_ssh_public(self): """Encode an SSH format DSA public key""" return b''.join((MPInt(self._key.p), MPInt(self._key.q), MPInt(self._key.g), MPInt(self._key.y))) def sign(self, data): """Return a signature of the specified data using this key""" if not self._key.x: raise ValueError('Private key needed for signing') r, s = self._key.sign(data) return b''.join((String(self.algorithm), String(r.to_bytes(20, 'big') + s.to_bytes(20, 'big')))) def verify(self, data, sig): """Verify a signature of the specified data using this key""" try: packet = SSHPacket(sig) if packet.get_string() != self.algorithm: return False sig = packet.get_string() packet.check_end() if len(sig) != 40: return False r = int.from_bytes(sig[:20], 'big') s = int.from_bytes(sig[20:], 'big') return self._key.verify(data, (r, s)) except PacketDecodeError: return False register_public_key_alg(b'ssh-dss', _DSAKey) register_certificate_alg(b'ssh-dss-cert-v01@openssh.com', _DSAKey, SSHCertificateV01) register_certificate_alg(b'ssh-dss-cert-v00@openssh.com', _DSAKey, SSHCertificateV00) asyncssh-1.3.0/asyncssh/ecdh.py000066400000000000000000000104171260630620200164560ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Elliptic curve Diffie-Hellman key exchange handler""" from hashlib import sha256, sha384, sha512 from .constants import DISC_KEY_EXCHANGE_FAILED, DISC_PROTOCOL_ERROR from .kex import Kex, register_kex_alg from .misc import DisconnectError from .packet import Byte, MPInt, String # pylint: disable=bad-whitespace # SSH KEX ECDH message values MSG_KEX_ECDH_INIT = 30 MSG_KEX_ECDH_REPLY = 31 # pylint: enable=bad-whitespace class _KexECDH(Kex): """Handler for elliptic curve Diffie-Hellman key exchange""" def __init__(self, alg, conn, hash_alg, ecdh_class, *args): super().__init__(alg, conn, hash_alg) self._priv = ecdh_class(*args) pub = self._priv.get_public() if conn.is_client(): self._client_pub = pub self._conn.send_packet(Byte(MSG_KEX_ECDH_INIT), String(pub)) else: self._server_pub = pub def _compute_hash(self, host_key_data, k): """Compute a hash of key information associated with the connection""" hash_obj = self._hash_alg() hash_obj.update(self._conn.get_hash_prefix()) hash_obj.update(String(host_key_data)) hash_obj.update(String(self._client_pub)) hash_obj.update(String(self._server_pub)) hash_obj.update(MPInt(k)) return hash_obj.digest() def _process_init(self, pkttype, packet): """Process an ECDH init message""" # pylint: disable=unused-argument if self._conn.is_client(): raise DisconnectError(DISC_PROTOCOL_ERROR, 'Unexpected kex init msg') self._client_pub = packet.get_string() packet.check_end() try: k = self._priv.get_shared(self._client_pub) except (AssertionError, ValueError): raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid kex init msg') from None host_key, host_key_data = self._conn.get_server_host_key() h = self._compute_hash(host_key_data, k) sig = host_key.sign(h) self._conn.send_packet(Byte(MSG_KEX_ECDH_REPLY), String(host_key_data), String(self._server_pub), String(sig)) self._conn.send_newkeys(k, h) def _process_reply(self, pkttype, packet): """Process an ECDH reply message""" # pylint: disable=unused-argument if self._conn.is_server(): raise DisconnectError(DISC_PROTOCOL_ERROR, 'Unexpected kex reply msg') host_key_data = packet.get_string() self._server_pub = packet.get_string() sig = packet.get_string() packet.check_end() try: k = self._priv.get_shared(self._server_pub) except (AssertionError, ValueError): raise DisconnectError(DISC_PROTOCOL_ERROR, 'Invalid kex reply msg') from None host_key = self._conn.validate_server_host_key(host_key_data) h = self._compute_hash(host_key_data, k) if not host_key.verify(h, sig): raise DisconnectError(DISC_KEY_EXCHANGE_FAILED, 'Key exchange hash mismatch') self._conn.send_newkeys(k, h) packet_handlers = { MSG_KEX_ECDH_INIT: _process_init, MSG_KEX_ECDH_REPLY: _process_reply } try: from .crypto import ECDH except ImportError: # pragma: no cover pass else: for _curve_id, _hash_alg in ((b'nistp521', sha512), (b'nistp384', sha384), (b'nistp256', sha256)): register_kex_alg(b'ecdh-sha2-' + _curve_id, _KexECDH, _hash_alg, ECDH, _curve_id) try: from .crypto import Curve25519DH except ImportError: # pragma: no cover pass else: register_kex_alg(b'curve25519-sha256@libssh.org', _KexECDH, sha256, Curve25519DH) asyncssh-1.3.0/asyncssh/ecdsa.py000066400000000000000000000227201260630620200166320ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """ECDSA public key encryption handler""" from .asn1 import ASN1DecodeError, BitString, ObjectIdentifier, TaggedDERObject from .asn1 import der_encode, der_decode from .crypto import lookup_ec_curve_by_params from .crypto import ECDSAPrivateKey, ECDSAPublicKey from .packet import MPInt, String, SSHPacket, PacketDecodeError from .public_key import SSHKey, SSHCertificateV01 from .public_key import KeyImportError, KeyExportError from .public_key import register_public_key_alg, register_certificate_alg # OID for EC prime fields PRIME_FIELD = ObjectIdentifier('1.2.840.10045.1.1') # Short variable names are used here, matching names in the spec # pylint: disable=invalid-name _alg_oids = {} _alg_oid_map = {} class _ECKey(SSHKey): """Handler for elliptic curve public key encryption""" pem_name = b'EC' pkcs8_oid = ObjectIdentifier('1.2.840.10045.2.1') def __init__(self, key): self.algorithm = b'ecdsa-sha2-' + key.curve_id self._alg_oid = _alg_oids[key.curve_id] self._key = key def __eq__(self, other): # This isn't protected access - both objects are _ECKey instances # pylint: disable=protected-access return (isinstance(other, self.__class__) and self._key.curve_id == other._key.curve_id and self._key.x == other._key.x and self._key.y == other._key.y and self._key.d == other._key.d) def __hash__(self): return hash((self._key.curve_id, self._key.x, self._key.y, self._key.d)) @classmethod def _lookup_curve(cls, alg_params): """Look up an EC curve matching the specified parameters""" if isinstance(alg_params, ObjectIdentifier): try: curve_id = _alg_oid_map[alg_params] except KeyError: raise KeyImportError('Unknown elliptic curve OID %s', alg_params) from None elif (isinstance(alg_params, tuple) and len(alg_params) >= 5 and alg_params[0] == 1 and isinstance(alg_params[1], tuple) and len(alg_params[1]) == 2 and alg_params[1][0] == PRIME_FIELD and isinstance(alg_params[2], tuple) and len(alg_params[2]) >= 2 and isinstance(alg_params[3], bytes) and isinstance(alg_params[2][0], bytes) and isinstance(alg_params[2][1], bytes) and isinstance(alg_params[4], int)): p = alg_params[1][1] a = int.from_bytes(alg_params[2][0], 'big') b = int.from_bytes(alg_params[2][1], 'big') point = alg_params[3] n = alg_params[4] try: curve_id = lookup_ec_curve_by_params(p, a, b, point, n) except ValueError as exc: raise KeyImportError(str(exc)) from None else: raise KeyImportError('Invalid EC curve parameters') return curve_id @classmethod def make_private(cls, curve_id, private_key, public_key): """Construct an EC private key""" if isinstance(private_key, bytes): private_key = int.from_bytes(private_key, 'big') return cls(ECDSAPrivateKey(curve_id, public_key, private_key)) @classmethod def make_public(cls, curve_id, public_key): """Construct an EC public key""" return cls(ECDSAPublicKey(curve_id, public_key)) @classmethod def decode_pkcs1_private(cls, key_data): """Decode a PKCS#1 format EC private key""" if (isinstance(key_data, tuple) and len(key_data) > 2 and key_data[0] == 1 and isinstance(key_data[1], bytes) and isinstance(key_data[2], TaggedDERObject) and key_data[2].tag == 0): alg_params = key_data[2].value private_key = key_data[1] if (len(key_data) > 3 and isinstance(key_data[3], TaggedDERObject) and key_data[3].tag == 1 and isinstance(key_data[3].value, BitString) and key_data[3].value.unused == 0): public_key = key_data[3].value.value else: public_key = None return cls._lookup_curve(alg_params), private_key, public_key else: return None @classmethod def decode_pkcs1_public(cls, key_data): """Decode a PKCS#1 format EC public key""" # pylint: disable=unused-argument raise KeyImportError('PKCS#1 not supported for EC public keys') @classmethod def decode_pkcs8_private(cls, alg_params, data): """Decode a PKCS#8 format EC private key""" try: key_data = der_decode(data) except ASN1DecodeError: key_data = None if (isinstance(key_data, tuple) and len(key_data) > 1 and key_data[0] == 1 and isinstance(key_data[1], bytes)): private_key = key_data[1] if (len(key_data) > 2 and isinstance(key_data[2], TaggedDERObject) and key_data[2].tag == 1 and isinstance(key_data[2].value, BitString) and key_data[2].value.unused == 0): public_key = key_data[2].value.value else: public_key = None return cls._lookup_curve(alg_params), private_key, public_key else: return None @classmethod def decode_pkcs8_public(cls, alg_params, key_data): """Decode a PKCS#8 format EC public key""" if isinstance(alg_params, ObjectIdentifier): return cls._lookup_curve(alg_params), key_data else: return None @classmethod def decode_ssh_private(cls, packet): """Decode an SSH format EC private key""" curve_id = packet.get_string() public_key = packet.get_string() private_key = packet.get_mpint() return curve_id, private_key, public_key @classmethod def decode_ssh_public(cls, packet): """Decode an SSH format EC public key""" curve_id = packet.get_string() public_key = packet.get_string() return curve_id, public_key def encode_public_tagged(self): """Encode an EC public key blob as a tagged bitstring""" return TaggedDERObject(1, BitString(self._key.public_value)) def encode_pkcs1_private(self): """Encode a PKCS#1 format EC private key""" if not self._key.d: raise KeyExportError('Key is not private') return (1, self._key.private_value, TaggedDERObject(0, self._alg_oid), self.encode_public_tagged()) def encode_pkcs1_public(self): """Encode a PKCS#1 format EC public key""" raise KeyExportError('PKCS#1 is not supported for EC public keys') def encode_pkcs8_private(self): """Encode a PKCS#8 format EC private key""" if not self._key.d: raise KeyExportError('Key is not private') return self._alg_oid, der_encode((1, self._key.private_value, self.encode_public_tagged())) def encode_pkcs8_public(self): """Encode a PKCS#8 format EC public key""" return self._alg_oid, self._key.public_value def encode_ssh_private(self): """Encode an SSH format EC private key""" if not self._key.d: raise KeyExportError('Key is not private') return b''.join((String(self._key.curve_id), String(self._key.public_value), MPInt(self._key.d))) def encode_ssh_public(self): """Encode an SSH format EC public key""" return b''.join((String(self._key.curve_id), String(self._key.public_value))) def sign(self, data): """Return a signature of the specified data using this key""" if not self._key.d: raise ValueError('Private key needed for signing') r, s = self._key.sign(data) sig = MPInt(r) + MPInt(s) return b''.join((String(self.algorithm), String(sig))) def verify(self, data, sig): """Verify a signature of the specified data using this key""" try: packet = SSHPacket(sig) if packet.get_string() != self.algorithm: return False sig = packet.get_string() packet.check_end() packet = SSHPacket(sig) r = packet.get_mpint() s = packet.get_mpint() packet.check_end() return self._key.verify(data, (r, s)) except PacketDecodeError: return False for _curve_id, _oid in {(b'nistp521', '1.3.132.0.35'), (b'nistp384', '1.3.132.0.34'), (b'nistp256', '1.2.840.10045.3.1.7')}: _algorithm = b'ecdsa-sha2-' + _curve_id _cert_algorithm = _algorithm + b'-cert-v01@openssh.com' _oid = ObjectIdentifier(_oid) _alg_oids[_curve_id] = _oid _alg_oid_map[_oid] = _curve_id register_public_key_alg(_algorithm, _ECKey) register_certificate_alg(_cert_algorithm, _ECKey, SSHCertificateV01) asyncssh-1.3.0/asyncssh/ed25519.py000066400000000000000000000062601260630620200165520ustar00rootroot00000000000000# Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Ed25519 public key encryption handler""" from .packet import String, SSHPacket from .public_key import SSHKey, SSHCertificateV01, KeyExportError from .public_key import register_public_key_alg, register_certificate_alg # Short variable names are used here, matching names in the spec # pylint: disable=invalid-name class _Ed25519Key(SSHKey): """Handler for Ed25519 public key encryption""" algorithm = b'ssh-ed25519' def __init__(self, vk, sk): self._vk = vk self._sk = sk def __eq__(self, other): # This isn't protected access - both objects are _Ed25519Key instances # pylint: disable=protected-access return (isinstance(other, self.__class__) and self._vk == other._vk and self._sk == other._sk) def __hash__(self): return hash(self._vk) @classmethod def make_private(cls, vk, sk): """Construct an Ed25519 private key""" return cls(vk, sk) @classmethod def make_public(cls, vk): """Construct an Ed25519 public key""" return cls(vk, None) @classmethod def decode_ssh_private(cls, packet): """Decode an SSH format Ed25519 private key""" vk = packet.get_string() sk = packet.get_string() return vk, sk @classmethod def decode_ssh_public(cls, packet): """Decode an SSH format Ed25519 public key""" vk = packet.get_string() return (vk,) def encode_ssh_private(self): """Encode an SSH format Ed25519 private key""" if self._sk is None: raise KeyExportError('Key is not private') return b''.join((String(self._vk), String(self._sk))) def encode_ssh_public(self): """Encode an SSH format Ed25519 public key""" return String(self._vk) def sign(self, data): """Return a signature of the specified data using this key""" if self._sk is None: raise ValueError('Private key needed for signing') sig = libnacl.crypto_sign(data, self._sk) return b''.join((String(self.algorithm), String(sig[:-len(data)]))) def verify(self, data, sig): """Verify a signature of the specified data using this key""" try: packet = SSHPacket(sig) if packet.get_string() != self.algorithm: return False sig = packet.get_string() packet.check_end() return libnacl.crypto_sign_open(sig + data, self._vk) == data except ValueError: return False try: import libnacl except (ImportError, OSError): # pragma: no cover pass else: register_public_key_alg(b'ssh-ed25519', _Ed25519Key) register_certificate_alg(b'ssh-ed25519-cert-v01@openssh.com', _Ed25519Key, SSHCertificateV01) asyncssh-1.3.0/asyncssh/forward.py000066400000000000000000000106531260630620200172210ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """SSH port forwarding handlers""" import asyncio from .misc import DisconnectError from .session import SSHTCPSession class SSHPortForwarder(SSHTCPSession): """SSH port forwarding connection handler""" def __init__(self, conn, loop, peer=None): self._conn = conn self._loop = loop self._peer = peer self._transport = None self._eof_received = False if peer: peer.set_peer(self) def set_peer(self, peer): """Set the peer forwarder to exchange data with""" self._peer = peer def clear_peer(self): """Clear the peer forwarder""" self._peer = None def set_transport(self, transport): """Set the transport to forward to/from""" self._transport = transport def clear_transport(self): """Close and clear the transport""" if self._transport: self._transport.close() self._transport = None def write(self, data): """Write data to the transport""" self._transport.write(data) def write_eof(self): """Write end of file to the transport""" self._transport.write_eof() def was_eof_received(self): """Return whether end of file has been received or not""" return self._eof_received def pause_reading(self): """Pause reading from the transport""" self._transport.pause_reading() def resume_reading(self): """Resume reading on the transport""" self._transport.resume_reading() def connection_made(self, transport): """Handle a newly opened connection""" self.set_transport(transport) def connection_lost(self, exc): """Handle an incoming connection close""" self.clear_transport() if self._peer: self._peer.clear_transport() self._peer.clear_peer() self.clear_peer() def data_received(self, data, datatype=None): """Handle incoming data from the transport""" self._peer.write(data) def eof_received(self): """Handle an incoming end of file from the transport""" self._eof_received = True self._peer.write_eof() return not self._peer.was_eof_received() def pause_writing(self): """Pause writing by asking peer to pause reading""" self._peer.pause_reading() def resume_writing(self): """Resume writing by asking peer to resume reading""" self._peer.resume_reading() class SSHLocalPortForwarder(SSHPortForwarder): """SSH local port forwarding connection handler""" def __init__(self, conn, loop, coro, dest_host, dest_port): super().__init__(conn, loop) self._coro = coro self._dest_host = dest_host self._dest_port = dest_port @asyncio.coroutine def _forward(self): """Set up a port forwarding for a local port""" def session_factory(): """Return an SSH port forwarder""" return SSHPortForwarder(self._conn, self._loop, self._peer) orig_host, orig_port = self._transport.get_extra_info('peername')[:2] try: _, self._peer = \ yield from self._coro(session_factory, self._dest_host, self._dest_port, orig_host, orig_port) self._peer.set_peer(self) self.resume_reading() except DisconnectError: self.clear_transport() def connection_made(self, transport): """Handle a newly opened connection""" super().connection_made(transport) transport.pause_reading() asyncio.async(self._forward(), loop=self._loop) class SSHRemotePortForwarder(SSHPortForwarder): """SSH remote port forwarding connection handler""" def __init__(self, conn, loop, peer): super().__init__(conn, loop, peer) self.pause_writing() def connection_made(self, transport): """Handle a newly opened connection""" super().connection_made(transport) self.resume_writing() asyncssh-1.3.0/asyncssh/kex.py000066400000000000000000000032601260630620200163400ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """SSH key exchange handlers""" from .packet import MPInt, SSHPacketHandler _kex_algs = [] _kex_handlers = {} class Kex(SSHPacketHandler): """Parent class for key exchange handlers""" def __init__(self, alg, conn, hash_alg): self.algorithm = alg self._conn = conn self._hash_alg = hash_alg def compute_key(self, k, h, x, session_id, keylen): """Compute keys from output of key exchange""" key = b'' while len(key) < keylen: hash_obj = self._hash_alg() hash_obj.update(MPInt(k)) hash_obj.update(h) hash_obj.update(key if key else x + session_id) key += hash_obj.digest() return key[:keylen] def register_kex_alg(alg, handler, hash_alg, *args): """Register a key exchange algorithm""" _kex_algs.append(alg) _kex_handlers[alg] = (handler, hash_alg, args) def get_kex_algs(): """Return a list of available key exchange algorithms""" return _kex_algs def get_kex(conn, alg): """Return a key exchange handler The function looks up a key exchange algorithm and returns a handler which can perform that type of key exchange. """ handler, hash_alg, args = _kex_handlers[alg] return handler(alg, conn, hash_alg, *args) asyncssh-1.3.0/asyncssh/known_hosts.py000066400000000000000000000120331260630620200201230ustar00rootroot00000000000000# Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation # Alexander Travov - proposed changes to add negated patterns, hashed # entries, and support for the revoked marker """Parser for SSH known_hosts files""" import binascii import hmac from hashlib import sha1 from .misc import ip_address from .pattern import HostPatternList from .public_key import KeyImportError, import_public_key class PlainHost: """A plain host entry in a known_hosts file""" def __init__(self, pattern, marker, key): self._pattern = HostPatternList(pattern) self.marker = marker self.key = key def matches(self, host, addr, ip): """Return whether a host or address matches this host pattern list""" return self._pattern.matches(host, addr, ip) class HashedHost: """A hashed host entry in a known_hosts file""" _HMAC_SHA1_MAGIC = '1' def __init__(self, pattern, marker, key): self.marker = marker self.key = key try: magic, salt, hosthash = pattern[1:].split('|') self._salt = binascii.a2b_base64(salt) self._hosthash = binascii.a2b_base64(hosthash) except (ValueError, binascii.Error): raise ValueError('Invalid known hosts hash entry: %s' % pattern) from None if magic != self._HMAC_SHA1_MAGIC: # Only support HMAC SHA-1 for now raise ValueError('Invalid known hosts hash type: %s' % magic) from None def _match(self, value): """Return whether this host hash matches a value""" hosthash = hmac.new(self._salt, value.encode(), sha1).digest() return hosthash == self._hosthash def matches(self, host, addr, ip): """Return whether a host or address matches this host hash""" # pylint: disable=unused-argument return (host and self._match(host)) or (addr and self._match(addr)) def _parse_entries(known_hosts): """Parse the entries in a known hosts file""" entries = [] for line in known_hosts.splitlines(): line = line.strip() if not line or line.startswith('#'): continue try: if line.startswith('@'): marker, pattern, key = line[1:].split(None, 2) else: marker = None pattern, key = line.split(None, 1) except ValueError: raise ValueError('Invalid known hosts entry: %s' % line) from None if marker not in (None, 'cert-authority', 'revoked'): raise ValueError('Invalid known hosts marker: %s' % marker) from None try: key = import_public_key(key) except KeyImportError: # Ignore keys in the file that we're unable to parse continue if pattern.startswith('|'): entry = HashedHost(pattern, marker, key) else: entry = PlainHost(pattern, marker, key) entries.append(entry) return entries def _match_entries(entries, host, addr, port=None): """Return matching keys in a known_hosts file""" ip = ip_address(addr) if addr else None if port: host = '[{}]:{}'.format(host, port) if host else None addr = '[{}]:{}'.format(addr, port) if addr else None host_keys = [] ca_keys = [] revoked_keys = [] for entry in entries: if entry.matches(host, addr, ip): if entry.marker == 'revoked': revoked_keys.append(entry.key) elif entry.marker == 'cert-authority': ca_keys.append(entry.key) else: host_keys.append(entry.key) return host_keys, ca_keys, revoked_keys def match_known_hosts(known_hosts, host, addr, port): """Match a host, IP address, and port against a known_hosts file This function looks up a host, IP address, and port in a file in OpenSSH ``known_hosts`` format and returns the host keys, CA keys, and revoked keys which match. If the port is not the default port and no match is found for it, the lookup is attempted again without a port number. """ if isinstance(known_hosts, str): with open(known_hosts, 'r') as f: known_hosts = f.read() else: known_hosts = known_hosts.decode() entries = _parse_entries(known_hosts) host_keys, ca_keys, revoked_keys = _match_entries(entries, host, addr, port) if port and not (host_keys or ca_keys): host_keys, ca_keys, revoked_keys = _match_entries(entries, host, addr) return host_keys, ca_keys, revoked_keys asyncssh-1.3.0/asyncssh/listener.py000066400000000000000000000071101260630620200173740ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """SSH listeners""" import asyncio from .channel import SSHTCPChannel class SSHListener(asyncio.AbstractServer): """SSH listener for inbound TCP connections""" def get_port(self): """Return the port number being listened on This method returns the port number that the remote listener was bound to. When the requested remote listening port is ``0`` to indicate a dynamic port, this method can be called to determine what listening port was selected. :returns: The port number being listened on """ raise NotImplementedError def close(self): """Stop listening for new connections This method can be called to stop listening for connections. Existing connections will remain open. """ raise NotImplementedError @asyncio.coroutine def wait_closed(self): """Wait for the listener to close This method is a coroutine which waits for the associated TCP listeners to be closed. """ raise NotImplementedError class SSHForwardListener(SSHListener): """A TCP listener used when forwarding traffic fromm local ports""" def __init__(self, listen_port, servers): self._listen_port = listen_port self._servers = servers def get_port(self): return self._listen_port def close(self): for server in self._servers: server.close() self._servers = [] @asyncio.coroutine def wait_closed(self): for server in self._servers: yield from server.wait_closed() class SSHClientListener(SSHListener): """SSH client listener used to accept inbound forwarded connections""" def __init__(self, conn, loop, session_factory, listen_host, listen_port, encoding, window, max_pktsize): self._conn = conn self._loop = loop self._session_factory = session_factory self._listen_host = listen_host self._listen_port = listen_port self._encoding = encoding self._window = window self._max_pktsize = max_pktsize self._waiters = [] def process_connection(self, orig_host, orig_port): """Process a forwarded TCP connection""" chan = SSHTCPChannel(self._conn, self._loop, self._encoding, self._window, self._max_pktsize) chan.set_inbound_peer_names(self._listen_host, self._listen_port, orig_host, orig_port) return chan, self._session_factory(orig_host, orig_port) def get_port(self): return self._listen_port def close(self): asyncio.async(self._conn.close_client_listener(self, self._listen_host, self._listen_port), loop=self._loop) self._conn = None for waiter in self._waiters: if not waiter.cancelled(): waiter.set_result(None) @asyncio.coroutine def wait_closed(self): if self._conn: waiter = asyncio.Future(loop=self._loop) self._waiters.append(waiter) yield from waiter asyncssh-1.3.0/asyncssh/logging.py000066400000000000000000000007601260630620200172010ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Sam Crooks - initial implementation # Ron Frederick - minor cleanup """Logging functions""" import logging logger = logging.getLogger(__package__) asyncssh-1.3.0/asyncssh/mac.py000066400000000000000000000050221260630620200163070ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """SSH message authentication handlers""" import hmac from hashlib import md5, sha1, sha256, sha512 _ETM = b'-etm@openssh.com' _mac_algs = [] _mac_params = {} _mac_handlers = {} class _MAC: """Parent class for SSH message authentication handlers""" def __init__(self, hash_alg, hash_size, key): self._hash_alg = hash_alg self._hash_size = hash_size self._key = key def sign(self, data): """Compute a signature for a message""" sig = hmac.new(self._key, data, self._hash_alg).digest() return sig[:self._hash_size] def verify(self, data, sig): """Verify the signature of a message""" return self.sign(data) == sig def register_mac_alg(alg, hash_alg, key_size, hash_size): """Register a MAC algorithm""" _mac_algs.append(alg) _mac_params[alg] = (key_size, hash_size, False) _mac_params[alg + _ETM] = (key_size, hash_size, True) _mac_handlers[alg] = (hash_alg, hash_size) _mac_handlers[alg + _ETM] = (hash_alg, hash_size) def get_mac_algs(): """Return a list of available MAC algorithms""" return [alg + _ETM for alg in _mac_algs] + _mac_algs def get_mac_params(alg): """Get parameters of a MAC algorithm This function returns the key and hash sizes of a MAC algorithm and whether or not to compute the MAC before or after encryption. """ return _mac_params[alg] def get_mac(alg, key): """Return a MAC handler This function returns a MAC object initialized with the specified kev that can be used for data signing and verification. """ hash_alg, hash_size = _mac_handlers[alg] return _MAC(hash_alg, hash_size, key) # pylint: disable=bad-whitespace register_mac_alg(b'hmac-sha2-256', sha256, 32, 32) register_mac_alg(b'hmac-sha2-512', sha512, 64, 64) register_mac_alg(b'hmac-sha1', sha1, 20, 20) register_mac_alg(b'hmac-md5', md5, 16, 16) register_mac_alg(b'hmac-sha2-256-96', sha256, 32, 12) register_mac_alg(b'hmac-sha2-512-96', sha512, 64, 12) register_mac_alg(b'hmac-sha1-96', sha1, 20, 12) register_mac_alg(b'hmac-md5-96', md5, 16, 12) asyncssh-1.3.0/asyncssh/misc.py000066400000000000000000000123211260630620200165020ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Miscellaneous utility classes and functions""" import ipaddress import socket from random import SystemRandom from .constants import DEFAULT_LANG # Define a version of randrange which is based on SystemRandom(), so that # we get back numbers suitable for cryptographic use. _random = SystemRandom() randrange = _random.randrange def all_ints(seq): """Return if a sequence contains all integers""" return all(isinstance(i, int) for i in seq) def mod_inverse(x, m): """Compute the modular inverse (x^-1) modulo m""" # pylint: disable=invalid-name a, b, c, d = m, x % m, 0, 1 while b: q, r = divmod(a, b) a, b, c, d = b, r, d, c - q*d if a == 1: return c if c >= 0 else c + m else: raise ValueError('%d has no inverse mod %d' % (x, m)) def _normalize_scoped_ip(addr): """Normalize scoped IP address The ipaddress module doesn't handle scoped addresses properly, so we strip off the CIDR suffix here and normalize scoped IP addresses using socket.inet_pton before we pass them into ipaddress. """ for family in (socket.AF_INET, socket.AF_INET6): try: return socket.inet_ntop(family, socket.inet_pton(family, addr)) except (ValueError, socket.error): pass return addr def ip_address(addr): """Wrapper for ipaddress.ip_address which supports scoped addresses""" return ipaddress.ip_address(_normalize_scoped_ip(addr)) def ip_network(addr): """Wrapper for ipaddress.ip_network which supports scoped addresses""" idx = addr.find('/') if idx >= 0: addr, mask = addr[:idx], addr[idx:] else: mask = '' return ipaddress.ip_network(_normalize_scoped_ip(addr) + mask) class Error(Exception): """General SSH error""" def __init__(self, errtype, code, reason, lang): super().__init__('%s Error: %s' % (errtype, reason)) self.code = code self.reason = reason self.lang = lang class DisconnectError(Error): """SSH disconnect error This exception is raised when a serious error occurs which causes the SSH connection to be disconnected. Exception codes should be taken from :ref:`disconnect reason codes `. :param integer code: Disconnect reason, taken from :ref:`disconnect reason codes ` :param string reason: A human-readable reason for the disconnect :param string lang: The language the reason is in """ def __init__(self, code, reason, lang=DEFAULT_LANG): super().__init__('Disconnect', code, reason, lang) class ChannelOpenError(Error): """SSH channel open error This exception is raised by connection handlers to report channel open failures. :param integer code: Channel open failure reason, taken from :ref:`channel open failure reason codes ` :param string reason: A human-readable reason for the channel open failure :param string lang: The language the reason is in """ def __init__(self, code, reason, lang=DEFAULT_LANG): super().__init__('Channel Open', code, reason, lang) class BreakReceived(Exception): """SSH break request received This exception is raised on an SSH server stdin stream when the client sends a break on the channel. :param integer msec: The duration of the break in milliseconds """ def __init__(self, msec): super().__init__('Break for %s msec' % msec) self.msec = msec class SignalReceived(Exception): """SSH signal request received This exception is raised on an SSH server stdin stream when the client sends a signal on the channel. :param string signal: The name of the signal sent by the client """ def __init__(self, signal): super().__init__('Signal: %s' % signal) self.signal = signal class TerminalSizeChanged(Exception): """SSH terminal size change notification received This exception is raised on an SSH server stdin stream when the client sends a terminal size change on the channel. :param integer width: The new terminal width :param integer height: The new terminal height :param integer pixwidth: The new terminal width in pixels :param integer pixheight: The new terminal height in pixels """ def __init__(self, width, height, pixwidth, pixheight): super().__init__('Terminal size change: (%s, %s, %s, %s)' % (width, height, pixwidth, pixheight)) self.width = width self.height = height self.pixwidth = pixwidth self.pixheight = pixheight asyncssh-1.3.0/asyncssh/packet.py000066400000000000000000000103111260630620200170130ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """SSH packet encoding and decoding functions""" class PacketDecodeError(ValueError): """Packet decoding error""" def Byte(value): """Encode a single byte""" return bytes((value,)) def Boolean(value): """Encode a boolean value""" return Byte(bool(value)) def UInt32(value): """Encode a 32-bit integer value""" return value.to_bytes(4, 'big') def UInt64(value): """Encode a 64-bit integer value""" return value.to_bytes(8, 'big') def String(value): """Encode a UTF-8 string value""" if isinstance(value, str): value = value.encode('utf-8', errors='strict') return len(value).to_bytes(4, 'big') + value def MPInt(value): """Encode a multiple precision integer value""" l = value.bit_length() l = l // 8 + 1 if value != 0 and l % 8 == 0 else (l + 7) // 8 return l.to_bytes(4, 'big') + value.to_bytes(l, 'big', signed=True) def NameList(value): """Encode a comma-separated list of byte strings""" return String(b','.join(value)) class SSHPacket: """Decoder class for SSH packets""" def __init__(self, packet): self._packet = packet self._idx = 0 self._len = len(packet) def __bool__(self): return self._idx != self._len def check_end(self): """Confirm that all of the data in the packet has been consumed""" if self: raise PacketDecodeError('Unexpected data at end of packet') def get_consumed_payload(self): """Return the portion of the packet consumed so far""" return self._packet[:self._idx] def get_remaining_payload(self): """Return the portion of the packet not yet consumed""" return self._packet[self._idx:] def get_bytes(self, size): """Extract the requested number of bytes from the packet""" if self._idx + size > self._len: raise PacketDecodeError('Incomplete packet') value = self._packet[self._idx:self._idx+size] self._idx += size return value def get_byte(self): """Extract a single byte from the packet""" return self.get_bytes(1)[0] def get_boolean(self): """Extract a boolean from the packet""" return bool(self.get_byte()) def get_uint32(self): """Extract a 32-bit integer from the packet""" return int.from_bytes(self.get_bytes(4), 'big') def get_uint64(self): """Extract a 64-bit integer from the packet""" return int.from_bytes(self.get_bytes(8), 'big') def get_string(self): """Extract a UTF-8 string from the packet""" return self.get_bytes(self.get_uint32()) def get_mpint(self): """Extract a multiple precision integer from the packet""" return int.from_bytes(self.get_string(), 'big') def get_namelist(self): """Extract a comma-separated list of byte strings from the packet""" namelist = self.get_string() return namelist.split(b',') if namelist else [] class SSHPacketHandler: """Parent class for SSH packet handlers Classes wishing to decode SSH packets can inherit from this class, defining the class variable packet_handlers as a dictionary which maps SSH packet types to handler methods in the class and then calling process_packet() to run the corresponding packet handler. The process_packet() function will return True if a handler was found and False otherwise. """ packet_handlers = {} def process_packet(self, pkttype, packet): """Call the packet handler defined for the specified packet. Return True if a handler was found, or False otherwise.""" if pkttype in self.packet_handlers: self.packet_handlers[pkttype](self, pkttype, packet) return True else: return False asyncssh-1.3.0/asyncssh/pattern.py000066400000000000000000000077031260630620200172340ustar00rootroot00000000000000# Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Pattern matching for principal and host names""" from fnmatch import fnmatch from .misc import ip_network class WildcardPattern: """A pattern matcher for '*' and '?' wildcards""" def __init__(self, pattern): # We need to escape square brackets in host patterns if we # want to use Python's fnmatch. self._pattern = ''.join('[[]' if ch == '[' else '[]]' if ch == ']' else ch for ch in pattern) def matches(self, value): """Return whether a wild card pattern matches a value""" return fnmatch(value, self._pattern) class WildcardHostPattern(WildcardPattern): """Match a host name or address against a wildcard pattern""" def matches(self, host, addr, ip): """Return whether a host or address matches a wild card host pattern""" # Arguments vary by class, but inheritance is still needed here # IP matching is only done for CIDRHostPattern # pylint: disable=arguments-differ,unused-argument return (host and super().matches(host)) or \ (addr and super().matches(addr)) class CIDRHostPattern: """Match IPv4/v6 address against CIDR-style subnet pattern""" def __init__(self, pattern): self._network = ip_network(pattern) def matches(self, host, addr, ip): """Return whether an IP address matches a CIDR address pattern""" # Host & addr matching is only done for WildcardHostPattern # pylint: disable=unused-argument return ip and ip in self._network class _PatternList: """Match against a list of comma-separated positive and negative patterns This class is a base class for building a pattern matcher that takes a set of comma-separated positive and negative patterns, returning ``True`` if one or more positive patterns match and no negative ones do. The pattern matching is done by objects returned by the build_pattern method. The arguments passed in when a match is performed will vary depending on what class build_pattern returns. """ def __init__(self, patterns): self._pos_patterns = [] self._neg_patterns = [] for pattern in patterns.split(','): if pattern.startswith('!'): negate = True pattern = pattern[1:] else: negate = False matcher = self.build_pattern(pattern) if negate: self._neg_patterns.append(matcher) else: self._pos_patterns.append(matcher) def build_pattern(self, pattern): """Abstract method to build a pattern object""" raise NotImplementedError def matches(self, *args): """Match a set of values against positive & negative pattern lists""" pos_match = any(p.matches(*args) for p in self._pos_patterns) neg_match = any(p.matches(*args) for p in self._neg_patterns) return pos_match and not neg_match class WildcardPatternList(_PatternList): """Match names against wildcard patterns""" def build_pattern(self, pattern): """Build a wild card pattern""" return WildcardPattern(pattern) class HostPatternList(_PatternList): """Match host names & addresses against wildcard and CIDR patterns""" def build_pattern(self, pattern): """Build a CIDR address or wild card host pattern""" try: return CIDRHostPattern(pattern) except ValueError: return WildcardHostPattern(pattern) asyncssh-1.3.0/asyncssh/pbe.py000066400000000000000000000466101260630620200163250ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Asymmetric key password based encryption functions""" import hmac import os from hashlib import md5, sha1, sha224, sha256, sha384, sha512 from .asn1 import ASN1DecodeError, ObjectIdentifier, der_encode, der_decode from .crypto import lookup_cipher # pylint: disable=bad-whitespace _ES1_MD5_DES = ObjectIdentifier('1.2.840.113549.1.5.3') _ES1_SHA1_DES = ObjectIdentifier('1.2.840.113549.1.5.10') _ES2 = ObjectIdentifier('1.2.840.113549.1.5.13') _P12_RC4_128 = ObjectIdentifier('1.2.840.113549.1.12.1.1') _P12_RC4_40 = ObjectIdentifier('1.2.840.113549.1.12.1.2') _P12_DES3 = ObjectIdentifier('1.2.840.113549.1.12.1.3') _P12_DES2 = ObjectIdentifier('1.2.840.113549.1.12.1.4') _ES2_CAST128 = ObjectIdentifier('1.2.840.113533.7.66.10') _ES2_DES3 = ObjectIdentifier('1.2.840.113549.3.7') _ES2_BF = ObjectIdentifier('1.3.6.1.4.1.3029.1.2') _ES2_DES = ObjectIdentifier('1.3.14.3.2.7') _ES2_AES128 = ObjectIdentifier('2.16.840.1.101.3.4.1.2') _ES2_AES192 = ObjectIdentifier('2.16.840.1.101.3.4.1.22') _ES2_AES256 = ObjectIdentifier('2.16.840.1.101.3.4.1.42') _ES2_PBKDF2 = ObjectIdentifier('1.2.840.113549.1.5.12') _ES2_SHA1 = ObjectIdentifier('1.2.840.113549.2.7') _ES2_SHA224 = ObjectIdentifier('1.2.840.113549.2.8') _ES2_SHA256 = ObjectIdentifier('1.2.840.113549.2.9') _ES2_SHA384 = ObjectIdentifier('1.2.840.113549.2.10') _ES2_SHA512 = ObjectIdentifier('1.2.840.113549.2.11') _ES2_SHA512_224 = ObjectIdentifier('1.2.840.113549.2.12') _ES2_SHA512_256 = ObjectIdentifier('1.2.840.113549.2.13') # pylint: enable=bad-whitespace _pkcs1_ciphers = {} _pkcs8_ciphers = {} _pbes2_ciphers = {} _pbes2_kdfs = {} _pbes2_prfs = {} _pkcs1_cipher_names = {} _pkcs8_cipher_suites = {} _pbes2_cipher_names = {} _pbes2_kdf_names = {} _pbes2_prf_names = {} def strxor(a, b): """Return the byte-wise XOR of two strings""" c = int.from_bytes(a, 'little') ^ int.from_bytes(b, 'little') return int.to_bytes(c, max(len(a), len(b)), 'little') class KeyEncryptionError(ValueError): """Key encryption error This exception is raised by key decryption functions when the data provided is not a valid encrypted private key. """ class _RFC1423Pad: """RFC 1423 padding functions This class implements RFC 1423 padding for encryption and decryption of data by block ciphers. On encryption, the data is padded by between 1 and the cipher's block size number of bytes, with the padding value being equal to the length of the padding. """ def __init__(self, cipher): self._cipher = cipher self._block_size = cipher.block_size def encrypt(self, data): """Pad data before encrypting it""" pad = self._block_size - (len(data) % self._block_size) data += pad * bytes((pad,)) return self._cipher.encrypt(data) def decrypt(self, data): """Remove padding from data after decrypting it""" data = self._cipher.decrypt(data) if data: pad = data[-1] if (1 <= pad <= self._block_size and data[-pad:] == pad * bytes((pad,))): return data[:-pad] raise KeyEncryptionError('Unable to decrypt key') def _pbkdf1(hash_alg, passphrase, salt, count, key_size): """PKCS#5 v1.5 key derivation function for password-based encryption This function implements the PKCS#5 v1.5 algorithm for deriving an encryption key from a passphrase and salt. The standard PBKDF1 function cannot generate more key bytes than the hash digest size, but 3DES uses a modified form of it which calls PBKDF1 recursively on the result to generate more key data. Support for this is implemented here. """ if isinstance(passphrase, str): passphrase = passphrase.encode('utf-8') key = passphrase + salt for _ in range(count): key = hash_alg(key).digest() if len(key) <= key_size: return key + _pbkdf1(hash_alg, key + passphrase, salt, count, key_size - len(key)) else: return key[:key_size] def _pbkdf2(prf, passphrase, salt, count, key_size): """PKCS#5 v2.0 key derivation function for password-based encryption This function implements the PKCS#5 v2.0 algorithm for deriving an encryption key from a passphrase and salt. """ # Short variable names are used here, matching names in the spec # pylint: disable=invalid-name if isinstance(passphrase, str): passphrase = passphrase.encode('utf-8') key = b'' i = 1 while len(key) < key_size: u = prf(passphrase, salt + i.to_bytes(4, 'big')) f = u for _ in range(1, count): u = prf(passphrase, u) f = strxor(f, u) key += f i += 1 return key[:key_size] def _pbkdf_p12(hash_alg, passphrase, salt, count, key_size, idx): """PKCS#12 key derivation function for password-based encryption This function implements the PKCS#12 algorithm for deriving an encryption key from a passphrase and salt. """ # Short variable names are used here, matching names in the spec # pylint: disable=invalid-name def _make_block(data, v): """Make a block a multiple of v bytes long by repeating data""" l = len(data) size = ((l + v - 1) // v) * v return (((size + l - 1) // l) * data)[:size] v = hash_alg().block_size D = v * bytes((idx,)) if isinstance(passphrase, str): passphrase = passphrase.encode('utf-16be') I = bytearray(_make_block(salt, v) + _make_block(passphrase + b'\0\0', v)) key = b'' while len(key) < key_size: A = D + I for i in range(count): A = hash_alg(A).digest() B = int.from_bytes(_make_block(A, v), 'big') for i in range(0, len(I), v): x = (int.from_bytes(I[i:i+v], 'big') + B + 1) % (1 << v*8) I[i:i+v] = x.to_bytes(v, 'big') key += A return key[:key_size] def _pbes1(params, passphrase, hash_alg, cipher, key_size): """PKCS#5 v1.5 cipher selection function for password-based encryption This function implements the PKCS#5 v1.5 algorithm for password-based encryption. It returns a cipher object which can be used to encrypt or decrypt data based on the specified encryption parameters, passphrase, and salt. """ if (not isinstance(params, tuple) or len(params) != 2 or not isinstance(params[0], bytes) or not isinstance(params[1], int)): raise KeyEncryptionError('Invalid PBES1 encryption parameters') salt, count = params key = _pbkdf1(hash_alg, passphrase, salt, count, key_size + cipher.block_size) key, iv = key[:key_size], key[key_size:] return _RFC1423Pad(cipher.new(key, iv)) def _pbe_p12(params, passphrase, hash_alg, cipher, key_size): """PKCS#12 cipher selection function for password-based encryption This function implements the PKCS#12 algorithm for password-based encryption. It returns a cipher object which can be used to encrypt or decrypt data based on the specified encryption parameters, passphrase, and salt. """ if (not isinstance(params, tuple) or len(params) != 2 or not isinstance(params[0], bytes) or len(params[0]) == 0 or not isinstance(params[1], int) or params[1] == 0): raise KeyEncryptionError('Invalid PBES1 PKCS#12 encryption parameters') salt, count = params key = _pbkdf_p12(hash_alg, passphrase, salt, count, key_size, 1) if cipher.cipher_name == 'arc4': cipher = cipher.new(key) else: iv = _pbkdf_p12(hash_alg, passphrase, salt, count, cipher.block_size, 2) cipher = _RFC1423Pad(cipher.new(key, iv)) return cipher def _pbes2_iv(params, key, cipher): """PKCS#5 v2.0 handler for PBES2 ciphers with an IV as a parameter This function returns the appropriate cipher object to use for PBES2 encryption for ciphers that have only an IV as an encryption parameter. """ if len(params) != 1 or not isinstance(params[0], bytes): raise KeyEncryptionError('Invalid PBES2 encryption parameters') if len(params[0]) != cipher.block_size: raise KeyEncryptionError('Invalid length IV for PBES2 encryption') return cipher.new(key, params[0]) def _pbes2_hmac_prf(hash_alg, digest_size=None): """PKCS#5 v2.0 handler for PBKDF2 psuedo-random function This function returns the appropriate PBKDF2 pseudo-random function to use for key derivation. """ return lambda key, msg: hmac.new(key, msg, hash_alg).digest()[:digest_size] def _pbes2_pbkdf2(params, passphrase, default_key_size): """PKCS#5 v2.0 handler for PBKDF2 key derivation This function parses the PBKDF2 arguments from a PKCS#8 encrypted key and returns the encryption key to use for encryption. """ if (len(params) != 1 or not isinstance(params[0], tuple) or len(params[0]) < 2): raise KeyEncryptionError('Invalid PBES2 key derivation parameters') params = list(params[0]) if not isinstance(params[0], bytes) or not isinstance(params[1], int): raise KeyEncryptionError('Invalid PBES2 key derivation parameters') salt = params.pop(0) count = params.pop(0) if params and isinstance(params[0], int): key_size = params.pop(0) # pragma: no cover, used only by RC2 else: key_size = default_key_size if params: if (isinstance(params[0], tuple) and len(params[0]) == 2 and isinstance(params[0][0], ObjectIdentifier)): prf_alg = params[0][0] if prf_alg in _pbes2_prfs: handler, args = _pbes2_prfs[prf_alg] prf = handler(*args) else: raise KeyEncryptionError('Unknown PBES2 pseudo-random ' 'function') else: raise KeyEncryptionError('Invalid PBES2 pseudo-random function ' 'parameters') else: prf = _pbes2_hmac_prf(sha1) return _pbkdf2(prf, passphrase, salt, count, key_size) def _pbes2(params, passphrase): """PKCS#5 v2.0 cipher selection function for password-based encryption This function implements the PKCS#5 v2.0 algorithm for password-based encryption. It returns a cipher object which can be used to encrypt or decrypt data based on the specified encryption parameters and passphrase. """ if (not isinstance(params, tuple) or len(params) != 2 or not isinstance(params[0], tuple) or len(params[0]) < 1 or not isinstance(params[1], tuple) or len(params[1]) < 1): raise KeyEncryptionError('Invalid PBES2 encryption parameters') kdf_params = list(params[0]) kdf_alg = kdf_params.pop(0) if kdf_alg not in _pbes2_kdfs: raise KeyEncryptionError('Unknown PBES2 key derivation function') enc_params = list(params[1]) enc_alg = enc_params.pop(0) if enc_alg not in _pbes2_ciphers: raise KeyEncryptionError('Unknown PBES2 encryption algorithm') kdf_handler, kdf_args = _pbes2_kdfs[kdf_alg] enc_handler, cipher, default_key_size = _pbes2_ciphers[enc_alg] key = kdf_handler(kdf_params, passphrase, default_key_size, *kdf_args) return _RFC1423Pad(enc_handler(enc_params, key, cipher)) def register_pkcs1_cipher(cipher_name, alg, cipher, mode, key_size): """Register a cipher used for PKCS#1 private key encryption""" cipher = lookup_cipher(cipher, mode) if cipher: # pragma: no branch _pkcs1_ciphers[alg] = (cipher, key_size) _pkcs1_cipher_names[cipher_name] = alg def register_pkcs8_cipher(cipher_name, hash_name, alg, handler, hash_alg, cipher, mode, key_size): """Register a cipher used for PKCS#8 private key encryption""" cipher = lookup_cipher(cipher, mode) if cipher: # pragma: no branch _pkcs8_ciphers[alg] = (handler, hash_alg, cipher, key_size) _pkcs8_cipher_suites[cipher_name, hash_name] = alg def register_pbes2_cipher(cipher_name, alg, handler, cipher, mode, key_size): """Register a PBES2 encryption algorithm""" cipher = lookup_cipher(cipher, mode) if cipher: # pragma: no branch _pbes2_ciphers[alg] = (handler, cipher, key_size) _pbes2_cipher_names[cipher_name] = (alg, key_size) def register_pbes2_kdf(kdf_name, alg, handler, *args): """Register a PBES2 key derivation function""" _pbes2_kdfs[alg] = (handler, args) _pbes2_kdf_names[kdf_name] = alg def register_pbes2_prf(hash_name, alg, handler, *args): """Register a PBES2 pseudo-random function""" _pbes2_prfs[alg] = (handler, args) _pbes2_prf_names[hash_name] = alg def pkcs1_encrypt(data, cipher, passphrase): """Encrypt PKCS#1 key data This function encrypts PKCS#1 key data using the specified cipher and passphrase. Available ciphers include: aes128-cbc, aes192-cbc, aes256-cbc, des-cbc, des3-cbc """ if cipher in _pkcs1_cipher_names: alg = _pkcs1_cipher_names[cipher] cipher, key_size = _pkcs1_ciphers[alg] iv = os.urandom(cipher.block_size) key = _pbkdf1(md5, passphrase, iv[:8], 1, key_size) cipher = _RFC1423Pad(cipher.new(key, iv)) return alg, iv, cipher.encrypt(data) else: raise KeyEncryptionError('Unknown PKCS#1 encryption algorithm') def pkcs1_decrypt(data, alg, iv, passphrase): """Decrypt PKCS#1 key data This function decrypts PKCS#1 key data using the specified algorithm, initialization vector, and passphrase. The algorithm name and IV should be taken from the PEM DEK-Info header. """ if alg in _pkcs1_ciphers: cipher, key_size = _pkcs1_ciphers[alg] key = _pbkdf1(md5, passphrase, iv[:8], 1, key_size) cipher = _RFC1423Pad(cipher.new(key, iv)) return cipher.decrypt(data) else: raise KeyEncryptionError('Unknown PKCS#1 encryption algorithm') def pkcs8_encrypt(data, cipher_name, hash_name, version, passphrase): """Encrypt PKCS#8 key data This function encrypts PKCS#8 key data using the specified cipher, hash, encryption version, and passphrase. Available ciphers include: aes128-cbc, aes192-cbc, aes256-cbc, blowfish-cbc, cast128-cbc, des-cbc, des2-cbc, des3-cbc, rc4-40, and rc4-128 Available hashes include: md5, sha1, sha256, sha384, sha512, sha512-224, sha512-256 Available versions include 1 for PBES1 and 2 for PBES2. Only some combinations of cipher, hash, and version are supported. """ if version == 1 and (cipher_name, hash_name) in _pkcs8_cipher_suites: alg = _pkcs8_cipher_suites[cipher_name, hash_name] handler, hash_alg, cipher, key_size = _pkcs8_ciphers[alg] params = (os.urandom(8), 2048) cipher = handler(params, passphrase, hash_alg, cipher, key_size) return der_encode(((alg, params), cipher.encrypt(data))) elif version == 2 and cipher_name in _pbes2_cipher_names: enc_alg, key_size = _pbes2_cipher_names[cipher_name] _, cipher, _ = _pbes2_ciphers[enc_alg] kdf_params = [os.urandom(8), 2048] iv = os.urandom(cipher.block_size) enc_params = (enc_alg, iv) if hash_name != 'sha1': if hash_name in _pbes2_prf_names: kdf_params.append((_pbes2_prf_names[hash_name], None)) else: raise KeyEncryptionError('Unknown PBES2 hash function') alg = _ES2 params = ((_ES2_PBKDF2, tuple(kdf_params)), enc_params) cipher = _pbes2(params, passphrase) else: raise KeyEncryptionError('Unknown PKCS#8 encryption algorithm') return der_encode(((alg, params), cipher.encrypt(data))) def pkcs8_decrypt(key_data, passphrase): """Decrypt PKCS#8 key data This function decrypts key data in PKCS#8 EncryptedPrivateKeyInfo format using the specified passphrase. """ if not isinstance(key_data, tuple) or len(key_data) != 2: raise KeyEncryptionError('Invalid PKCS#8 encrypted key format') alg_params, data = key_data if (not isinstance(alg_params, tuple) or len(alg_params) != 2 or not isinstance(data, bytes)): raise KeyEncryptionError('Invalid PKCS#8 encrypted key format') alg, params = alg_params if alg == _ES2: cipher = _pbes2(params, passphrase) elif alg in _pkcs8_ciphers: handler, hash_alg, cipher, key_size = _pkcs8_ciphers[alg] cipher = handler(params, passphrase, hash_alg, cipher, key_size) else: raise KeyEncryptionError('Unknown PKCS#8 encryption algorithm') try: return der_decode(cipher.decrypt(data)) except ASN1DecodeError: raise KeyEncryptionError('Invalid PKCS#8 encrypted key data') # pylint: disable=bad-whitespace _pkcs1_cipher_list = ( ('aes128-cbc', b'AES-128-CBC', 'aes', 'cbc', 16), ('aes192-cbc', b'AES-192-CBC', 'aes', 'cbc', 24), ('aes256-cbc', b'AES-256-CBC', 'aes', 'cbc', 32), ('des-cbc', b'DES-CBC', 'des', 'cbc', 8), ('des3-cbc', b'DES-EDE3-CBC', 'des3', 'cbc', 24) ) _pkcs8_cipher_list = ( ('des-cbc', 'md5', _ES1_MD5_DES, _pbes1, md5, 'des', 'cbc', 8), ('des-cbc', 'sha1', _ES1_SHA1_DES, _pbes1, sha1, 'des', 'cbc', 8), ('des2-cbc', 'sha1', _P12_DES2, _pbe_p12, sha1, 'des3', 'cbc', 16), ('des3-cbc', 'sha1', _P12_DES3, _pbe_p12, sha1, 'des3', 'cbc', 24), ('rc4-40', 'sha1', _P12_RC4_40, _pbe_p12, sha1, 'arc4', None, 5), ('rc4-128', 'sha1', _P12_RC4_128, _pbe_p12, sha1, 'arc4', None, 16) ) _pbes2_cipher_list = ( ('aes128-cbc', _ES2_AES128, _pbes2_iv, 'aes', 'cbc', 16), ('aes192-cbc', _ES2_AES192, _pbes2_iv, 'aes', 'cbc', 24), ('aes256-cbc', _ES2_AES256, _pbes2_iv, 'aes', 'cbc', 32), ('blowfish-cbc', _ES2_BF, _pbes2_iv, 'blowfish', 'cbc', 16), ('cast128-cbc', _ES2_CAST128, _pbes2_iv, 'cast', 'cbc', 16), ('des-cbc', _ES2_DES, _pbes2_iv, 'des', 'cbc', 8), ('des3-cbc', _ES2_DES3, _pbes2_iv, 'des3', 'cbc', 24) ) _pbes2_kdf_list = ( ('pbkdf2', _ES2_PBKDF2, _pbes2_pbkdf2), ) _pbes2_prf_list = ( ('sha1', _ES2_SHA1, _pbes2_hmac_prf, sha1), ('sha224', _ES2_SHA224, _pbes2_hmac_prf, sha224), ('sha256', _ES2_SHA256, _pbes2_hmac_prf, sha256), ('sha384', _ES2_SHA384, _pbes2_hmac_prf, sha384), ('sha512', _ES2_SHA512, _pbes2_hmac_prf, sha512), ('sha512-224', _ES2_SHA512_224, _pbes2_hmac_prf, sha512, 28), ('sha512-256', _ES2_SHA512_256, _pbes2_hmac_prf, sha512, 32) ) for _args in _pkcs1_cipher_list: register_pkcs1_cipher(*_args) for _args in _pkcs8_cipher_list: register_pkcs8_cipher(*_args) for _args in _pbes2_cipher_list: register_pbes2_cipher(*_args) for _args in _pbes2_kdf_list: register_pbes2_kdf(*_args) for _args in _pbes2_prf_list: register_pbes2_prf(*_args) asyncssh-1.3.0/asyncssh/public_key.py000066400000000000000000001240141260630620200177000ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """SSH asymmetric encryption handlers""" import binascii import os import time try: import bcrypt _bcrypt_available = True except ImportError: # pragma: no cover _bcrypt_available = False from .asn1 import ASN1DecodeError, BitString, der_encode, der_decode from .cipher import get_encryption_params, get_cipher from .misc import ip_network from .packet import String, UInt32, UInt64, PacketDecodeError, SSHPacket from .pbe import KeyEncryptionError, pkcs1_encrypt, pkcs8_encrypt from .pbe import pkcs1_decrypt, pkcs8_decrypt _public_key_algs = [] _certificate_algs = [] _public_key_alg_map = {} _certificate_alg_map = {} _pem_map = {} _pkcs8_oid_map = {} # SSH certificate types CERT_TYPE_USER = 1 CERT_TYPE_HOST = 2 _OPENSSH_KEY_V1 = b'openssh-key-v1\0' _OPENSSH_SALT_LEN = 16 _OPENSSH_WRAP_LEN = 70 def _wrap_base64(data, wrap=64): """Break a Base64 value into multiple lines.""" data = binascii.b2a_base64(data)[:-1] return b'\n'.join(data[i:i+wrap] for i in range(0, len(data), wrap)) + b'\n' class KeyImportError(ValueError): """Key import error This exception is raised by key import functions when the data provided cannot be imported as a valid key. """ class KeyExportError(ValueError): """Key export error This exception is raised by key export functions when the requested format is unknown or encryption is requested for a format which doesn't support it. """ class SSHKey: """Parent class which holds an asymmetric encryption key""" algorithm = None pem_name = None pkcs8_oid = None def encode_pkcs1_private(self): """Export parameters associated with a PKCS#1 private key""" # pylint: disable=no-self-use raise KeyExportError('PKCS#1 private key export not supported') def encode_pkcs1_public(self): """Export parameters associated with a PKCS#1 public key""" # pylint: disable=no-self-use raise KeyExportError('PKCS#1 public key export not supported') def encode_pkcs8_private(self): """Export parameters associated with a PKCS#8 private key""" # pylint: disable=no-self-use raise KeyExportError('PKCS#8 private key export not supported') def encode_pkcs8_public(self): """Export parameters associated with a PKCS#8 public key""" # pylint: disable=no-self-use raise KeyExportError('PKCS#8 public key export not supported') def encode_ssh_private(self): """Export parameters associated with an OpenSSH private key""" # pylint: disable=no-self-use raise KeyExportError('OpenSSH private key export not supported') def encode_ssh_public(self): """Export parameters associated with an OpenSSH public key""" # pylint: disable=no-self-use raise KeyExportError('OpenSSH public key export not supported') def get_ssh_public_key(self): """Return OpenSSH public key in binary format""" return String(self.algorithm) + self.encode_ssh_public() def export_private_key(self, format_name, passphrase=None, cipher_name='aes256-cbc', hash_name='sha256', pbe_version=2, rounds=16): """Export a private key in the requested format This function returns this object's private key encoded in the requested format. If a passphrase is specified, the key will be exported in encrypted form. Available formats include: pkcs1-der, pkcs1-pem, pkcs8-der, pkcs8-pem, openssh Encryption is supported in pkcs1-pem, pkcs8-der, pkcs8-pem, and openssh formats. For pkcs1-pem, only the cipher can be specified. For pkcs8-der and pkcs-8, cipher, hash and PBE version can be specified. For openssh, cipher and rounds can be specified. Available ciphers for pkcs1-pem are: aes128-cbc, aes192-cbc, aes256-cbc, des-cbc, des3-cbc Available ciphers for pkcs8-der and pkcs8-pem are: aes128-cbc, aes192-cbc, aes256-cbc, blowfish-cbc, cast128-cbc, des-cbc, des2-cbc, des3-cbc, rc4-40, rc4-128 Available ciphers for openssh format include the following :ref:`encryption algorithms `. Available hashes include: md5, sha1, sha256, sha384, sha512, sha512-224, sha512-256 Available PBE versions include 1 for PBES1 and 2 for PBES2. Not all combinations of cipher, hash, and version are supported. The default cipher is aes256. In the pkcs8 formats, the default hash is sha256 and default version is PBES2. In openssh format, the default number of rounds is 16. :param string format_name: The format to export the key in. :param string passphrase: (optional) A passphrase to encrypt the private key with. :param string cipher_name: (optional) The cipher to use for private key encryption. :param string hash_name: (optional) The hash to use for private key encryption. :param integer pbe_version: (optional) The PBE version to use for private key encryption. :param integer rounds: (optional) The number of KDF rounds to apply to the passphrase. """ if format_name in ('pkcs1-der', 'pkcs1-pem'): data = der_encode(self.encode_pkcs1_private()) if passphrase is not None: if format_name == 'pkcs1-der': raise KeyExportError('PKCS#1 DER format does not support ' 'private key encryption') alg, iv, data = pkcs1_encrypt(data, cipher_name, passphrase) headers = (b'Proc-Type: 4,ENCRYPTED\n' + b'DEK-Info: ' + alg + b',' + binascii.b2a_hex(iv).upper() + b'\n\n') else: headers = b'' if format_name == 'pkcs1-pem': keytype = self.pem_name + b' PRIVATE KEY' data = (b'-----BEGIN ' + keytype + b'-----\n' + headers + _wrap_base64(data) + b'-----END ' + keytype + b'-----\n') return data elif format_name in ('pkcs8-der', 'pkcs8-pem'): alg_params, data = self.encode_pkcs8_private() data = der_encode((0, (self.pkcs8_oid, alg_params), data)) if passphrase is not None: data = pkcs8_encrypt(data, cipher_name, hash_name, pbe_version, passphrase) if format_name == 'pkcs8-pem': if passphrase is not None: keytype = b'ENCRYPTED PRIVATE KEY' else: keytype = b'PRIVATE KEY' data = (b'-----BEGIN ' + keytype + b'-----\n' + _wrap_base64(data) + b'-----END ' + keytype + b'-----\n') return data elif format_name == 'openssh': check = os.urandom(4) nkeys = 1 comment = b'' keydata = String(self.algorithm) + self.encode_ssh_private() data = b''.join((check, check, keydata, String(comment))) if passphrase is not None: if not _bcrypt_available: # pragma: no cover raise KeyExportError('OpenSSH private key encryption ' 'requires bcrypt') try: alg = cipher_name.encode('ascii') key_size, iv_size, block_size, mode = \ get_encryption_params(alg) except (KeyError, UnicodeEncodeError): raise KeyEncryptionError('Unknown cipher: %s' % cipher_name) from None kdf = b'bcrypt' salt = os.urandom(_OPENSSH_SALT_LEN) kdf_data = b''.join((String(salt), UInt32(rounds))) if isinstance(passphrase, str): passphrase = passphrase.encode('utf-8') # pylint: disable=no-member key = bcrypt.kdf(passphrase, salt, key_size + iv_size, rounds) # pylint: enable=no-member cipher = get_cipher(alg, key[:key_size], key[key_size:]) block_size = max(block_size, 8) else: cipher = None alg = b'none' kdf = b'none' kdf_data = b'' block_size = 8 mac = b'' pad = len(data) % block_size if pad: # pragma: no branch data = data + bytes(range(1, block_size + 1 - pad)) if cipher: if mode == 'chacha': data, mac = cipher.encrypt_and_sign(b'', data, UInt64(0)) elif mode == 'gcm': data, mac = cipher.encrypt_and_sign(b'', data) else: data, mac = cipher.encrypt(data), b'' data = b''.join((_OPENSSH_KEY_V1, String(alg), String(kdf), String(kdf_data), UInt32(nkeys), String(self.get_ssh_public_key()), String(data), mac)) return (b'-----BEGIN OPENSSH PRIVATE KEY-----\n' + _wrap_base64(data, _OPENSSH_WRAP_LEN) + b'-----END OPENSSH PRIVATE KEY-----\n') else: raise KeyExportError('Unknown export format') def export_public_key(self, format_name): """Export a public key in the requested format This function returns this object's public key encoded in the requested format. Available formats include: pkcs1-der, pkcs1-pem, pkcs8-der, pkcs8-pem, openssh, rfc4716 :param string format: The format to export the key in. """ if format_name in ('pkcs1-der', 'pkcs1-pem'): data = der_encode(self.encode_pkcs1_public()) if format_name == 'pkcs1-pem': keytype = self.pem_name + b' PUBLIC KEY' data = (b'-----BEGIN ' + keytype + b'-----\n' + _wrap_base64(data) + b'-----END ' + keytype + b'-----\n') return data elif format_name in ('pkcs8-der', 'pkcs8-pem'): alg_params, data = self.encode_pkcs8_public() data = der_encode(((self.pkcs8_oid, alg_params), BitString(data))) if format_name == 'pkcs8-pem': data = (b'-----BEGIN PUBLIC KEY-----\n' + _wrap_base64(data) + b'-----END PUBLIC KEY-----\n') return data elif format_name == 'openssh': data = self.get_ssh_public_key() return self.algorithm + b' ' + binascii.b2a_base64(data) elif format_name == 'rfc4716': data = self.get_ssh_public_key() return (b'---- BEGIN SSH2 PUBLIC KEY ----\n' + _wrap_base64(data) + b'---- END SSH2 PUBLIC KEY ----\n') else: raise KeyExportError('Unknown export format') def write_private_key(self, filename, *args, **kwargs): """Write a private key to a file in the requested format This function is a simple wrapper around export_private_key which writes the exported key data to a file. :param string filename: The filename to write the private key to. :param \\*args,\\ \\*\\*kwargs: Additional arguments to pass through to :func:`export_private_key`. """ with open(filename, 'wb') as f: f.write(self.export_private_key(*args, **kwargs)) def write_public_key(self, filename, *args, **kwargs): """Write a public key to a file in the requested format This function is a simple wrapper around export_public_key which writes the exported key data to a file. :param string filename: The filename to write the public key to. :param \\*args,\\ \\*\\*kwargs: Additional arguments to pass through to :func:`export_public_key`. """ with open(filename, 'wb') as f: f.write(self.export_public_key(*args, **kwargs)) class SSHCertificate: """Parent class which holds an SSH certificate""" # pylint: disable=bad-whitespace _user_critical_options = {} _user_extensions = {} _host_critical_options = {} _host_extensions = {} # pylint: enable=bad-whitespace def __init__(self, packet, algorithm, key_handler, key_params, serial, cert_type, key_id, valid_principals, valid_after, valid_before, options, extensions): signing_key = decode_ssh_public_key(packet.get_string()) msg = packet.get_consumed_payload() signature = packet.get_string() packet.check_end() if not signing_key.verify(msg, signature): raise KeyImportError('Invalid certificate signature') self.algorithm = algorithm self.key = key_handler.make_public(*key_params) self.data = packet.get_consumed_payload() self.options = {} self.principals = [] self.signing_key = signing_key self._serial = serial self._cert_type = cert_type self._key_id = key_id packet = SSHPacket(valid_principals) while packet: try: principal = packet.get_string().decode('utf-8') except UnicodeDecodeError: raise KeyImportError('Invalid characters in principal name') self.principals.append(principal) self._valid_after = valid_after self._valid_before = valid_before if cert_type == CERT_TYPE_USER: self._decode_options(options, self._user_critical_options, True) self._decode_options(extensions, self._user_extensions, False) elif cert_type == CERT_TYPE_HOST: self._decode_options(options, self._host_critical_options, True) self._decode_options(extensions, self._host_extensions, False) else: raise KeyImportError('Unknown certificate type') @staticmethod def _parse_force_command(packet): """Parse a force-command option""" try: return packet.get_string().decode('utf-8') except UnicodeDecodeError: raise KeyImportError('Invalid characters in command') from None @staticmethod def _parse_source_address(packet): """Parse a source-address option""" try: return [ip_network(addr.decode('ascii')) for addr in packet.get_namelist()] except (UnicodeDecodeError, ValueError): raise KeyImportError('Invalid source address') from None def _decode_options(self, options, valid_options, critical=True): """Decode options found in this certificate""" packet = SSHPacket(options) while packet: name = packet.get_string() data_packet = SSHPacket(packet.get_string()) decoder = valid_options.get(name) if decoder: data = decoder(data_packet) if callable(decoder) else True data_packet.check_end() self.options[name.decode('ascii')] = data elif critical: raise KeyImportError('Unrecognized critical option: %s' % name.decode('ascii', errors='replace')) def validate(self, cert_type, principal): """Validate the certificate type, validity period, and principal This method validates that the certificate is of the specified type, that the current time is within the certificate validity period, and that the principal being authenticated is one of the certificate's valid principals. :param integer cert_type: The expected :ref:`certificate type `. :param string principal: The principal being authenticated. :raises: :exc:`ValueError` if any of the validity checks fail """ if self._cert_type != cert_type: raise ValueError('Invalid certificate type') now = time.time() if now < self._valid_after: raise ValueError('Certificate not yet valid') if now >= self._valid_before: raise ValueError('Certificate expired') if principal and self.principals and principal not in self.principals: raise ValueError('Certificate principal mismatch') class SSHCertificateV00(SSHCertificate): """Decoder class for version 00 SSH certificates""" _user_critical_options = { b'force-command': SSHCertificate._parse_force_command, b'permit-X11-forwarding': True, b'permit-agent-forwarding': True, b'permit-port-forwarding': True, b'permit-pty': True, b'permit-user-rc': True, b'source-address': SSHCertificate._parse_source_address } def __init__(self, packet, algorithm, key_handler): key_params = key_handler.decode_ssh_public(packet) cert_type = packet.get_uint32() key_id = packet.get_string() valid_principals = packet.get_string() valid_after = packet.get_uint64() valid_before = packet.get_uint64() options = packet.get_string() _ = packet.get_string() # nonce _ = packet.get_string() # reserved super().__init__(packet, algorithm, key_handler, key_params, 0, cert_type, key_id, valid_principals, valid_after, valid_before, options, b'') class SSHCertificateV01(SSHCertificate): """Decoder class for version 01 SSH certificates""" _user_critical_options = { b'force-command': SSHCertificate._parse_force_command, b'source-address': SSHCertificate._parse_source_address } _user_extensions = { b'permit-X11-forwarding': True, b'permit-agent-forwarding': True, b'permit-port-forwarding': True, b'permit-pty': True, b'permit-user-rc': True } def __init__(self, packet, algorithm, key_handler): _ = packet.get_string() # nonce key_params = key_handler.decode_ssh_public(packet) serial = packet.get_uint64() cert_type = packet.get_uint32() key_id = packet.get_string() valid_principals = packet.get_string() valid_after = packet.get_uint64() valid_before = packet.get_uint64() options = packet.get_string() extensions = packet.get_string() _ = packet.get_string() # reserved super().__init__(packet, algorithm, key_handler, key_params, serial, cert_type, key_id, valid_principals, valid_after, valid_before, options, extensions) def _decode_pkcs1_private(pem_name, key_data): """Decode a PKCS#1 format private key""" handler = _pem_map.get(pem_name) if handler is None: raise KeyImportError('Unknown PEM key type: %s' % pem_name.decode('ascii')) key_params = handler.decode_pkcs1_private(key_data) if key_params is None: raise KeyImportError('Invalid %s private key' % pem_name.decode('ascii')) return handler.make_private(*key_params) def _decode_pkcs1_public(pem_name, key_data): """Decode a PKCS#1 format public key""" handler = _pem_map.get(pem_name) if handler is None: raise KeyImportError('Unknown PEM key type: %s' % pem_name.decode('ascii')) key_params = handler.decode_pkcs1_public(key_data) if key_params is None: raise KeyImportError('Invalid %s public key' % pem_name.decode('ascii')) return handler.make_public(*key_params) def _decode_pkcs8_private(key_data): """Decode a PKCS#8 format private key""" if (isinstance(key_data, tuple) and len(key_data) >= 3 and key_data[0] in (0, 1) and isinstance(key_data[1], tuple) and len(key_data[1]) == 2 and isinstance(key_data[2], bytes)): alg, alg_params = key_data[1] handler = _pkcs8_oid_map.get(alg) if handler is None: raise KeyImportError('Unknown PKCS#8 algorithm') key_params = handler.decode_pkcs8_private(alg_params, key_data[2]) if key_params is None: raise KeyImportError('Invalid %s private key' % handler.pem_name.decode('ascii')) return handler.make_private(*key_params) else: raise KeyImportError('Invalid PKCS#8 private key') def _decode_pkcs8_public(key_data): """Decode a PKCS#8 format public key""" if (isinstance(key_data, tuple) and len(key_data) == 2 and isinstance(key_data[0], tuple) and len(key_data[0]) == 2 and isinstance(key_data[1], BitString) and key_data[1].unused == 0): alg, alg_params = key_data[0] handler = _pkcs8_oid_map.get(alg) if handler is None: raise KeyImportError('Unknown PKCS#8 algorithm') key_params = handler.decode_pkcs8_public(alg_params, key_data[1].value) if key_params is None: raise KeyImportError('Invalid %s public key' % handler.pem_name.decode('ascii')) return handler.make_public(*key_params) else: raise KeyImportError('Invalid PKCS#8 public key') def _decode_openssh_private(data, passphrase): """Decode an OpenSSH format private key""" try: if not data.startswith(_OPENSSH_KEY_V1): raise KeyImportError('Unrecognized OpenSSH private key type') data = data[len(_OPENSSH_KEY_V1):] packet = SSHPacket(data) cipher_name = packet.get_string() kdf = packet.get_string() kdf_data = packet.get_string() nkeys = packet.get_uint32() _ = packet.get_string() # public_key key_data = packet.get_string() mac = packet.get_remaining_payload() if nkeys != 1: raise KeyImportError('Invalid OpenSSH private key') if cipher_name != b'none': if not _bcrypt_available: # pragma: no cover raise KeyEncryptionError('OpenSSH private key encryption ' 'requires bcrypt') if passphrase is None: raise KeyImportError('Passphrase must be specified to import ' 'encrypted private keys') try: key_size, iv_size, block_size, mode = \ get_encryption_params(cipher_name) except KeyError: raise KeyEncryptionError('Unknown cipher: %s' % cipher_name.decode('ascii')) from None if kdf != b'bcrypt': raise KeyEncryptionError('Unknown kdf: %s' % kdf.decode('ascii')) packet = SSHPacket(kdf_data) salt = packet.get_string() rounds = packet.get_uint32() packet.check_end() if isinstance(passphrase, str): passphrase = passphrase.encode('utf-8') try: # pylint: disable=no-member key = bcrypt.kdf(passphrase, salt, key_size + iv_size, rounds) # pylint: enable=no-member except ValueError: raise KeyEncryptionError('Invalid OpenSSH ' 'private key') from None cipher = get_cipher(cipher_name, key[:key_size], key[key_size:]) if mode == 'chacha': key_data = cipher.verify_and_decrypt(b'', key_data, UInt64(0), mac) mac = b'' elif mode == 'gcm': key_data = cipher.verify_and_decrypt(b'', key_data, mac) mac = b'' else: key_data = cipher.decrypt(key_data) if key_data is None: raise KeyEncryptionError('Incorrect passphrase') block_size = max(block_size, 8) else: block_size = 8 if mac: raise KeyImportError('Invalid OpenSSH private key') packet = SSHPacket(key_data) check1 = packet.get_uint32() check2 = packet.get_uint32() if check1 != check2: if cipher_name != b'none': raise KeyEncryptionError('Incorrect passphrase') from None else: raise KeyImportError('Invalid OpenSSH private key') alg = packet.get_string() handler = _public_key_alg_map.get(alg) if not handler: raise KeyImportError('Unknown OpenSSH private key algorithm') key_params = handler.decode_ssh_private(packet) _ = packet.get_string() # comment pad = packet.get_remaining_payload() if len(pad) >= block_size or pad != bytes(range(1, len(pad) + 1)): raise KeyImportError('Invalid OpenSSH private key') return handler.make_private(*key_params) except PacketDecodeError: raise KeyImportError('Invalid OpenSSH private key') def _decode_der_private(data, passphrase): """Decode a DER format private key""" try: # pylint: disable=unpacking-non-sequence key_data, end = der_decode(data, partial_ok=True) # pylint: enable=unpacking-non-sequence except ASN1DecodeError: raise KeyImportError('Invalid DER private key') from None # First, if there's a passphrase, try to decrypt PKCS#8 if passphrase is not None: try: key_data = pkcs8_decrypt(key_data, passphrase) except KeyEncryptionError: # Decryption failed - try decoding it as unencrypted pass # Then, try to decode PKCS#8 try: return _decode_pkcs8_private(key_data), end except KeyImportError: # PKCS#8 failed - try PKCS#1 instead pass # If that fails, try each of the possible PKCS#1 encodings for pem_name in _pem_map: try: return _decode_pkcs1_private(pem_name, key_data), end except KeyImportError: # Try the next PKCS#1 encoding pass raise KeyImportError('Invalid DER private key') def _decode_der_public(data): """Decode a DER format public key""" try: # pylint: disable=unpacking-non-sequence key_data, end = der_decode(data, partial_ok=True) # pylint: enable=unpacking-non-sequence except ASN1DecodeError: raise KeyImportError('Invalid DER public key') from None # First, try to decode PKCS#8 try: return _decode_pkcs8_public(key_data), end except KeyImportError: # PKCS#8 failed - try PKCS#1 instead pass # If that fails, try each of the possible PKCS#1 encodings for pem_name in _pem_map: try: return _decode_pkcs1_public(pem_name, key_data), end except KeyImportError: # Try the next PKCS#1 encoding pass raise KeyImportError('Invalid DER public key') def _decode_pem(lines, keytype): """Decode a PEM format key""" start = None line = '' for i, line in enumerate(lines): line = line.strip() if (line.startswith(b'-----BEGIN ') and line.endswith(b' ' + keytype + b'-----')): start = i+1 break if not start: raise KeyImportError('Missing PEM header of type %s' % keytype.decode('ascii')) pem_name = line[11:-(6+len(keytype))].strip() if pem_name: keytype = pem_name + b' ' + keytype headers = {} for start, line in enumerate(lines[start:], start): line = line.strip() if b':' in line: hdr, value = line.split(b':') headers[hdr.strip()] = value.strip() else: break end = None tail = b'-----END ' + keytype + b'-----' for i, line in enumerate(lines[start:], start): line = line.strip() if line == tail: end = i break if not end: raise KeyImportError('Missing PEM footer') try: data = binascii.a2b_base64(b''.join(lines[start:end])) except binascii.Error: raise KeyImportError('Invalid PEM data') from None return pem_name, headers, data, end+1 def _decode_pem_private(lines, passphrase): """Decode a PEM format private key""" pem_name, headers, data, end = _decode_pem(lines, b'PRIVATE KEY') if pem_name == b'OPENSSH': return _decode_openssh_private(data, passphrase), end if headers.get(b'Proc-Type') == b'4,ENCRYPTED': if passphrase is None: raise KeyImportError('Passphrase must be specified to import ' 'encrypted private keys') dek_info = headers.get(b'DEK-Info', b'').split(b',') if len(dek_info) != 2: raise KeyImportError('Invalid PEM encryption params') alg, iv = dek_info try: iv = binascii.a2b_hex(iv) except binascii.Error: raise KeyImportError('Invalid PEM encryption params') from None try: data = pkcs1_decrypt(data, alg, iv, passphrase) except KeyEncryptionError: raise KeyImportError('Unable to decrypt PKCS#1 ' 'private key') from None try: key_data = der_decode(data) except ASN1DecodeError: raise KeyImportError('Invalid PEM private key') from None if pem_name == b'ENCRYPTED': if passphrase is None: raise KeyImportError('Passphrase must be specified to import ' 'encrypted private keys') pem_name = b'' try: key_data = pkcs8_decrypt(key_data, passphrase) except KeyEncryptionError: raise KeyImportError('Unable to decrypt PKCS#8 ' 'private key') from None if pem_name: return _decode_pkcs1_private(pem_name, key_data), end else: return _decode_pkcs8_private(key_data), end def _decode_pem_public(lines): """Decode a PEM format public key""" pem_name, _, data, end = _decode_pem(lines, b'PUBLIC KEY') try: key_data = der_decode(data) except ASN1DecodeError: raise KeyImportError('Invalid PEM public key') from None if pem_name: return _decode_pkcs1_public(pem_name, key_data), end else: return _decode_pkcs8_public(key_data), end def _decode_openssh(line): """Decode an OpenSSH format public key or certificate""" line = line.split() if len(line) < 2: raise KeyImportError('Invalid OpenSSH public key or certificate') try: return binascii.a2b_base64(line[1]) except binascii.Error: raise KeyImportError('Invalid OpenSSH public key ' 'or certificate') from None def _decode_rfc4716(lines): """Decode an RFC 4716 format public key""" start = None for i, line in enumerate(lines): line = line.strip() if line == b'---- BEGIN SSH2 PUBLIC KEY ----': start = i+1 break if not start: raise KeyImportError('Missing RFC 4716 header') continuation = False for start, line in enumerate(lines[start:], start): line = line.strip() if continuation or b':' in line: continuation = line.endswith(b'\\') else: break end = None for i, line in enumerate(lines[start:], start): line = line.strip() if line == b'---- END SSH2 PUBLIC KEY ----': end = i break if not end: raise KeyImportError('Missing RFC 4716 footer') try: return binascii.a2b_base64(b''.join(lines[start:end])), end+1 except binascii.Error: raise KeyImportError('Invalid RFC 4716 public key ' 'or certificate') from None def register_public_key_alg(algorithm, handler): """Register a new public key algorithm""" _public_key_alg_map[algorithm] = handler _public_key_algs.append(algorithm) if handler.pem_name: _pem_map[handler.pem_name] = handler if handler.pkcs8_oid: _pkcs8_oid_map[handler.pkcs8_oid] = handler def register_certificate_alg(algorithm, key_handler, cert_handler): """Register a new certificate algorithm""" _certificate_alg_map[algorithm] = (key_handler, cert_handler) _certificate_algs.append(algorithm) def get_public_key_algs(): """Return supported public key algorithms""" return _public_key_algs def get_certificate_algs(): """Return supported certificate-based public key algorithms""" return _certificate_algs def decode_ssh_public_key(data): """Decode a packetized SSH public key""" try: packet = SSHPacket(data) alg = packet.get_string() handler = _public_key_alg_map.get(alg) if handler: key_params = handler.decode_ssh_public(packet) packet.check_end() key = handler.make_public(*key_params) key.algorithm = alg return key else: raise KeyImportError('Unknown key algorithm: %s' % alg.decode('ascii', errors='replace')) except PacketDecodeError: raise KeyImportError('Invalid public key') from None def decode_ssh_certificate(data): """Decode a packetized SSH certificate""" try: packet = SSHPacket(data) alg = packet.get_string() key_handler, cert_handler = _certificate_alg_map.get(alg, (None, None)) if cert_handler: return cert_handler(packet, alg, key_handler) else: raise KeyImportError('Unknown certificate algorithm: %s' % alg.decode('ascii', errors='replace')) except PacketDecodeError: raise KeyImportError('Invalid certificate') from None def import_private_key(data, passphrase=None): """Import a private key This function imports a private key encoded in PKCS#1 or PKCS#8 DER or PEM format or OpenSSH format. Encrypted private keys can be imported by specifying the passphrase needed to decrypt them. :param data: The data to import. :param string passphrase: (optional) The passphrase to use to decrypt the key. :type data: bytes or ASCII string :returns: An :class:`SSHKey` private key """ if isinstance(data, str): try: data = data.encode('ascii') except UnicodeEncodeError: raise KeyImportError('Invalid encoding for private key') from None stripped_key = data.lstrip() if stripped_key.startswith(b'-----'): key, _ = _decode_pem_private(stripped_key.splitlines(), passphrase) else: key, _ = _decode_der_private(data, passphrase) return key def import_public_key(data): """Import a public key This function imports a public key encoded in OpenSSH, RFC4716, or PKCS#1 or PKCS#8 DER or PEM format. :param data: The data to import. :type data: bytes or ASCII string :returns: An :class:`SSHKey` public key """ if isinstance(data, str): try: data = data.encode('ascii') except UnicodeEncodeError: raise KeyImportError('Invalid encoding for public key') from None stripped_key = data.lstrip() if stripped_key.startswith(b'-----'): key, _ = _decode_pem_public(stripped_key.splitlines()) elif stripped_key.startswith(b'---- '): data, _ = _decode_rfc4716(stripped_key.splitlines()) key = decode_ssh_public_key(data) elif data.startswith(b'\x30'): key, _ = _decode_der_public(data) elif data: data = _decode_openssh(stripped_key.splitlines()[0]) key = decode_ssh_public_key(data) else: raise KeyImportError('Invalid public key') return key def import_certificate(data): """Import a certificate This function imports an SSH certificate in OpenSSH or RFC4716 format. :param data: The data to import. :type data: bytes or ASCII string :returns: An :class:`SSHCertificate` certificate """ if isinstance(data, str): try: data = data.encode('ascii') except UnicodeEncodeError: raise KeyImportError('Invalid encoding for certificate') from None stripped_key = data.lstrip() if stripped_key.startswith(b'---- '): data, _ = _decode_rfc4716(stripped_key.splitlines()) else: data = _decode_openssh(stripped_key.splitlines()[0]) return decode_ssh_certificate(data) def read_private_key(filename, passphrase=None): """Read a private key from a file This function reads a private key from a file. See the function :func:`import_private_key` for information about the formats supported. :param string filename: The file to read the key from. :param string passphrase: (optional) The passphrase to use to decrypt the key. :returns: An :class:`SSHKey` private key """ with open(filename, 'rb') as f: return import_private_key(f.read(), passphrase) def read_public_key(filename): """Read a public key from a file This function reads a public key from a file. See the function :func:`import_public_key` for information about the formats supported. :param string filename: The file to read the key from. :returns: An :class:`SSHKey` public key """ with open(filename, 'rb') as f: return import_public_key(f.read()) def read_certificate(filename): """Read a certificate from a file This function reads an SSH certificate from a file. See the function :func:`import_certificate` for information about the formats supported. :param string filename: The file to read the certificate from. :returns: An :class:`SSHCertificate` certificate """ with open(filename, 'rb') as f: return import_certificate(f.read()) def read_private_key_list(filename, passphrase=None): """Read a list of private keys from a file This function reads a list of private keys from a file. See the function :func:`import_private_key` for information about the formats supported. If any of the keys are encrypted, they must all be encrypted with the same passphrase. :param string filename: The file to read the keys from. :param string passphrase: (optional) The passphrase to use to decrypt the keys. :returns: A list of :class:`SSHKey` private keys """ with open(filename, 'rb') as f: data = f.read() keys = [] stripped_key = data.strip() if stripped_key.startswith(b'-----'): lines = stripped_key.splitlines() while lines: key, end = _decode_pem_private(lines, passphrase) keys.append(key) lines = lines[end:] else: while data: key, end = _decode_der_private(data, passphrase) keys.append(key) data = data[end:] return keys def read_public_key_list(filename): """Read a list of public keys from a file This function reads a list of public keys from a file. See the function :func:`import_public_key` for information about the formats supported. :param string filename: The file to read the keys from. :returns: A list of :class:`SSHKey` public keys """ with open(filename, 'rb') as f: data = f.read() keys = [] stripped_key = data.strip() if stripped_key.startswith(b'-----'): lines = stripped_key.splitlines() while lines: key, end = _decode_pem_public(lines) keys.append(key) lines = lines[end:] elif stripped_key.startswith(b'---- '): lines = stripped_key.splitlines() while lines: data, end = _decode_rfc4716(lines) keys.append(decode_ssh_public_key(data)) lines = lines[end:] elif data.startswith(b'\x30'): while data: key, end = _decode_der_public(data) keys.append(key) data = data[end:] else: for line in stripped_key.splitlines(): keys.append(decode_ssh_public_key(_decode_openssh(line))) return keys def read_certificate_list(filename): """Read a list of certificates from a file This function reads a list of SSH certificates from a file. See the function :func:`import_certificate` for information about the formats supported. :param string filename: The file to read the certificates from. :returns: A list of :class:`SSHCertificate` certificates """ with open(filename, 'rb') as f: data = f.read() certs = [] stripped_key = data.strip() if stripped_key.startswith(b'---- '): lines = stripped_key.splitlines() while lines: data, end = _decode_rfc4716(lines) certs.append(decode_ssh_certificate(data)) lines = lines[end:] else: for line in stripped_key.splitlines(): certs.append(decode_ssh_certificate(_decode_openssh(line))) return certs asyncssh-1.3.0/asyncssh/rsa.py000066400000000000000000000135371260630620200163460ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """RSA public key encryption handler""" from .asn1 import ASN1DecodeError, ObjectIdentifier, der_encode, der_decode from .crypto import RSAPrivateKey, RSAPublicKey from .misc import all_ints from .packet import MPInt, String, PacketDecodeError, SSHPacket from .public_key import SSHKey, SSHCertificateV00, SSHCertificateV01 from .public_key import KeyExportError from .public_key import register_public_key_alg, register_certificate_alg # Short variable names are used here, matching names in the spec # pylint: disable=invalid-name class _RSAKey(SSHKey): """Handler for RSA public key encryption""" algorithm = b'ssh-rsa' pem_name = b'RSA' pkcs8_oid = ObjectIdentifier('1.2.840.113549.1.1.1') def __init__(self, key): self._key = key def __eq__(self, other): # This isn't protected access - both objects are _RSAKey instances # pylint: disable=protected-access return (isinstance(other, self.__class__) and self._key.n == other._key.n and self._key.e == other._key.e and self._key.d == other._key.d) def __hash__(self): return hash((self._key.n, self._key.e, self._key.d, self._key.p, self._key.q)) @classmethod def make_private(cls, *args): """Construct an RSA private key""" return cls(RSAPrivateKey(*args)) @classmethod def make_public(cls, *args): """Construct an RSA public key""" return cls(RSAPublicKey(*args)) @classmethod def decode_pkcs1_private(cls, key_data): """Decode a PKCS#1 format RSA private key""" if (isinstance(key_data, tuple) and all_ints(key_data) and len(key_data) >= 9): return key_data[1:9] else: return None @classmethod def decode_pkcs1_public(cls, key_data): """Decode a PKCS#1 format RSA public key""" if (isinstance(key_data, tuple) and all_ints(key_data) and len(key_data) == 2): return key_data else: return None @classmethod def decode_pkcs8_private(cls, alg_params, data): """Decode a PKCS#8 format RSA private key""" if alg_params is not None: return None try: key_data = der_decode(data) except ASN1DecodeError: return None return cls.decode_pkcs1_private(key_data) @classmethod def decode_pkcs8_public(cls, alg_params, data): """Decode a PKCS#8 format RSA public key""" if alg_params is not None: return None try: key_data = der_decode(data) except ASN1DecodeError: return None return cls.decode_pkcs1_public(key_data) @classmethod def decode_ssh_private(cls, packet): """Decode an SSH format RSA private key""" n = packet.get_mpint() e = packet.get_mpint() d = packet.get_mpint() iqmp = packet.get_mpint() p = packet.get_mpint() q = packet.get_mpint() return n, e, d, p, q, d % (p-1), d % (q-1), iqmp @classmethod def decode_ssh_public(cls, packet): """Decode an SSH format RSA public key""" e = packet.get_mpint() n = packet.get_mpint() return n, e def encode_pkcs1_private(self): """Encode a PKCS#1 format RSA private key""" if not self._key.d: raise KeyExportError('Key is not private') return (0, self._key.n, self._key.e, self._key.d, self._key.p, self._key.q, self._key.dmp1, self._key.dmq1, self._key.iqmp) def encode_pkcs1_public(self): """Encode a PKCS#1 format RSA public key""" return self._key.n, self._key.e def encode_pkcs8_private(self): """Encode a PKCS#8 format RSA private key""" return None, der_encode(self.encode_pkcs1_private()) def encode_pkcs8_public(self): """Encode a PKCS#8 format RSA public key""" return None, der_encode(self.encode_pkcs1_public()) def encode_ssh_private(self): """Encode an SSH format RSA private key""" if not self._key.d: raise KeyExportError('Key is not private') return b''.join((MPInt(self._key.n), MPInt(self._key.e), MPInt(self._key.d), MPInt(self._key.iqmp), MPInt(self._key.p), MPInt(self._key.q))) def encode_ssh_public(self): """Encode an SSH format RSA public key""" return b''.join((MPInt(self._key.e), MPInt(self._key.n))) def sign(self, data): """Return a signature of the specified data using this key""" if not self._key.d: raise ValueError('Private key needed for signing') sig = self._key.sign(data) return b''.join((String(self.algorithm), String(sig))) def verify(self, data, sig): """Verify a signature of the specified data using this key""" try: packet = SSHPacket(sig) if packet.get_string() != self.algorithm: return False sig = packet.get_string() packet.check_end() return self._key.verify(data, sig) except PacketDecodeError: return False register_public_key_alg(b'ssh-rsa', _RSAKey) register_certificate_alg(b'ssh-rsa-cert-v01@openssh.com', _RSAKey, SSHCertificateV01) register_certificate_alg(b'ssh-rsa-cert-v00@openssh.com', _RSAKey, SSHCertificateV00) asyncssh-1.3.0/asyncssh/saslprep.py000066400000000000000000000061101260630620200173770ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """SASLprep implementation This module implements the stringprep algorithm defined in RFC 3454 and the SASLprep profile of stringprep defined in RFC 4013. This profile is used to normalize usernames and passwords sent in the SSH protocol. """ # The stringprep module should not be flagged as deprecated # pylint: disable=deprecated-module import stringprep # pylint: enable=deprecated-module import unicodedata class SASLPrepError(ValueError): """Invalid data provided to saslprep""" def _check_bidi(s): """Enforce bidirectional character check from RFC 3454 (stringprep)""" r_and_al_cat = False l_cat = False for c in s: if not r_and_al_cat and stringprep.in_table_d1(c): r_and_al_cat = True if not l_cat and stringprep.in_table_d2(c): l_cat = True if r_and_al_cat and l_cat: raise SASLPrepError('Both RandALCat and LCat characters present') if r_and_al_cat and not (stringprep.in_table_d1(s[0]) and stringprep.in_table_d1(s[-1])): raise SASLPrepError('RandALCat character not at both start and end') def _stringprep(s, check_unassigned, mapping, normalization, prohibited, bidi): """Implement a stringprep profile as defined in RFC 3454""" if not isinstance(s, str): raise TypeError('argument 0 must be str, not %s' % type(s).__name__) if check_unassigned: # pragma: no branch for c in s: if stringprep.in_table_a1(c): raise SASLPrepError('Unassigned character: %r' % c) if mapping: # pragma: no branch s = mapping(s) if normalization: # pragma: no branch s = unicodedata.normalize(normalization, s) if prohibited: # pragma: no branch for c in s: for lookup in prohibited: if lookup(c): raise SASLPrepError('Prohibited character: %r' % c) if bidi: # pragma: no branch _check_bidi(s) return s def _map_saslprep(s): """Map stringprep table B.1 to nothing and C.1.2 to ASCII space""" r = [] for c in s: if stringprep.in_table_c12(c): r.append(' ') elif not stringprep.in_table_b1(c): r.append(c) return ''.join(r) def saslprep(s): """Implement SASLprep profile defined in RFC 4013""" prohibited = (stringprep.in_table_c12, stringprep.in_table_c21_c22, stringprep.in_table_c3, stringprep.in_table_c4, stringprep.in_table_c5, stringprep.in_table_c6, stringprep.in_table_c7, stringprep.in_table_c8, stringprep.in_table_c9) return _stringprep(s, True, _map_saslprep, 'NFKC', prohibited, True) asyncssh-1.3.0/asyncssh/session.py000066400000000000000000000372011260630620200172360ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """SSH session handlers""" class SSHSession: """SSH session handler""" def connection_made(self, chan): """Called when a channel is opened successfully This method is called when a channel is opened successfully. The channel parameter should be stored if needed for later use. :param chan: The channel which was successfully opened. :type chan: :class:`SSHClientChannel` """ def connection_lost(self, exc): """Called when a channel is closed This method is called when a channel is closed. If the channel is shut down cleanly, *exc* will be ``None``. Otherwise, it will be an exception explaining the reason for the channel close. :param exc: The exception which caused the channel to close, or ``None`` if the channel closed cleanly. :type exc: :class:`Exception` """ def session_started(self): """Called when the session is started This method is called when a session has started up. For client and server sessions, this will be called once a shell, exec, or subsystem request has been successfully completed. For TCP sessions, it will be called immediately after the connection is opened. """ def data_received(self, data, datatype): """Called when data is received on the channel This method is called when data is received on the channel. If an encoding was specified when the channel was created, the data will be delivered as a string after decoding with the requested encoding. Otherwise, the data will be delivered as bytes. :param data: The data received on the channel :param datatype: The extended data type of the data, from :ref:`extended data types ` :type data: string or bytes """ def eof_received(self): """Called when EOF is received on the channel This method is called when an end-of-file indication is received on the channel, after which no more data will be received. If this method returns ``True``, the channel remains half open and data may still be sent. Otherwise, the channel is automatically closed after this method returns. This is the default behavior. """ def pause_writing(self): """Called when the write buffer becomes full This method is called when the channel's write buffer becomes full and no more data can be sent until the remote system adjusts its window. While data can still be buffered locally, applications may wish to stop producing new data until the write buffer has drained. """ def resume_writing(self): """Called when the write buffer has sufficiently drained This method is called when the channel's send window reopens and enough data has drained from the write buffer to allow the application to produce more data. """ class SSHClientSession(SSHSession): """SSH client session handler Applications should subclass this when implementing an SSH client session handler. The functions listed below should be implemented to define application-specific behavior. In particular, the standard ``asyncio`` protocol methods such as :meth:`connection_made`, :meth:`connection_lost`, :meth:`data_received`, :meth:`eof_received`, :meth:`pause_writing`, and :meth:`resume_writing` are all supported. In addition, :meth:`session_started` is called as soon as the SSH session is fully started, :meth:`xon_xoff_requested` can be used to determine if the server wants the client to support XON/XOFF flow control, and :meth:`exit_status_received` and :meth:`exit_signal_received` can be used to receive session exit information. """ def xon_xoff_requested(self, client_can_do): """XON/XOFF flow control has been enabled or disabled This method is called to notify the client whether or not to enable XON/XOFF flow control. If client_can_do is ``True`` and output is being sent to an interactive terminal the application should allow input of Control-S and Control-Q to pause and resume output, respectively. If client_can_do is ``False``, Control-S and Control-Q should be treated as normal input and passed through to the server. Non-interactive applications can ignore this request. By default, this message is ignored. :param boolean client_can_do: Whether or not to enable XON/XOFF flow control """ def exit_status_received(self, status): """A remote exit status has been received for this session This method is called when the shell, command, or subsystem running on the server terminates and returns an exit status. A zero exit status generally means that the operation was successful. This call will generally be followed by a call to :meth:`connection_lost`. By default, the exit status is ignored. :param integer status: The exit status returned by the remote process """ def exit_signal_received(self, signal, core_dumped, msg, lang): """A remote exit signal has been received for this session This method is called when the shell, command, or subsystem running on the server terminates abnormally with a signal. A more detailed error may also be provided, along with an indication of whether the remote process dumped core. This call will generally be followed by a call to :meth:`connection_lost`. By default, exit signals are ignored. :param string signal: The signal which caused the remote process to exit :param boolean core_dumped: Whether or not the remote process dumped core :param msg: Details about what error occurred :param lang: The language the error message is in """ class SSHServerSession(SSHSession): """SSH server session handler Applications should subclass this when implementing an SSH server session handler. The functions listed below should be implemented to define application-specific behavior. In particular, the standard ``asyncio`` protocol methods such as :meth:`connection_made`, :meth:`connection_lost`, :meth:`data_received`, :meth:`eof_received`, :meth:`pause_writing`, and :meth:`resume_writing` are all supported. In addition, :meth:`pty_requested` is called when the client requests a pseudo-terminal, one of :meth:`shell_requested`, :meth:`exec_requested`, or :meth:`subsystem_requested` is called depending on what type of session the client wants to start, :meth:`session_started` is called once the SSH session is fully started, :meth:`terminal_size_changed` is called when the client's terminal size changes, :meth:`signal_received` is called when the client sends a signal, and :meth:`break_received` is called when the client sends a break. """ def pty_requested(self, term_type, term_size, term_modes): """A psuedo-terminal has been requested This method is called when the client sends a request to allocate a pseudo-terminal with the requested terminal type, size, and POSIX terminal modes. This method should return ``True`` if the request for the pseudo-terminal is accepted. Otherwise, it should return ``False`` to reject the request. By default, requests to allocate a pseudo-terminal are accepted but nothing is done with the associated terminal information. Applications wishing to use this information should implement this method and have it return ``True``, or call :meth:`get_terminal_type() `, :meth:`get_terminal_size() `, or :meth:`get_terminal_mode() ` on the :class:`SSHServerChannel` to get the information they need after a shell, command, or subsystem is started. :param string term: Terminal type to set for this session :param tuple term_size: Terminal size to set for this session provided as a tuple of four integers: the width and height of the terminal in characters followed by the width and height of the terminal in pixels :param dictionary term_modes: POSIX terminal modes to set for this session, where keys are taken from :ref:`POSIX terminal modes ` with values defined in section 8 of :rfc:`4254#section-8`. :returns: A boolean indicating if the request for a pseudo-terminal was allowed or not """ # pylint: disable=no-self-use,unused-argument return True def terminal_size_changed(self, width, height, pixwidth, pixheight): """The terminal size has changed This method is called when a client requests a pseudo-terminal and again whenever the the size of he client's terminal window changes. By default, this information is ignored, but applications wishing to use the terminal size can implement this method to get notified whenever it changes. :param integer width: The width of the terminal in characters :param integer height: The height of the terminal in characters :param integer pixwidth: (optional) The width of the terminal in pixels :param integer pixheight: (optional) The height of the terminal in pixels """ # pylint: disable=no-self-use,unused-argument def shell_requested(self): """The client has requested a shell This method should be implemented by the application to perform whatever processing is required when a client makes a request to open an interactive shell. It should return ``True`` to accept the request, or ``False`` to reject it. If the application returns ``True``, the :meth:`session_started` method will be called once the channel is fully open. No output should be sent until this method is called. By default this method returns ``False`` to reject all requests. :returns: A boolean indicating if the shell request was allowed or not """ # pylint: disable=no-self-use,unused-argument return False def exec_requested(self, command): """The client has requested to execute a command This method should be implemented by the application to perform whatever processing is required when a client makes a request to execute a command. It should return ``True`` to accept the request, or ``False`` to reject it. If the application returns ``True``, the :meth:`session_started` method will be called once the channel is fully open. No output should be sent until this method is called. By default this method returns ``False`` to reject all requests. :param string command: The command the client has requested to execute :returns: A boolean indicating if the exec request was allowed or not """ # pylint: disable=no-self-use,unused-argument return False def subsystem_requested(self, subsystem): """The client has requested to start a subsystem This method should be implemented by the application to perform whatever processing is required when a client makes a request to start a subsystem. It should return ``True`` to accept the request, or ``False`` to reject it. If the application returns ``True``, the :meth:`session_started` method will be called once the channel is fully open. No output should be sent until this method is called. By default this method returns ``False`` to reject all requests. :param string subsystem: The subsystem to start :returns: A boolean indicating if the request to open the subsystem was allowed or not """ # pylint: disable=no-self-use,unused-argument return False def break_received(self, msec): """The client has sent a break This method is called when the client requests that the server perform a break operation on the terminal. If the break is performed, this method should return ``True``. Otherwise, it should return ``False``. By default, this method returns ``False`` indicating that no break was performed. :param integer msec: The duration of the break in milliseconds :returns: A boolean to indicate if the break operation was performed or not """ # pylint: disable=no-self-use,unused-argument return False def signal_received(self, signal): """The client has sent a signal This method is called when the client delivers a signal on the channel. By default, signals from the client are ignored. """ # pylint: disable=no-self-use,unused-argument class SSHTCPSession(SSHSession): """SSH TCP connection session handler Applications should subclass this when implementing a handler for SSH direct or forwarded TCP connections. SSH client applications wishing to open a direct connection should call :meth:`create_connection() ` on their :class:`SSHClientConnection`, passing in a factory which returns instances of this class. Server applications wishing to allow direct connections should implement the coroutine :meth:`connection_requested() ` on their :class:`SSHServer` object and have it return instances of this class. Server applications wishing to allow connection forwarding back to the client should implement the coroutine :meth:`server_requested() ` on their :class:`SSHServer` object and call :meth:`create_connection() ` on their :class:`SSHServerConnection` for each new connection, passing it a factory which returns instances of this class. When a connection is successfully opened, :meth:`session_started` will be called, after which the application can begin sending data. Received data will be passed to the :meth:`data_received` method. """ asyncssh-1.3.0/asyncssh/sftp.py000066400000000000000000003742351260630620200165420ustar00rootroot00000000000000# Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation # Jonathan Slenders - proposed changes to allow SFTP server callbacks # to be coroutines """SFTP handlers""" import asyncio import grp import os import posixpath import pwd import stat import time from collections import OrderedDict from fnmatch import fnmatch from os import SEEK_SET, SEEK_CUR, SEEK_END from .constants import DEFAULT_LANG from .constants import FXP_INIT, FXP_VERSION, FXP_OPEN, FXP_CLOSE, FXP_READ from .constants import FXP_WRITE, FXP_LSTAT, FXP_FSTAT, FXP_SETSTAT from .constants import FXP_FSETSTAT, FXP_OPENDIR, FXP_READDIR, FXP_REMOVE from .constants import FXP_MKDIR, FXP_RMDIR, FXP_REALPATH, FXP_STAT, FXP_RENAME from .constants import FXP_READLINK, FXP_SYMLINK, FXP_STATUS, FXP_HANDLE from .constants import FXP_DATA, FXP_NAME, FXP_ATTRS, FXP_EXTENDED from .constants import FXP_EXTENDED_REPLY from .constants import FXF_READ, FXF_WRITE, FXF_APPEND from .constants import FXF_CREAT, FXF_TRUNC, FXF_EXCL from .constants import FILEXFER_ATTR_SIZE, FILEXFER_ATTR_UIDGID from .constants import FILEXFER_ATTR_PERMISSIONS, FILEXFER_ATTR_ACMODTIME from .constants import FILEXFER_ATTR_EXTENDED, FILEXFER_ATTR_UNDEFINED from .constants import FX_OK, FX_EOF, FX_NO_SUCH_FILE, FX_PERMISSION_DENIED from .constants import FX_FAILURE, FX_BAD_MESSAGE, FX_NO_CONNECTION from .constants import FX_CONNECTION_LOST, FX_OP_UNSUPPORTED from .misc import Error from .packet import Byte, String, UInt32, UInt64, PacketDecodeError, SSHPacket from .session import SSHClientSession, SSHServerSession _SFTP_VERSION = 3 _SFTP_BLOCK_SIZE = 8192 def _setstat(path, attrs): """Utility function to set file attributes""" if attrs.size is not None: os.truncate(path, attrs.size) if attrs.uid is not None and attrs.gid is not None: os.chown(path, attrs.uid, attrs.gid) if attrs.permissions is not None: os.chmod(path, stat.S_IMODE(attrs.permissions)) if attrs.atime is not None and attrs.mtime is not None: os.utime(path, times=(attrs.atime, attrs.mtime)) class _Record: """General-purpose record type with fixed set of fields""" __slots__ = OrderedDict() def __init__(self, *args, **kwargs): for k, v in self.__slots__.items(): setattr(self, k, v) for k, v in zip(self.__slots__, args): setattr(self, k, v) for k, v in kwargs.items(): setattr(self, k, v) def __repr__(self): return '%s(%s)' % (self.__class__.__name__, ', '.join('%s=%r' % (k, getattr(self, k)) for k in self.__slots__)) class _LocalFile: """A coroutine wrapper around local file I/O""" def __init__(self, f): self._file = f def __enter__(self): return self def __exit__(self, *excinfo): self._file.close() @classmethod def encode(cls, path): """Encode path name using filesystem native encoding This method has no effect if the path is already a byte string. """ if isinstance(path, str): path = os.fsencode(path) return path @classmethod def decode(cls, path, want_string=True): """Decode path name using filesystem native encoding This method has no effect if want_string is set to ``False``. """ if want_string: path = os.fsdecode(path) return path @classmethod def compose_path(cls, path, parent=None): """Compose a path If parent is not specified, just encode the path. """ path = cls.encode(path) return os.path.join(parent, path) if parent else path @classmethod @asyncio.coroutine def open(cls, *args): """Open a local file""" return cls(open(*args)) @classmethod @asyncio.coroutine def stat(cls, path): """Get attributes of a local file or directory, following symlinks""" return SFTPAttrs.from_local(os.stat(path)) @classmethod @asyncio.coroutine def lstat(cls, path): """Get attributes of a local file, directory, or symlink""" return SFTPAttrs.from_local(os.lstat(path)) @classmethod @asyncio.coroutine def setstat(cls, path, attrs): """Set attributes of a local file or directory""" _setstat(path, attrs) @classmethod @asyncio.coroutine def truncate(cls, path): """Truncate a local file to the specified size""" os.truncate(path) @classmethod @asyncio.coroutine def chown(cls, path, uid, gid): """Change the owner user and group id of a local file or directory""" os.chown(path, uid, gid) @classmethod @asyncio.coroutine def chmod(cls, path, mode): """Change the file permissions of a local file or directory""" os.chmod(path, mode) @classmethod @asyncio.coroutine def utime(cls, path, times=None): """Change the access and modify times of a local file or directory""" os.utime(path, times) @classmethod @asyncio.coroutine def exists(cls, path): """Return if the local path exists and isn't a broken symbolic link""" return os.path.exists(path) @classmethod @asyncio.coroutine def lexists(cls, path): """Return if the local path exists, without following symbolic links""" return os.path.lexists(path) @classmethod @asyncio.coroutine def getatime(cls, path): """Return the last access time of a local file or directory""" return os.path.getatime(path) @classmethod @asyncio.coroutine def getmtime(cls, path): """Return the last modification time of a local file or directory""" return os.path.getmtime(path) @classmethod @asyncio.coroutine def getsize(cls, path): """Return the size of a local file or directory""" return os.path.getsize(path) @classmethod @asyncio.coroutine def isdir(cls, path): """Return if the local path refers to a directory""" return os.path.isdir(path) @classmethod @asyncio.coroutine def isfile(cls, path): """Return if the local path refers to a regular file""" return os.path.isfile(path) @classmethod @asyncio.coroutine def islink(cls, path): """Return if the local path refers to a symbolic link""" return os.path.islink(path) @classmethod @asyncio.coroutine def remove(cls, path): """Remove a local file""" os.remove(path) @classmethod @asyncio.coroutine def unlink(cls, path): """Remove a local file (see :meth:`remove`)""" os.unlink(path) @classmethod @asyncio.coroutine def rename(cls, oldpath, newpath): """Rename a local file, directory, or link""" os.rename(oldpath, newpath) @classmethod @asyncio.coroutine def readdir(cls, path): """Read the contents of a local directory""" names = os.listdir(path) return [SFTPName(filename=name, attrs=(yield from cls.stat(name))) for name in names] @classmethod @asyncio.coroutine def listdir(cls, path): """Read the names of the files in a local directory""" return os.listdir(path) @classmethod @asyncio.coroutine def mkdir(cls, path): """Create a local directory with the specified attributes""" os.mkdir(path) @classmethod @asyncio.coroutine def rmdir(cls, path): """Remove a local directory""" os.rmdir(path) @classmethod @asyncio.coroutine def realpath(cls, path): """Return the canonical version of a local path""" return os.path.realpath(path) @classmethod @asyncio.coroutine def getcwd(cls): """Return the local current working directory""" return os.getcwd() @classmethod @asyncio.coroutine def chdir(cls, path): """Change the local current working directory""" os.chdir(path) @classmethod @asyncio.coroutine def readlink(cls, path): """Return the target of a local symbolic link""" return os.readlink(path) @classmethod @asyncio.coroutine def symlink(cls, oldpath, newpath): """Create a local symbolic link""" os.symlink(oldpath, newpath) @classmethod @asyncio.coroutine def link(cls, oldpath, newpath): """Create a local hard link""" os.link(oldpath, newpath) @asyncio.coroutine def read(self, size=-1, offset=None): """Read data from the local file""" if offset is not None: self._file.seek(offset) return self._file.read(size) @asyncio.coroutine def write(self, data, offset=None): """Write data to the local file""" if offset is not None: self._file.seek(offset) return self._file.write(data) @asyncio.coroutine def seek(self, offset, from_what=SEEK_SET): """Seek to a new position in the local file""" return self._file.seek(offset, from_what) @asyncio.coroutine def tell(self): """Return the current position in the local file""" return self._file.tell() @asyncio.coroutine def close(self): """Close the local file""" self._file.close() class SFTPError(Error): """SFTP error This exception is raised when an error occurs while processing an SFTP request. Exception codes should be taken from :ref:`SFTP error codes `. :param integer code: Disconnect reason, taken from :ref:`disconnect reason codes ` :param string reason: A human-readable reason for the disconnect :param string lang: The language the reason is in """ def __init__(self, code, reason, lang=DEFAULT_LANG): super().__init__('SFTP', code, reason, lang) class SFTPAttrs(_Record): """SFTP file attributes SFTPAttrs is a simple record class with the following fields: ============ =========================================== ====== Field Description Type ============ =========================================== ====== size File size in bytes uint64 uid User id of file owner uint32 gid Group id of file owner uint32 permissions Bit mask of POSIX file permissions, uint32 atime Last access time, UNIX epoch seconds uint32 mtime Last modification time, UNIX epoch seconds uint32 ============ =========================================== ====== In addition to the above, an ``nlink`` field is provided which stores the number of links to this file, but it is not encoded in the SFTP protocol. It's included here only so that it can be used to create the default ``longname`` string in :class:`SFTPName` objects. Extended attributes can also be added via a field named ``extended`` which is a list of string name/value pairs. When setting attributes using an :class:`SFTPAttrs`, only fields which have been initialized will be changed on the selected file. """ # Unfortunately, pylint can't handle attributes defined with setattr # pylint: disable=attribute-defined-outside-init __slots__ = OrderedDict((('size', None), ('uid', None), ('gid', None), ('permissions', None), ('atime', None), ('mtime', None), ('nlink', None), ('extended', []))) def encode(self): """Encode SFTP attributes as bytes in an SSH packet""" flags = 0 attrs = [] if self.size is not None: flags |= FILEXFER_ATTR_SIZE attrs.append(UInt64(self.size)) if self.uid is not None and self.gid is not None: flags |= FILEXFER_ATTR_UIDGID attrs.append(UInt32(self.uid) + UInt32(self.gid)) if self.permissions is not None: flags |= FILEXFER_ATTR_PERMISSIONS attrs.append(UInt32(self.permissions)) if self.atime is not None and self.mtime is not None: flags |= FILEXFER_ATTR_ACMODTIME attrs.append(UInt32(int(self.atime)) + UInt32(int(self.mtime))) if self.extended: flags |= FILEXFER_ATTR_EXTENDED attrs.append(UInt32(len(self.extended))) attrs.extend(String(type) + String(data) for type, data in self.extended) return UInt32(flags) + b''.join(attrs) @classmethod def decode(cls, packet): """Decode bytes in an SSH packet as SFTP attributes""" flags = packet.get_uint32() attrs = cls() if flags & FILEXFER_ATTR_UNDEFINED: raise SFTPError(FX_BAD_MESSAGE, 'Unsupported attribute flags') if flags & FILEXFER_ATTR_SIZE: attrs.size = packet.get_uint64() if flags & FILEXFER_ATTR_UIDGID: attrs.uid = packet.get_uint32() attrs.gid = packet.get_uint32() if flags & FILEXFER_ATTR_PERMISSIONS: attrs.permissions = packet.get_uint32() if flags & FILEXFER_ATTR_ACMODTIME: attrs.atime = packet.get_uint32() attrs.mtime = packet.get_uint32() if flags & FILEXFER_ATTR_EXTENDED: count = packet.get_uint32() attrs.extended = [] for _ in range(count): attr = packet.get_string() data = packet.get_string() attrs.extended.append((attr, data)) return attrs @classmethod def from_local(cls, result): """Convert from local stat attributes""" return cls(result.st_size, result.st_uid, result.st_gid, result.st_mode, result.st_atime, result.st_mtime, result.st_nlink) class SFTPVFSAttrs(_Record): """SFTP file system attributes SFTPVFSAttrs is a simple record class with the following fields: ============ =========================================== ====== Field Description Type ============ =========================================== ====== bsize File system block size (I/O size) uint64 frsize Fundamental block size (allocation size) uint64 blocks Total data blocks (in frsize units) uint64 bfree Free data blocks uint64 bavail Available data blocks (for non-root) uint64 files Total file inodes uint64 ffree Free file inodes uint64 favail Available file inodes (for non-root) uint64 fsid File system id uint64 flags File system flags (read-only, no-setuid) uint64 namemax Maximum filename length uint64 ============ =========================================== ====== """ # Unfortunately, pylint can't handle attributes defined with setattr # pylint: disable=attribute-defined-outside-init __slots__ = OrderedDict((('bsize', 0), ('frsize', 0), ('blocks', 0), ('bfree', 0), ('bavail', 0), ('files', 0), ('ffree', 0), ('favail', 0), ('fsid', 0), ('flags', 0), ('namemax', 0))) def encode(self): """Encode SFTP statvfs attributes as bytes in an SSH packet""" return b''.join((UInt64(self.bsize), UInt64(self.frsize), UInt64(self.blocks), UInt64(self.bfree), UInt64(self.bavail), UInt64(self.files), UInt64(self.ffree), UInt64(self.favail), UInt64(self.fsid), UInt64(self.flags), UInt64(self.namemax))) @classmethod def decode(cls, packet): """Decode bytes in an SSH packet as SFTP statvfs attributes""" vfsattrs = cls() vfsattrs.bsize = packet.get_uint64() vfsattrs.frsize = packet.get_uint64() vfsattrs.blocks = packet.get_uint64() vfsattrs.bfree = packet.get_uint64() vfsattrs.bavail = packet.get_uint64() vfsattrs.files = packet.get_uint64() vfsattrs.ffree = packet.get_uint64() vfsattrs.favail = packet.get_uint64() vfsattrs.fsid = packet.get_uint64() vfsattrs.flags = packet.get_uint64() vfsattrs.namemax = packet.get_uint64() return vfsattrs @classmethod def from_local(cls, result): """Convert from local statvfs attributes""" return cls(result.f_bsize, result.f_frsize, result.f_blocks, result.f_bfree, result.f_bavail, result.f_files, result.f_ffree, result.f_favail, 0, result.f_flag, result.f_namemax) class SFTPName(_Record): """SFTP file name and attributes SFTPName is a simple record class with the following fields: ========= ================================== ================== Field Description Type ========= ================================== ================== filename Filename string or bytes longname Expanded form of filename & attrs string or bytes attrs File attributes :class:`SFTPAttrs` ========= ================================== ================== A list of these is returned by :meth:`readdir() ` in :class:`SFTPClient` when retrieving the contents of a directory. """ __slots__ = OrderedDict((('filename', ''), ('longname', ''), ('attrs', SFTPAttrs()))) def encode(self): """Encode an SFTP name as bytes in an SSH packet""" # pylint: disable=no-member return (String(self.filename) + String(self.longname) + self.attrs.encode()) @classmethod def decode(cls, packet): """Decode bytes in an SSH packet as an SFTP name""" filename = packet.get_string() longname = packet.get_string() attrs = SFTPAttrs.decode(packet) return cls(filename, longname, attrs) class SFTPSession: """SFTP session handler""" # SFTP implementations with broken order for SYMLINK arguments _nonstandard_symlink_impls = ['OpenSSH', 'paramiko'] # Return types by message -- unlisted entries always return FXP_STATUS, # those below return FXP_STATUS on error _return_types = { FXP_OPEN: FXP_HANDLE, FXP_READ: FXP_DATA, FXP_LSTAT: FXP_ATTRS, FXP_FSTAT: FXP_ATTRS, FXP_OPENDIR: FXP_HANDLE, FXP_READDIR: FXP_NAME, FXP_REALPATH: FXP_NAME, FXP_STAT: FXP_ATTRS, FXP_READLINK: FXP_NAME, b'statvfs@openssh.com': FXP_EXTENDED_REPLY, b'fstatvfs@openssh.com': FXP_EXTENDED_REPLY } def __init__(self): self._chan = None self._inpbuf = b'' self._pktlen = 0 self._recv_handler = self._recv_pkthdr def _cleanup(self, code, reason, lang=DEFAULT_LANG): """Clean up this SFTP session""" # pylint: disable=unused-argument if self._chan: self._chan.close() self._chan = None def _recv_pkthdr(self): """Receive and parse an SFTP packet header""" if len(self._inpbuf) < 4: return False self._pktlen = int.from_bytes(self._inpbuf[:4], 'big') self._inpbuf = self._inpbuf[4:] self._recv_handler = self._recv_packet return True def _recv_packet(self): """Receive the rest of an SFTP packet and process it""" if len(self._inpbuf) < self._pktlen: return False try: packet = SSHPacket(self._inpbuf[:self._pktlen]) self._inpbuf = self._inpbuf[self._pktlen:] pkttype = packet.get_byte() if pkttype == FXP_INIT: self._process_init(packet) elif pkttype == FXP_VERSION: self._process_version(packet) else: pktid = packet.get_uint32() self._process_packet(pkttype, pktid, packet) except PacketDecodeError as exc: self._cleanup(FX_BAD_MESSAGE, str(exc)) self._recv_handler = self._recv_pkthdr return True def _process_init(self, packet): """Abstract method for processing an init packet""" raise NotImplementedError def _process_version(self, packet): """Abstract method for processing a version packet""" raise NotImplementedError def _process_packet(self, pkttype, pktid, packet): """Abstract method for processing other SFTP packets""" raise NotImplementedError def _process_connection_open(self): """Abstract method for handling a newly opened SFTP connection""" raise NotImplementedError def connection_made(self, chan): """Handle a newly opened SFTP connection""" self._chan = chan self._process_connection_open() def connection_lost(self, exc): """Handle an incoming connection close""" reason = str(exc) if exc else 'Connection closed' self._cleanup(FX_CONNECTION_LOST, reason) def data_received(self, data, datatype): """Handle incoming data""" # pylint: disable=unused-argument if data: self._inpbuf += data while self._inpbuf and self._recv_handler(): pass def eof_received(self): """Handle an incoming end of file""" self.connection_lost(None) def send_packet(self, *args): """Send an SFTP packet""" payload = b''.join(args) self._chan.write(UInt32(len(payload)) + payload) def exit(self): """Handle a request to close the SFTP connection""" self._cleanup(FX_CONNECTION_LOST, 'Session closed by application') class SFTPClientSession(SFTPSession, SSHClientSession): """An SFTP client session handler""" _extensions = [] def __init__(self, loop, version_waiter): super().__init__() self._loop = loop self._version = None self._next_pktid = 0 self._requests = {None: (None, version_waiter)} self._exc = SFTPError(FX_NO_CONNECTION, 'Connection not yet open') self._nonstandard_symlink = False self._supports_posix_rename = False self._supports_statvfs = False self._supports_fstatvfs = False self._supports_hardlink = False self._supports_fsync = False def _cleanup(self, code, reason, lang=DEFAULT_LANG): """Clean up this SFTP client session""" self._exc = SFTPError(code, reason, lang) for _, waiter in self._requests.values(): if waiter and not waiter.cancelled(): waiter.set_exception(self._exc) self._requests = {} super()._cleanup(code, reason, lang) def _send_request(self, pkttype, *args, waiter=None): """Send an SFTP request""" if self._exc: raise self._exc pktid = self._next_pktid self._next_pktid = (self._next_pktid + 1) & 0xffffffff return_type = self._return_types.get(pkttype) self._requests[pktid] = (return_type, waiter) if isinstance(pkttype, bytes): hdr = Byte(FXP_EXTENDED) + UInt32(pktid) + String(pkttype) else: hdr = Byte(pkttype) + UInt32(pktid) self.send_packet(hdr, *args) @asyncio.coroutine def _make_request(self, pkttype, *args): """Make an SFTP request and wait for a response""" waiter = asyncio.Future(loop=self._loop) self._send_request(pkttype, *args, waiter=waiter) return (yield from waiter) def _process_connection_open(self): """Process a newly opened SFTP client connection""" self._exc = None def session_started(self): """Begin SFTP client connection handshake""" extensions = (String(name) + String(data) for name, data in self._extensions) self.send_packet(Byte(FXP_INIT), UInt32(_SFTP_VERSION), *extensions) def _process_init(self, packet): """Process an incoming SFTP init packet""" self._cleanup(FX_OP_UNSUPPORTED, 'FXP_INIT not expected on client') def _process_version(self, packet): """Process an incoming SFTP version packet""" version = packet.get_uint32() extensions = [] while packet: name = packet.get_string() data = packet.get_string() extensions.append((name, data)) if version != _SFTP_VERSION: self._cleanup(FX_BAD_MESSAGE, 'Unsupported version: %d' % version) return try: _, version_waiter = self._requests.pop(None) except KeyError: self._cleanup(FX_BAD_MESSAGE, 'FXP_VERSION already received') return self._version = version for name, data in extensions: if name == b'posix-rename@openssh.com' and data == b'1': self._supports_posix_rename = True elif name == b'statvfs@openssh.com' and data == b'2': self._supports_statvfs = True elif name == b'fstatvfs@openssh.com' and data == b'2': self._supports_fstatvfs = True elif name == b'hardlink@openssh.com' and data == b'1': self._supports_hardlink = True elif name == b'fsync@openssh.com' and data == b'1': self._supports_fsync = True if version == 3: # Check if the server has a buggy SYMLINK implementation server_version = self._chan.get_extra_info('server_version', '') if any(name in server_version for name in self._nonstandard_symlink_impls): self._nonstandard_symlink = True if not version_waiter.cancelled(): version_waiter.set_result(None) def _process_packet(self, pkttype, pktid, packet): """Process other incoming SFTP packets""" try: return_type, waiter = self._requests.pop(pktid) except KeyError: self._cleanup(FX_BAD_MESSAGE, 'Invalid response id') return if pkttype not in (FXP_STATUS, return_type): self._cleanup(FX_BAD_MESSAGE, 'Unexpected response type: %s' % pkttype) return if waiter and not waiter.cancelled(): try: result = self._packet_handlers[pkttype](self, packet) if result is not None or return_type is None: waiter.set_result(result) else: exc = SFTPError(FX_BAD_MESSAGE, 'Unexpected FX_OK response') waiter.set_exception(exc) except PacketDecodeError as exc: exc = SFTPError(FX_BAD_MESSAGE, str(exc)) waiter.set_exception(exc) except SFTPError as exc: waiter.set_exception(exc) def _process_status(self, packet): """Process an incoming SFTP status response""" # pylint: disable=no-self-use code = packet.get_uint32() try: reason = packet.get_string().decode('utf-8') lang = packet.get_string().decode('ascii') except UnicodeDecodeError: raise SFTPError(FX_BAD_MESSAGE, 'Invalid status message') packet.check_end() if code == FX_OK: return None else: raise SFTPError(code, reason, lang) def _process_handle(self, packet): """Process an incoming SFTP handle response""" # pylint: disable=no-self-use handle = packet.get_string() packet.check_end() return handle def _process_data(self, packet): """Process an incoming SFTP data response""" # pylint: disable=no-self-use data = packet.get_string() packet.check_end() return data def _process_name(self, packet): """Process an incoming SFTP name response""" # pylint: disable=no-self-use count = packet.get_uint32() names = [SFTPName.decode(packet) for i in range(count)] packet.check_end() return names def _process_attrs(self, packet): """Process an incoming SFTP attributes response""" # pylint: disable=no-self-use attrs = SFTPAttrs().decode(packet) packet.check_end() return attrs def _process_extended_reply(self, packet): """Process an incoming SFTP extended reply response""" # pylint: disable=no-self-use # Let the caller do the decoding for extended replies return packet _packet_handlers = { FXP_STATUS: _process_status, FXP_HANDLE: _process_handle, FXP_DATA: _process_data, FXP_NAME: _process_name, FXP_ATTRS: _process_attrs, FXP_EXTENDED_REPLY: _process_extended_reply } def open(self, filename, pflags, attrs): """Make an SFTP open request""" return self._make_request(FXP_OPEN, String(filename), UInt32(pflags), attrs.encode()) def close(self, handle): """Make an SFTP close request""" return self._make_request(FXP_CLOSE, String(handle)) def nonblocking_close(self, handle): """Send an SFTP close request without blocking on the response""" # Used by context managers, since they can't block to wait for a reply self._send_request(FXP_CLOSE, String(handle)) def read(self, handle, offset, length): """Make an SFTP read request""" return self._make_request(FXP_READ, String(handle), UInt64(offset), UInt32(length)) def write(self, handle, offset, data): """Make an SFTP write request""" return self._make_request(FXP_WRITE, String(handle), UInt64(offset), String(data)) def stat(self, path): """Make an SFTP stat request""" return self._make_request(FXP_STAT, String(path)) def lstat(self, path): """Make an SFTP lstat request""" return self._make_request(FXP_LSTAT, String(path)) def fstat(self, handle): """Make an SFTP fstat request""" return self._make_request(FXP_FSTAT, String(handle)) def setstat(self, path, attrs): """Make an SFTP setstat request""" return self._make_request(FXP_SETSTAT, String(path), attrs.encode()) def fsetstat(self, handle, attrs): """Make an SFTP fsetstat request""" return self._make_request(FXP_FSETSTAT, String(handle), attrs.encode()) @asyncio.coroutine def statvfs(self, path): """Make an SFTP statvfs request""" if self._supports_statvfs: packet = yield from self._make_request(b'statvfs@openssh.com', String(path)) vfsattrs = SFTPVFSAttrs.decode(packet) packet.check_end() return vfsattrs else: raise SFTPError(FX_OP_UNSUPPORTED, 'statvfs not supported') @asyncio.coroutine def fstatvfs(self, handle): """Make an SFTP fstatvfs request""" if self._supports_fstatvfs: packet = yield from self._make_request(b'fstatvfs@openssh.com', String(handle)) vfsattrs = SFTPVFSAttrs.decode(packet) packet.check_end() return vfsattrs else: raise SFTPError(FX_OP_UNSUPPORTED, 'fstatvfs not supported') def remove(self, path): """Make an SFTP remove request""" return self._make_request(FXP_REMOVE, String(path)) def rename(self, oldpath, newpath): """Make an SFTP rename request""" return self._make_request(FXP_RENAME, String(oldpath), String(newpath)) def posix_rename(self, oldpath, newpath): """Make an SFTP POSIX rename request""" if self._supports_posix_rename: return self._make_request(b'posix-rename@openssh.com', String(oldpath), String(newpath)) else: raise SFTPError(FX_OP_UNSUPPORTED, 'POSIX rename not supported') def opendir(self, path): """Make an SFTP opendir request""" return self._make_request(FXP_OPENDIR, String(path)) def readdir(self, handle): """Make an SFTP readdir request""" return self._make_request(FXP_READDIR, String(handle)) def mkdir(self, path, attrs): """Make an SFTP mkdir request""" return self._make_request(FXP_MKDIR, String(path), attrs.encode()) def rmdir(self, path): """Make an SFTP rmdir request""" return self._make_request(FXP_RMDIR, String(path)) def realpath(self, path): """Make an SFTP realpath request""" return self._make_request(FXP_REALPATH, String(path)) def readlink(self, path): """Make an SFTP readlink request""" return self._make_request(FXP_READLINK, String(path)) def symlink(self, oldpath, newpath): """Make an SFTP symlink request""" if self._nonstandard_symlink: args = String(oldpath) + String(newpath) else: args = String(newpath) + String(oldpath) return self._make_request(FXP_SYMLINK, args) def link(self, oldpath, newpath): """Make an SFTP link request""" if self._supports_hardlink: return self._make_request(b'hardlink@openssh.com', String(oldpath), String(newpath)) else: raise SFTPError(FX_OP_UNSUPPORTED, 'link not supported') def fsync(self, handle): """Make an SFTP fsync request""" if self._supports_fsync: return self._make_request(b'fsync@openssh.com', String(handle)) else: raise SFTPError(FX_OP_UNSUPPORTED, 'fsync not supported') class SFTPFile: """SFTP client remote file object This class represents an open file on a remote SFTP server. It is opened with the :meth:`open() ` method on the :class:`SFTPClient` class and provides methods to read and write data and get and set attributes on the open file. """ def __init__(self, session, handle, appending, encoding, errors): self._session = session self._handle = handle self._appending = appending self._encoding = encoding self._errors = errors self._offset = None if appending else 0 def __enter__(self): """Allow SFTPFile to be used as a context manager""" return self def __exit__(self, *exc_info): """Automatically close the file when used as a context manager""" if self._handle: self._session.nonblocking_close(self._handle) self._handle = None @asyncio.coroutine def _end(self): """Return the offset of the end of the file""" attrs = yield from self.stat() return attrs.size @asyncio.coroutine def read(self, size=-1, offset=None): """Read data from the remote file This method reads and returns up to ``size`` bytes of data from the remote file. If size is negative, all data up to the end of the file is returned. If offset is specified, the read will be performed starting at that offset rather than the current file position. This argument should be provided if you want to issue parallel reads on the same file, since the file position is not predictable in that case. Data will be returned as a string if an encoding was set when the file was opened. Otherwise, data is returned as bytes. An empty string or bytes object is returned when at EOF. :param integer size: The number of bytes to read :param integer offset: (optional) The offset from the beginning of the file to begin reading :returns: data read from the file, as a string or bytes :raises: | :exc:`ValueError` if the file has been closed | :exc:`UnicodeDecodeError` if the data can't be decoded using the requested encoding | :exc:`SFTPError` if the server returns an error """ if self._handle is None: raise ValueError('I/O operation on closed file') if offset is None: offset = self._offset if offset is None: # We're appending and haven't seeked backward in the file # since the last write, so there's no data to return data = b'' elif size is None or size < 0: data = [] try: while True: result = yield from self._session.read(self._handle, offset, _SFTP_BLOCK_SIZE) data.append(result) offset += len(result) self._offset = offset except SFTPError as exc: if exc.code != FX_EOF: raise data = b''.join(data) else: data = b'' try: data = yield from self._session.read(self._handle, offset, size) self._offset = offset + len(data) except SFTPError as exc: if exc.code != FX_EOF: raise if self._encoding: data = data.decode(self._encoding, self._errors) return data @asyncio.coroutine def write(self, data, offset=None): """Write data to the remote file This method writes the specified data at the current position in the remote file. :param data: The data to write to the file :param integer offset: (optional) The offset from the beginning of the file to begin writing :type data: string or bytes If offset is specified, the write will be performed starting at that offset rather than the current file position. This argument should be provided if you want to issue parallel writes on the same file, since the file position is not predictable in that case. :returns: number of bytes written :raises: | :exc:`ValueError` if the file has been closed | :exc:`UnicodeEncodeError` if the data can't be encoded using the requested encoding | :exc:`SFTPError` if the server returns an error """ if self._handle is None: raise ValueError('I/O operation on closed file') if offset is None: # Offset is ignored when appending, so fill in an offset of 0 # if we don't have a current file position offset = self._offset or 0 if self._encoding: data = data.encode(self._encoding, self._errors) yield from self._session.write(self._handle, offset, data) self._offset = None if self._appending else offset + len(data) return len(data) @asyncio.coroutine def seek(self, offset, from_what=SEEK_SET): """Seek to a new position in the remote file This method changes the position in the remote file. The ``offset`` passed in is treated as relative to the beginning of the file if ``from_what`` is set to ``SEEK_SET`` (the default), relative to the current file position if it is set to ``SEEK_CUR``, or relative to the end of the file if it is set to ``SEEK_END``. :param integer offset: The amount to seek :param integer from_what: (optional) The reference point to use (SEEK_SET, SEEK_CUR, or SEEK_END) :returns: The new byte offset from the beginning of the file """ if self._handle is None: raise ValueError('I/O operation on closed file') if from_what == SEEK_SET: self._offset = offset elif from_what == SEEK_CUR: self._offset += offset elif from_what == SEEK_END: self._offset = (yield from self._end()) + offset return self._offset @asyncio.coroutine def tell(self): """Return the current position in the remote file This method returns the current position in the remote file. :returns: The current byte offset from the beginning of the file """ if self._handle is None: raise ValueError('I/O operation on closed file') if self._offset is None: self._offset = yield from self._end() return self._offset @asyncio.coroutine def stat(self): """Return file attributes of the remote file This method queries file attributes of the currently open file. :returns: An :class:`SFTPAttrs` containing the file attributes :raises: :exc:`SFTPError` if the server returns an error """ if self._handle is None: raise ValueError('I/O operation on closed file') return (yield from self._session.fstat(self._handle)) @asyncio.coroutine def setstat(self, attrs): """Set attributes of the remote file This method sets file attributes of the currently open file. :param attrs: File attributes to set on the file :type attrs: :class:`SFTPAttrs` :raises: :exc:`SFTPError` if the server returns an error """ if self._handle is None: raise ValueError('I/O operation on closed file') yield from self._session.fsetstat(self._handle, attrs) @asyncio.coroutine def statvfs(self): """Return file system attributes of the remote file This method queries attributes of the file system containing the currently open file. :returns: An :class:`SFTPVFSAttrs` containing the file system attributes :raises: :exc:`SFTPError` if the server doesn't support this extension or returns an error """ if self._handle is None: raise ValueError('I/O operation on closed file') return (yield from self._session.fstatvfs(self._handle)) @asyncio.coroutine def truncate(self, size=None): """Truncate the remote file to the specified size This method changes the remote file's size to the specified value. If a size is not provided, the current file position is used. :param integer size: (optional) The desired size of the file, in bytes :raises: :exc:`SFTPError` if the server returns an error """ if size is None: size = self._offset yield from self.setstat(SFTPAttrs(size=size)) @asyncio.coroutine def chown(self, uid, gid): """Change the owner user and group id of the remote file This method changes the user and group id of the currently open file. :param integer uid: The new user id to assign to the file :param integer gid: The new group id to assign to the file :raises: :exc:`SFTPError` if the server returns an error """ yield from self.setstat(SFTPAttrs(uid=uid, gid=gid)) @asyncio.coroutine def chmod(self, mode): """Change the file permissions of the remote file This method changes the permissions of the currently open file. :param integer mode: The new file permissions, expressed as an integer :raises: :exc:`SFTPError` if the server returns an error """ yield from self.setstat(SFTPAttrs(permissions=mode)) @asyncio.coroutine def utime(self, times=None): """Change the access and modify times of the remote file This method changes the access and modify times of the currently open file. If ``times`` is not provided, the times will be changed to the current time. :param times: (optional) The new access and modify times, as seconds relative to the UNIX epoch :type times: tuple of two integer or float values :raises: :exc:`SFTPError` if the server returns an error """ # pylint: disable=unpacking-non-sequence if times is None: atime = mtime = time.time() else: atime, mtime = times yield from self.setstat(SFTPAttrs(atime=atime, mtime=mtime)) @asyncio.coroutine def fsync(self): """Force the remote file data to be written to disk""" if self._handle is None: raise ValueError('I/O operation on closed file') yield from self._session.fsync(self._handle) @asyncio.coroutine def close(self): """Close the remote file""" if self._handle: yield from self._session.close(self._handle) self._handle = None class SFTPClient: """SFTP client This class represents the client side of an SFTP session. It is started by calling the :meth:`start_sftp_client() ` method on the :class:`SSHClientConnection` class. """ _open_modes = { 'r': FXF_READ, 'w': FXF_WRITE | FXF_CREAT | FXF_TRUNC, 'a': FXF_WRITE | FXF_CREAT | FXF_APPEND, 'x': FXF_WRITE | FXF_CREAT | FXF_EXCL, 'r+': FXF_READ | FXF_WRITE, 'w+': FXF_READ | FXF_WRITE | FXF_CREAT | FXF_TRUNC, 'a+': FXF_READ | FXF_WRITE | FXF_CREAT | FXF_APPEND, 'x+': FXF_READ | FXF_WRITE | FXF_CREAT | FXF_EXCL } def __init__(self, session, path_encoding, path_errors): self._session = session self._path_encoding = path_encoding self._path_errors = path_errors self._cwd = None def __enter__(self): """Allow SFTPClient to be used as a context manager""" return self def __exit__(self, *exc_info): """Automatically close the session when used as a context manager""" self.exit() def encode(self, path): """Encode path name using configured path encoding This method has no effect if the path is already a byte string. """ if isinstance(path, str): if self._path_encoding: path = path.encode(self._path_encoding, self._path_errors) else: raise SFTPError('Path must be bytes when encoding is not set') return path def decode(self, path, want_string=True): """Decode path name using configured path encoding This method has no effect if want_string is set to ``False``. """ if want_string and self._path_encoding: try: path = path.decode(self._path_encoding, self._path_errors) except UnicodeDecodeError: raise SFTPError(FX_BAD_MESSAGE, 'Unable to decode name') return path def compose_path(self, path, parent=...): """Compose a path If parent is not specified, return a path relative to the current remote working directory. """ if parent is ...: parent = self._cwd path = self.encode(path) return posixpath.join(parent, path) if parent else path @asyncio.coroutine def _mode(self, path, statfunc=None): """Return the mode of a remote path, or 0 if it can't be accessed""" if statfunc is None: statfunc = self.stat try: return (yield from statfunc(path)).permissions except SFTPError as exc: if exc.code in (FX_NO_SUCH_FILE, FX_PERMISSION_DENIED): return 0 else: raise @asyncio.coroutine def _glob(self, fs, basedir, patlist, decode, result): """Match a glob pattern""" pattern, patlist = patlist[0], patlist[1:] for name in (yield from fs.listdir(basedir or b'.')): if pattern != name and name in (b'.', b'..'): continue if name[:1] == b'.' and not pattern[:1] == b'.': continue if fnmatch(name, pattern): newbase = fs.compose_path(name, parent=basedir) if not patlist: result.append(fs.decode(newbase, decode)) elif (yield from fs.isdir(newbase)): yield from self._glob(fs, newbase, patlist, decode, result) @asyncio.coroutine def _begin_glob(self, fs, patterns, error_handler): """Begin a new glob pattern match""" if isinstance(patterns, (str, bytes)): patterns = [patterns] result = [] for pattern in patterns: if not pattern: return decode = isinstance(pattern, str) patlist = self.encode(pattern).split(b'/') if not patlist[0]: basedir = b'/' patlist = patlist[1:] else: basedir = None names = [] try: yield from self._glob(fs, basedir, patlist, decode, names) if names: result.extend(names) else: raise SFTPError(FX_NO_SUCH_FILE, 'No matches found') except (OSError, SFTPError) as exc: # pylint: disable=attribute-defined-outside-init exc.srcpath = pattern if error_handler: error_handler(exc) else: raise exc return result @asyncio.coroutine def _copy(self, srcfs, dstfs, srcpath, dstpath, preserve, recurse, follow_symlinks, error_handler): """Copy a file, directory, or symbolic link""" if follow_symlinks: srcattrs = yield from srcfs.stat(srcpath) else: srcattrs = yield from srcfs.lstat(srcpath) try: if stat.S_ISDIR(srcattrs.permissions): if not recurse: raise SFTPError(FX_FAILURE, '%s is a directory' % srcpath.decode('utf-8', errors='replace')) if not (yield from dstfs.isdir(dstpath)): yield from dstfs.mkdir(dstpath) names = yield from srcfs.listdir(srcpath) for name in names: if name in (b'.', b'..'): continue srcfile = srcfs.compose_path(name, parent=srcpath) dstfile = dstfs.compose_path(name, parent=dstpath) yield from self._copy(srcfs, dstfs, srcfile, dstfile, preserve, recurse, follow_symlinks, error_handler) elif stat.S_ISLNK(srcattrs.permissions): targetpath = yield from srcfs.readlink(srcpath) yield from dstfs.symlink(targetpath, dstpath) else: with (yield from srcfs.open(srcpath, 'rb')) as src: with (yield from dstfs.open(dstpath, 'wb')) as dst: while True: data = yield from src.read(_SFTP_BLOCK_SIZE) if not data: break yield from dst.write(data) if preserve: yield from dstfs.setstat( dstpath, SFTPAttrs(permissions=srcattrs.permissions, atime=srcattrs.atime, mtime=srcattrs.mtime)) except (OSError, SFTPError) as exc: # pylint: disable=attribute-defined-outside-init exc.srcpath = srcpath exc.dstpath = dstpath if error_handler: error_handler(exc) else: raise @asyncio.coroutine def _begin_copy(self, srcfs, dstfs, srcpaths, dstpath, preserve, recurse, follow_symlinks, error_handler): """Begin a new file upload, download, or copy""" dst_isdir = dstpath is None or (yield from dstfs.isdir(dstpath)) dstpath = self.encode(dstpath) if isinstance(srcpaths, (str, bytes)): srcpaths = [srcpaths] elif not dst_isdir: raise SFTPError(FX_FAILURE, '%s must be a directory' % dstpath.decode('utf-8', errors='replace')) for srcfile in srcpaths: srcfile = self.encode(srcfile) filename = posixpath.basename(srcfile) if dstpath is None: dstfile = filename elif dst_isdir: dstfile = dstfs.compose_path(filename, parent=dstpath) else: dstfile = dstpath yield from self._copy(srcfs, dstfs, srcfile, dstfile, preserve, recurse, follow_symlinks, error_handler) @asyncio.coroutine def get(self, remotepaths, localpath=None, *, preserve=False, recurse=False, follow_symlinks=False, error_handler=None): """Download remote files This method downloads one or more files or directories from the remote system. Either a single remote path or a sequence of remote paths to download can be provided. When downloading a single file or directory, the local path can be either the full path to download data into or the path to an existing directory where the data should be placed. In the latter case, the base file name from the remote path will be used as the local name. When downloading multiple files, the local path must refer to an existing directory. If no local path is provided, the file is downloaded into the current local working directory. If preserve is ``True``, the access and modification times and permissions of the original file are set on the downloaded file. If recurse is ``True`` and the remote path points at a directory, the entire subtree under that directory is downloaded. If follow_symlinks is set to ``True``, symbolic links found on the remote system will have the contents of their target downloaded rather than creating a local symbolic link. When using this option during a recursive download, one needs to watch out for links that result in loops. If error_handler is specified and an error occurs during the download, this handler will be called with the exception instead of it being raised. This is intended to primarily be used when multiple remote paths are provided or when recurse is set to ``True``, to allow error information to be collected without aborting the download of the remaining files. The error handler can raise an exception if it wants the download to completely stop. Otherwise, after an error, the download will continue starting with the next file. :param remotepaths: The paths of the remote files or directories to download :param string localpath: (optional) The path of the local file or directory to download into :param bool preserve: (optional) Whether or not to preserve the original file attributes :param bool recurse: (optional) Whether or not to recursively copy directories :param bool follow_symlinks: (optional) Whether or not to follow symbolic links :param callable error_handler: (optional) The function to call when an error occurs :type remotepaths: string or bytes, or a sequence of these :raises: | :exc:`OSError` if a local file I/O error occurs | :exc:`SFTPError` if the server returns an error """ yield from self._begin_copy(self, _LocalFile, remotepaths, localpath, preserve, recurse, follow_symlinks, error_handler) @asyncio.coroutine def put(self, localpaths, remotepath=None, *, preserve=False, recurse=False, follow_symlinks=False, error_handler=None): """Upload local files This method uploads one or more files or directories to the remote system. Either a single local path or a sequence of local paths to upload can be provided. When uploading a single file or directory, the remote path can be either the full path to upload data into or the path to an existing directory where the data should be placed. In the latter case, the base file name from the local path will be used as the remote name. When uploading multiple files, the remote path must refer to an existing directory. If no remote path is provided, the file is uploaded into the current remote working directory. If preserve is ``True``, the access and modification times and permissions of the original file are set on the uploaded file. If recurse is ``True`` and the local path points at a directory, the entire subtree under that directory is uploaded. If follow_symlinks is set to ``True``, symbolic links found on the local system will have the contents of their target uploaded rather than creating a remote symbolic link. When using this option during a recursive upload, one needs to watch out for links that result in loops. If error_handler is specified and an error occurs during the upload, this handler will be called with the exception instead of it being raised. This is intended to primarily be used when multiple local paths are provided or when recurse is set to ``True``, to allow error information to be collected without aborting the upload of the remaining files. The error handler can raise an exception if it wants the upload to completely stop. Otherwise, after an error, the upload will continue starting with the next file. :param localpaths: The paths of the local files or directories to upload :param remotepath: (optional) The path of the remote file or directory to upload into :param bool preserve: (optional) Whether or not to preserve the original file attributes :param bool recurse: (optional) Whether or not to recursively copy directories :param bool follow_symlinks: (optional) Whether or not to follow symbolic links :param callable error_handler: (optional) The function to call when an error occurs :type localpaths: string or bytes, or a sequence of these :type remotepath: string or bytes :raises: | :exc:`OSError` if a local file I/O error occurs | :exc:`SFTPError` if the server returns an error """ yield from self._begin_copy(_LocalFile, self, localpaths, remotepath, preserve, recurse, follow_symlinks, error_handler) @asyncio.coroutine def copy(self, srcpaths, dstpath=None, *, preserve=False, recurse=False, follow_symlinks=False, error_handler=None): """Copy remote files to a new location This method copies one or more files or directories on the remote system to a new location. Either a single source path or a sequence of source paths to copy can be provided. When copying a single file or directory, the destination path can be either the full path to copy data into or the path to an existing directory where the data should be placed. In the latter case, the base file name from the source path will be used as the destination name. When copying multiple files, the destination path must refer to an existing remote directory. If no destination path is provided, the file is copied into the current remote working directory. If preserve is ``True``, the access and modification times and permissions of the original file are set on the copied file. If recurse is ``True`` and the source path points at a directory, the entire subtree under that directory is copied. If follow_symlinks is set to ``True``, symbolic links found in the source will have the contents of their target copied rather than creating a copy of the symbolic link. When using this option during a recursive copy, one needs to watch out for links that result in loops. If error_handler is specified and an error occurs during the copy, this handler will be called with the exception instead of it being raised. This is intended to primarily be used when multiple source paths are provided or when recurse is set to ``True``, to allow error information to be collected without aborting the copy of the remaining files. The error handler can raise an exception if it wants the copy to completely stop. Otherwise, after an error, the copy will continue starting with the next file. :param srcpaths: The paths of the remote files or directories to copy :param dstpath: (optional) The path of the remote file or directory to copy into :param bool preserve: (optional) Whether or not to preserve the original file attributes :param bool recurse: (optional) Whether or not to recursively copy directories :param bool follow_symlinks: (optional) Whether or not to follow symbolic links :param callable error_handler: (optional) The function to call when an error occurs :type srcpaths: string or bytes, or a sequence of these :type dstpath: string or bytes :raises: | :exc:`OSError` if a local file I/O error occurs | :exc:`SFTPError` if the server returns an error """ yield from self._begin_copy(self, self, srcpaths, dstpath, preserve, recurse, follow_symlinks, error_handler) @asyncio.coroutine def mget(self, remotepaths, localpath=None, *, preserve=False, recurse=False, follow_symlinks=False, error_handler=None): """Download remote files with glob pattern match This method downloads files and directories from the remote system matching one or more glob patterns. The arguments to this method are identical to the :meth:`get` method, except that the remote paths specified can contain '*' and '?' wildcard characters. """ matches = yield from self._begin_glob(self, remotepaths, error_handler) yield from self._begin_copy(self, _LocalFile, matches, localpath, preserve, recurse, follow_symlinks, error_handler) @asyncio.coroutine def mput(self, localpaths, remotepath=None, *, preserve=False, recurse=False, follow_symlinks=False, error_handler=None): """Upload local files with glob pattern match This method uploads files and directories to the remote system matching one or more glob patterns. The arguments to this method are identical to the :meth:`put` method, except that the local paths specified can contain '*' and '?' wildcard characters. """ matches = yield from self._begin_glob(_LocalFile, localpaths, error_handler) yield from self._begin_copy(_LocalFile, self, matches, remotepath, preserve, recurse, follow_symlinks, error_handler) @asyncio.coroutine def mcopy(self, srcpaths, dstpath=None, *, preserve=False, recurse=False, follow_symlinks=False, error_handler=None): """Download remote files with glob pattern match This method copies files and directories on the remote system matching one or more glob patterns. The arguments to this method are identical to the :meth:`copy` method, except that the source paths specified can contain '*' and '?' wildcard characters. """ matches = yield from self._begin_glob(self, srcpaths, error_handler) yield from self._begin_copy(self, self, matches, dstpath, preserve, recurse, follow_symlinks, error_handler) @asyncio.coroutine def glob(self, patterns, error_handler=None): """Match remote files against glob patterns This method matches remote files against one or more glob patterns. Either a single pattern or a sequence of patterns can be provided to match against. If error_handler is specified and an error occurs during the match, this handler will be called with the exception instead of it being raised. This is intended to primarily be used when multiple patterns are provided to allow error information to be collected without aborting the match against the remaining patterns. The error handler can raise an exception if it wants to completely abort the match. Otherwise, after an error, the match will continue starting with the next pattern. An error will be raised if any of the patterns completely fail to match, and this can either stop the match against the remaining patterns or be handled by the error_handler just like other errors. :param patterns: Glob patterns to try and match remote files against :param callable error_handler: (optional) The function to call when an error occurs :type patterns: string or bytes, or a sequence of these :raises: :exc:`SFTPError` if the server returns an error or no match is found """ return (yield from self._begin_glob(self, patterns, error_handler)) @asyncio.coroutine def open(self, path, pflags_or_mode=FXF_READ, attrs=SFTPAttrs(), encoding='utf-8', errors='strict'): """Open a remote file This method opens a remote file and returns an :class:`SFTPFile` object which can be used to read and write data and get and set file attributes. The path can be either a string or bytes value. If it is a string, it will be encoded using the file encoding specified when the :class:`SFTPClient` was started. The following open mode flags are supported: ========== ====================================================== Mode Description ========== ====================================================== FXF_READ Open the file for reading. FXF_WRITE Open the file for writing. If both this and FXF_READ are set, open the file for both reading and writing. FXF_APPEND Force writes to append data to the end of the file regardless of seek position. FXF_CREAT Create the file if it doesn't exist. Without this, attempts to open a non-existent file will fail. FXF_TRUNC Truncate the file to zero length if it already exists. FXF_EXCL Return an error when trying to open a file which already exists. ========== ====================================================== By default, file data is read and written as strings in UTF-8 format with strict error checking, but this can be changed using the ``encoding`` and ``errors`` parameters. To read and write data as bytes in binary format, an ``encoding`` value of ``None`` can be used. Instead of these flags, a Python open mode string can also be provided. Python open modes map to the above flags as follows: ==== ============================================= Mode Flags ==== ============================================= r FXF_READ w FXF_WRITE | FXF_CREAT | FXF_TRUNC a FXF_WRITE | FXF_CREAT | FXF_APPEND x FXF_WRITE | FXF_CREAT | FXF_EXCL r+ FXF_READ | FXF_WRITE w+ FXF_READ | FXF_WRITE | FXF_CREAT | FXF_TRUNC a+ FXF_READ | FXF_WRITE | FXF_CREAT | FXF_APPEND x+ FXF_READ | FXF_WRITE | FXF_CREAT | FXF_EXCL ==== ============================================= Including a 'b' in the mode causes the ``encoding`` to be set to ``None``, forcing all data to be read and written as bytes in binary format. The attrs argument is used to set initial attributes of the file if it needs to be created. Otherwise, this argument is ignored. :param path: The name of the remote file to open :param pflags_or_mode: (optional) The access mode to use for the remote file (see above) :param attrs: (optional) File attributes to use if the file needs to be created :param string encoding: (optional) The Unicode encoding to use for data read and written to the remote file :param string errors: (optional) The error-handling mode if an invalid Unicode byte sequence is detected, defaulting to 'strict' which raises an exception :type path: string or bytes :type pflags_or_mode: integer or string :type attrs: :class:`SFTPAttrs` :returns: An :class:`SFTPFile` to use to access the file :raises: | :exc:`ValueError` if the mode is not valid | :exc:`SFTPError` if the server returns an error """ if isinstance(pflags_or_mode, str): mode = pflags_or_mode if 'b' in mode: # Avoid a false positive where pylint thinks mode is an int # pylint: disable=no-member mode = mode.replace('b', '') encoding = None pflags = self._open_modes.get(mode) if not pflags: raise ValueError('Invalid mode: %r' % mode) else: pflags = pflags_or_mode path = self.compose_path(path) handle = yield from self._session.open(path, pflags, attrs) return SFTPFile(self._session, handle, pflags & FXF_APPEND, encoding, errors) @asyncio.coroutine def stat(self, path): """Get attributes of a remote file or directory, following symlinks This method queries the attributes of a remote file or directory. If the path provided is a symbolic link, the returned attributes will correspond to the target of the link. :param path: The path of the remote file or directory to get attributes for :type path: string or bytes :returns: An :class:`SFTPAttrs` containing the file attributes :raises: :exc:`SFTPError` if the server returns an error """ path = self.compose_path(path) return (yield from self._session.stat(path)) @asyncio.coroutine def lstat(self, path): """Get attributes of a remote file, directory, or symlink This method queries the attributes of a remote file, directory, or symlink. Unlike :meth:`stat`, this method returns the attributes of a symlink itself rather than the target of that link. :param path: The path of the remote file, directory, or link to get attributes for :type path: string or bytes :returns: An :class:`SFTPAttrs` containing the file attributes :raises: :exc:`SFTPError` if the server returns an error """ path = self.compose_path(path) return (yield from self._session.lstat(path)) @asyncio.coroutine def setstat(self, path, attrs): """Set attributes of a remote file or directory This method sets attributes of a remote file or directory. If the path provided is a symbolic link, the attributes will be set on the target of the link. A subset of the fields in ``attrs`` can be initialized and only those attributes will be changed. :param path: The path of the remote file or directory to set attributes for :param attrs: File attributes to set :type path: string or bytes :type attrs: :class:`SFTPAttrs` :raises: :exc:`SFTPError` if the server returns an error """ path = self.compose_path(path) yield from self._session.setstat(path, attrs) @asyncio.coroutine def statvfs(self, path): """Get attributes of a remote file system This method queries the attributes of the file system containing the specified path. :param path: The path of the remote file system to get attributes for :type path: string or bytes :returns: An :class:`SFTPVFSAttrs` containing the file system attributes :raises: :exc:`SFTPError` if the server doesn't support this extension or returns an error """ path = self.compose_path(path) return (yield from self._session.statvfs(path)) @asyncio.coroutine def truncate(self, path, size): """Truncate a remote file to the specified size This method truncates a remote file to the specified size. If the path provided is a symbolic link, the target of the link will be truncated. :param path: The path of the remote file to be truncated :param integer size: The desired size of the file, in bytes :type path: string or bytes :raises: :exc:`SFTPError` if the server returns an error """ yield from self.setstat(path, SFTPAttrs(size=size)) @asyncio.coroutine def chown(self, path, uid, gid): """Change the owner user and group id of a remote file or directory This method changes the user and group id of a remote file or directory. If the path provided is a symbolic link, the target of the link will be changed. :param path: The path of the remote file to change :param integer uid: The new user id to assign to the file :param integer gid: The new group id to assign to the file :type path: string or bytes :raises: :exc:`SFTPError` if the server returns an error """ yield from self.setstat(path, SFTPAttrs(uid=uid, gid=gid)) @asyncio.coroutine def chmod(self, path, mode): """Change the file permissions of a remote file or directory This method changes the permissions of a remote file or directory. If the path provided is a symbolic link, the target of the link will be changed. :param path: The path of the remote file to change :param integer mode: The new file permissions, expressed as an integer :type path: string or bytes :raises: :exc:`SFTPError` if the server returns an error """ yield from self.setstat(path, SFTPAttrs(permissions=mode)) @asyncio.coroutine def utime(self, path, times=None): """Change the access and modify times of a remote file or directory This method changes the access and modify times of a remote file or directory. If ``times`` is not provided, the times will be changed to the current time. If the path provided is a symbolic link, the target of the link will be changed. :param path: The path of the remote file to change :param times: (optional) The new access and modify times, as seconds relative to the UNIX epoch :type path: string or bytes :type times: tuple of two integer or float values :raises: :exc:`SFTPError` if the server returns an error """ # pylint: disable=unpacking-non-sequence if times is None: atime = mtime = time.time() else: atime, mtime = times yield from self.setstat(path, SFTPAttrs(atime=atime, mtime=mtime)) @asyncio.coroutine def exists(self, path): """Return if the remote path exists and isn't a broken symbolic link :param path: The remote path to check :type path: string or bytes :raises: :exc:`SFTPError` if the server returns an error """ return bool((yield from self._mode(path))) @asyncio.coroutine def lexists(self, path): """Return if the remote path exists, without following symbolic links :param path: The remote path to check :type path: string or bytes :raises: :exc:`SFTPError` if the server returns an error """ return bool((yield from self._mode(path, statfunc=self.lstat))) @asyncio.coroutine def getatime(self, path): """Return the last access time of a remote file or directory :param path: The remote path to check :type path: string or bytes :raises: :exc:`SFTPError` if the server returns an error """ return (yield from self.stat(path)).atime @asyncio.coroutine def getmtime(self, path): """Return the last modification time of a remote file or directory :param path: The remote path to check :type path: string or bytes :raises: :exc:`SFTPError` if the server returns an error """ return (yield from self.stat(path)).mtime @asyncio.coroutine def getsize(self, path): """Return the size of a remote file or directory :param path: The remote path to check :type path: string or bytes :raises: :exc:`SFTPError` if the server returns an error """ return (yield from self.stat(path)).size @asyncio.coroutine def isdir(self, path): """Return if the remote path refers to a directory :param path: The remote path to check :type path: string or bytes :raises: :exc:`SFTPError` if the server returns an error """ return stat.S_ISDIR((yield from self._mode(path))) @asyncio.coroutine def isfile(self, path): """Return if the remote path refers to a regular file :param path: The remote path to check :type path: string or bytes :raises: :exc:`SFTPError` if the server returns an error """ return stat.S_ISREG((yield from self._mode(path))) @asyncio.coroutine def islink(self, path): """Return if the remote path refers to a symbolic link :param path: The remote path to check :type path: string or bytes :raises: :exc:`SFTPError` if the server returns an error """ return stat.S_ISLNK((yield from self._mode(path, statfunc=self.lstat))) @asyncio.coroutine def remove(self, path): """Remove a remote file This method removes a remote file or symbolic link. :param path: The path of the remote file or link to remove :type path: string or bytes :raises: :exc:`SFTPError` if the server returns an error """ path = self.compose_path(path) yield from self._session.remove(path) @asyncio.coroutine def unlink(self, path): """Remove a remote file (see :meth:`remove`)""" yield from self.remove(path) @asyncio.coroutine def rename(self, oldpath, newpath): """Rename a remote file, directory, or link This method renames a remote file, directory, or link. .. note:: This requests the standard SFTP version of rename which will not overwrite the new path if it already exists. To request POSIX behavior where the new path is removed before the rename, use :meth:`posix_rename`. :param oldpath: The path of the remote file, directory, or link to rename :param newpath: The new name for this file, directory, or link :type oldpath: string or bytes :type newpath: string or bytes :raises: :exc:`SFTPError` if the server returns an error """ oldpath = self.compose_path(oldpath) newpath = self.compose_path(newpath) yield from self._session.rename(oldpath, newpath) def posix_rename(self, oldpath, newpath): """Rename a remote file, directory, or link with POSIX semantics This method renames a remote file, directory, or link, removing the prior instance of new path if it previously existed. This method may not be supported by all SFTP servers. :param oldpath: The path of the remote file, directory, or link to rename :param newpath: The new name for this file, directory, or link :type oldpath: string or bytes :type newpath: string or bytes :raises: :exc:`SFTPError` if the server doesn't support this extension or returns an error """ oldpath = self.compose_path(oldpath) newpath = self.compose_path(newpath) yield from self._session.posix_rename(oldpath, newpath) @asyncio.coroutine def readdir(self, path='.'): """Read the contents of a remote directory This method reads the contents of a directory, returning the names and attributes of what is contained there. If no path is provided, it defaults to the current remote working directory. :param path: (optional) The path of the remote directory to read :type path: string or bytes :returns: A list of :class:`SFTPName` entries, with path names matching the type used to pass in the path :raises: :exc:`SFTPError` if the server returns an error """ names = [] dirpath = self.compose_path(path) handle = yield from self._session.opendir(dirpath) try: while True: names.extend((yield from self._session.readdir(handle))) except SFTPError as exc: if exc.code != FX_EOF: raise finally: yield from self._session.close(handle) if isinstance(path, str): for name in names: name.filename = self.decode(name.filename) name.longname = self.decode(name.longname) return names @asyncio.coroutine def listdir(self, path='.'): """Read the names of the files in a remote directory This method reads the names of files and subdirectories in a remote directory. If no path is provided, it defaults to the current remote working directory. :param path: (optional) The path of the remote directory to read :type path: string or bytes :returns: A list of file/subdirectory names, matching the type used to pass in the path :raises: :exc:`SFTPError` if the server returns an error """ names = yield from self.readdir(path) return [name.filename for name in names] @asyncio.coroutine def mkdir(self, path, attrs=SFTPAttrs()): """Create a remote directory with the specified attributes This method creates a new remote directory at the specified path with the requested attributes. :param path: The path of where the new remote directory should be created :param attrs: (optional) The file attributes to use when creating the directory :type path: string or bytes :type attrs: :class:`SFTPAttrs` :raises: :exc:`SFTPError` if the server returns an error """ path = self.compose_path(path) yield from self._session.mkdir(path, attrs) @asyncio.coroutine def rmdir(self, path): """Remove a remote directory This method removes a remote directory. The directory must be empty for the removal to succeed. :param path: The path of the remote directory to remove :type path: string or bytes :raises: :exc:`SFTPError` if the server returns an error """ path = self.compose_path(path) yield from self._session.rmdir(path) @asyncio.coroutine def realpath(self, path): """Return the canonical version of a remote path This method returns a canonical version of the requested path. :param path: (optional) The path of the remote directory to canonicalize :type path: string or bytes :returns: The canonical path as a string or bytes, matching the type used to pass in the path :raises: :exc:`SFTPError` if the server returns an error """ fullpath = self.compose_path(path) names = yield from self._session.realpath(fullpath) if len(names) > 1: raise SFTPError(FX_BAD_MESSAGE, 'Too many names returned') return self.decode(names[0].filename, isinstance(path, str)) @asyncio.coroutine def getcwd(self): """Return the current remote working directory :returns: The current remote working directory, decoded using the specified path encoding :raises: :exc:`SFTPError` if the server returns an error """ if self._cwd is None: self._cwd = yield from self.realpath(b'.') return self.decode(self._cwd) @asyncio.coroutine def chdir(self, path): """Change the current remote working directory :param path: The path to set as the new remote working directory :type path: string or bytes :raises: :exc:`SFTPError` if the server returns an error """ self._cwd = yield from self.realpath(self.encode(path)) @asyncio.coroutine def readlink(self, path): """Return the target of a remote symbolic link This method returns the target of a symbolic link. :param path: The path of the remote symbolic link to follow :type path: string or bytes :returns: The target path of the link as a string or bytes :raises: :exc:`SFTPError` if the server returns an error """ linkpath = self.compose_path(path) names = yield from self._session.readlink(linkpath) if len(names) > 1: raise SFTPError(FX_BAD_MESSAGE, 'Too many names returned') return self.decode(names[0].filename, isinstance(path, str)) @asyncio.coroutine def symlink(self, oldpath, newpath): """Create a remote symbolic link This method creates a symbolic link. The argument order here matches the standard Python :meth:`os.symlink` call. The argument order sent on the wire is automatically adapted depending on the version information sent by the server, as a number of servers (OpenSSH in particular) did not follow the SFTP standard when implementing this call. :param oldpath: The path the link should point to :param newpath: The path of where to create the remote symbolic link :type oldpath: string or bytes :type newpath: string or bytes :raises: :exc:`SFTPError` if the server returns an error """ oldpath = self.compose_path(oldpath) newpath = self.encode(newpath) yield from self._session.symlink(oldpath, newpath) @asyncio.coroutine def link(self, oldpath, newpath): """Create a remote hard link This method creates a hard link to the remote file specified by oldpath at the location specified by newpath. This method may not be supported by all SFTP servers. :param oldpath: The path of the remote file the hard link should point to :param newpath: The path of where to create the remote hard link :type oldpath: string or bytes :type newpath: string or bytes :raises: :exc:`SFTPError` if the server doesn't support this extension or returns an error """ oldpath = self.compose_path(oldpath) newpath = self.compose_path(newpath) yield from self._session.link(oldpath, newpath) def exit(self): """Exit the SFTP client session This method exists the SFTP client session, closing the corresponding channel opened on the server. """ self._session.exit() class SFTPServerSession(SFTPSession, SSHServerSession): """An SFTP server session handler""" _extensions = [(b'posix-rename@openssh.com', b'1'), (b'statvfs@openssh.com', b'2'), (b'fstatvfs@openssh.com', b'2'), (b'hardlink@openssh.com', b'1'), (b'fsync@openssh.com', b'1')] def __init__(self, server): super().__init__() self._server = server self._version = None self._nonstandard_symlink = False self._next_handle = 0 self._file_handles = {} self._dir_handles = {} self._packet_queue = asyncio.queues.Queue() self._queue_task = asyncio.async(self._process_packet_queue()) def _cleanup(self, code, reason, lang=DEFAULT_LANG): """Clean up this SFTP server session""" if self._queue_task: self._queue_task.cancel() self._queue_task = None if self._server: for file_obj in self._file_handles: self._server.close(file_obj) self._server.exit() self._server = None self._file_handles = [] self._dir_handles = [] super()._cleanup(code, reason, lang) def _get_next_handle(self): """Get the next available unique file handle number""" while True: handle = self._next_handle.to_bytes(4, 'big') self._next_handle = (self._next_handle + 1) & 0xffffffff if (handle not in self._file_handles and handle not in self._dir_handles): return handle def _process_connection_open(self): """Process a newly opened SFTP client connection""" pass def _process_init(self, packet): """Process an incoming SFTP init packet""" version = packet.get_uint32() if version == 3: # Check if the server has a buggy SYMLINK implementation client_version = self._chan.get_extra_info('client_version', '') if any(name in client_version for name in self._nonstandard_symlink_impls): self._nonstandard_symlink = True version = min(version, _SFTP_VERSION) extensions = (String(name) + String(data) for name, data in self._extensions) self.send_packet(Byte(FXP_VERSION), UInt32(version), *extensions) def _process_version(self, packet): """Process an incoming SFTP version packet""" self._cleanup(FX_BAD_MESSAGE, 'Version message not expected on server') def _process_packet(self, pkttype, pktid, packet): """Process other incoming SFTP packets""" self._packet_queue.put_nowait((pkttype, pktid, packet)) @asyncio.coroutine def _process_packet_queue(self): """Process queued SFTP client requests""" while True: pkttype, pktid, packet = yield from self._packet_queue.get() try: if pkttype == FXP_EXTENDED: pkttype = packet.get_string() handler = self._packet_handlers.get(pkttype) if not handler: raise SFTPError(FX_OP_UNSUPPORTED, 'Unsupported request type: %s' % pkttype) return_type = self._return_types.get(pkttype, FXP_STATUS) result = yield from handler(self, packet) if return_type == FXP_STATUS: result = UInt32(FX_OK) + String('') + String('') elif return_type in (FXP_HANDLE, FXP_DATA): result = String(result) elif return_type == FXP_NAME: result = (UInt32(len(result)) + b''.join(name.encode() for name in result)) else: if isinstance(result, os.stat_result): result = SFTPAttrs.from_local(result) elif isinstance(result, os.statvfs_result): result = SFTPVFSAttrs.from_local(result) result = result.encode() except NotImplementedError as exc: name = handler.__name__[9:] return_type = FXP_STATUS result = (UInt32(FX_OP_UNSUPPORTED) + String('Operation not supported: %s' % name) + String(DEFAULT_LANG)) except OSError as exc: return_type = FXP_STATUS result = (UInt32(FX_FAILURE) + String(exc.strerror) + String(DEFAULT_LANG)) except PacketDecodeError as exc: return_type = FXP_STATUS result = (UInt32(FX_BAD_MESSAGE) + String(str(exc)) + String(DEFAULT_LANG)) except SFTPError as exc: return_type = FXP_STATUS result = (UInt32(exc.code) + String(exc.reason) + String(exc.lang)) self.send_packet(Byte(return_type), UInt32(pktid), result) @asyncio.coroutine def _process_open(self, packet): """Process an incoming SFTP open request""" path = packet.get_string() pflags = packet.get_uint32() attrs = SFTPAttrs.decode(packet) packet.check_end() result = self._server.open(path, pflags, attrs) if asyncio.iscoroutine(result): result = yield from result handle = self._get_next_handle() self._file_handles[handle] = result return handle @asyncio.coroutine def _process_close(self, packet): """Process an incoming SFTP close request""" handle = packet.get_string() packet.check_end() file_obj = self._file_handles.pop(handle, None) if file_obj: result = self._server.close(file_obj) if asyncio.iscoroutine(result): yield from result return if self._dir_handles.pop(handle, None) is not None: return raise SFTPError(FX_FAILURE, 'Invalid file handle') @asyncio.coroutine def _process_read(self, packet): """Process an incoming SFTP read request""" handle = packet.get_string() offset = packet.get_uint64() length = packet.get_uint32() packet.check_end() file_obj = self._file_handles.get(handle) if file_obj: result = self._server.read(file_obj, offset, length) if asyncio.iscoroutine(result): result = yield from result if result: return result else: raise SFTPError(FX_EOF, '') else: raise SFTPError(FX_FAILURE, 'Invalid file handle') @asyncio.coroutine def _process_write(self, packet): """Process an incoming SFTP write request""" handle = packet.get_string() offset = packet.get_uint64() data = packet.get_string() packet.check_end() file_obj = self._file_handles.get(handle) if file_obj: result = self._server.write(file_obj, offset, data) if asyncio.iscoroutine(result): result = yield from result return result else: raise SFTPError(FX_FAILURE, 'Invalid file handle') @asyncio.coroutine def _process_lstat(self, packet): """Process an incoming SFTP lstat request""" path = packet.get_string() packet.check_end() result = self._server.lstat(path) if asyncio.iscoroutine(result): result = yield from result return result @asyncio.coroutine def _process_fstat(self, packet): """Process an incoming SFTP fstat request""" handle = packet.get_string() packet.check_end() file_obj = self._file_handles.get(handle) if file_obj: result = self._server.fstat(file_obj) if asyncio.iscoroutine(result): result = yield from result return result else: raise SFTPError(FX_FAILURE, 'Invalid file handle') @asyncio.coroutine def _process_setstat(self, packet): """Process an incoming SFTP setstat request""" path = packet.get_string() attrs = SFTPAttrs.decode(packet) packet.check_end() result = self._server.setstat(path, attrs) if asyncio.iscoroutine(result): result = yield from result return result @asyncio.coroutine def _process_fsetstat(self, packet): """Process an incoming SFTP fsetstat request""" handle = packet.get_string() attrs = SFTPAttrs.decode(packet) packet.check_end() file_obj = self._file_handles.get(handle) if file_obj: result = self._server.fsetstat(file_obj, attrs) if asyncio.iscoroutine(result): result = yield from result return result else: raise SFTPError(FX_FAILURE, 'Invalid file handle') @asyncio.coroutine def _process_opendir(self, packet): """Process an incoming SFTP opendir request""" path = packet.get_string() packet.check_end() listdir_result = self._server.listdir(path) if asyncio.iscoroutine(listdir_result): listdir_result = yield from listdir_result for i, name in enumerate(listdir_result): # pylint: disable=no-member if isinstance(name, bytes): name = SFTPName(name) listdir_result[i] = name # pylint: disable=attribute-defined-outside-init filename = os.path.join(path, name.filename) attr_result = self._server.lstat(filename) if asyncio.iscoroutine(attr_result): attr_result = yield from attr_result if isinstance(attr_result, os.stat_result): attr_result = SFTPAttrs.from_local(attr_result) name.attrs = attr_result if not name.longname: longname_result = self._server.format_longname(name) if asyncio.iscoroutine(longname_result): yield from longname_result handle = self._get_next_handle() self._dir_handles[handle] = listdir_result return handle @asyncio.coroutine def _process_readdir(self, packet): """Process an incoming SFTP readdir request""" handle = packet.get_string() packet.check_end() names = self._dir_handles.get(handle) if names: self._dir_handles[handle] = [] return names else: raise SFTPError(FX_EOF, '') @asyncio.coroutine def _process_remove(self, packet): """Process an incoming SFTP remove request""" path = packet.get_string() packet.check_end() result = self._server.remove(path) if asyncio.iscoroutine(result): result = yield from result return result @asyncio.coroutine def _process_mkdir(self, packet): """Process an incoming SFTP mkdir request""" path = packet.get_string() attrs = SFTPAttrs.decode(packet) packet.check_end() result = self._server.mkdir(path, attrs) if asyncio.iscoroutine(result): result = yield from result return result @asyncio.coroutine def _process_rmdir(self, packet): """Process an incoming SFTP rmdir request""" path = packet.get_string() packet.check_end() result = self._server.rmdir(path) if asyncio.iscoroutine(result): result = yield from result return result @asyncio.coroutine def _process_realpath(self, packet): """Process an incoming SFTP realpath request""" path = packet.get_string() packet.check_end() return [SFTPName(self._server.realpath(path))] @asyncio.coroutine def _process_stat(self, packet): """Process an incoming SFTP stat request""" path = packet.get_string() packet.check_end() result = self._server.stat(path) if asyncio.iscoroutine(result): result = yield from result return result @asyncio.coroutine def _process_rename(self, packet): """Process an incoming SFTP rename request""" oldpath = packet.get_string() newpath = packet.get_string() packet.check_end() result = self._server.rename(oldpath, newpath) if asyncio.iscoroutine(result): result = yield from result return result @asyncio.coroutine def _process_readlink(self, packet): """Process an incoming SFTP readlink request""" path = packet.get_string() packet.check_end() result = self._server.readlink(path) if asyncio.iscoroutine(result): result = yield from result return [SFTPName(result)] @asyncio.coroutine def _process_symlink(self, packet): """Process an incoming SFTP symlink request""" if self._nonstandard_symlink: oldpath = packet.get_string() newpath = packet.get_string() else: newpath = packet.get_string() oldpath = packet.get_string() packet.check_end() result = self._server.symlink(oldpath, newpath) if asyncio.iscoroutine(result): result = yield from result return result @asyncio.coroutine def _process_posix_rename(self, packet): """Process an incoming SFTP POSIX rename request""" oldpath = packet.get_string() newpath = packet.get_string() packet.check_end() result = self._server.posix_rename(oldpath, newpath) if asyncio.iscoroutine(result): result = yield from result return result @asyncio.coroutine def _process_statvfs(self, packet): """Process an incoming SFTP statvfs request""" path = packet.get_string() packet.check_end() result = self._server.statvfs(path) if asyncio.iscoroutine(result): result = yield from result return result @asyncio.coroutine def _process_fstatvfs(self, packet): """Process an incoming SFTP fstatvfs request""" handle = packet.get_string() packet.check_end() file_obj = self._file_handles.get(handle) if file_obj: result = self._server.fstatvfs(file_obj) if asyncio.iscoroutine(result): result = yield from result return result else: raise SFTPError(FX_FAILURE, 'Invalid file handle') @asyncio.coroutine def _process_link(self, packet): """Process an incoming SFTP hard link request""" oldpath = packet.get_string() newpath = packet.get_string() packet.check_end() result = self._server.link(oldpath, newpath) if asyncio.iscoroutine(result): result = yield from result return result @asyncio.coroutine def _process_fsync(self, packet): """Process an incoming SFTP fsync request""" handle = packet.get_string() packet.check_end() file_obj = self._file_handles.get(handle) if file_obj: result = self._server.fsync(file_obj) if asyncio.iscoroutine(result): result = yield from result return result else: raise SFTPError(FX_FAILURE, 'Invalid file handle') _packet_handlers = { FXP_OPEN: _process_open, FXP_CLOSE: _process_close, FXP_READ: _process_read, FXP_WRITE: _process_write, FXP_LSTAT: _process_lstat, FXP_FSTAT: _process_fstat, FXP_SETSTAT: _process_setstat, FXP_FSETSTAT: _process_fsetstat, FXP_OPENDIR: _process_opendir, FXP_READDIR: _process_readdir, FXP_REMOVE: _process_remove, FXP_MKDIR: _process_mkdir, FXP_RMDIR: _process_rmdir, FXP_REALPATH: _process_realpath, FXP_STAT: _process_stat, FXP_RENAME: _process_rename, FXP_READLINK: _process_readlink, FXP_SYMLINK: _process_symlink, b'posix-rename@openssh.com': _process_posix_rename, b'statvfs@openssh.com': _process_statvfs, b'fstatvfs@openssh.com': _process_fstatvfs, b'hardlink@openssh.com': _process_link, b'fsync@openssh.com': _process_fsync } class SFTPServer: """SFTP server Applications should subclass this when implementing an SFTP server. The methods listed below should be implemented to provide the desired application behavior. .. note:: Any method can optionally be defined as a coroutine if that method needs to perform blocking opertions to determine its result. The ``conn`` object provided here refers to the :class:`SSHServerConnection` instance this SFTP server is associated with. It can be queried to determine which user the client authenticated as or to request key and certificate options or permissions which should be applied to this session. If the ``chroot`` argument is specified when this object is created, the default :meth:`map_path` and :meth:`reverse_map_path` methods will enforce a virtual root directory starting in that location, limiting access to only files within that directory tree. This will also affect path names returned by the :meth:`realpath` and :meth:`readlink` methods. """ # The default implementation of a number of these methods don't need self # pylint: disable=no-self-use def __init__(self, conn, chroot=None): self._conn = conn if chroot: self._chroot = os.fsencode(os.path.realpath(chroot)) else: self._chroot = None def format_longname(self, name): """Format the long name associated with an SFTP name This method fills in the ``longname`` field of a :class:`SFTPName` object. By default, it generates something similar to UNIX "ls -l" output. The ``filename`` and ``attrs`` fields of the :class:`SFTPName` should already be filled in before this method is called. :param name: The :class:`SFTPName` instance to format the long name for :type name: :class:`SFTPName` """ if name.attrs.permissions is not None: mode = stat.filemode(name.attrs.permissions) else: mode = '' nlink = str(name.attrs.nlink) if name.attrs.nlink else '' if name.attrs.uid is not None: try: user = pwd.getpwuid(name.attrs.uid).pw_name except KeyError: user = str(name.attrs.uid) else: user = '' if name.attrs.gid is not None: try: group = grp.getgrgid(name.attrs.gid).gr_name except KeyError: group = str(name.attrs.gid) else: group = '' size = str(name.attrs.size) if name.attrs.size is not None else '' if name.attrs.mtime is not None: now = time.time() mtime = time.localtime(name.attrs.mtime) if now - 365*24*60*60/2 < name.attrs.mtime <= now: modtime = time.strftime('%b %e %H:%M', mtime) else: modtime = time.strftime('%b %e %Y', mtime) else: modtime = '' detail = '{:10s} {:>4s} {:8s} {:8s} {:>8s} {:12s} '.format( mode, nlink, user, group, size, modtime) name.longname = detail.encode('utf-8') + name.filename def map_path(self, path): """Map the path requested by the client to a local path This method can be overridden to provide a custom mapping from path names requested by the client to paths in the local filesystem. By default, it will enforce a virtual "chroot" if one was specified when this server was created. Otherwise, path names are left unchanged, with relative paths being interpreted based on the working directory of the currently running process. :param bytes path: The path name to map :returns: A byte string containing he local path name to operate on """ if self._chroot: normpath = os.path.normpath(os.path.join(b'/', path)) return os.path.join(self._chroot, normpath[1:]) else: return path def reverse_map_path(self, path): """Reverse map a local path into the path reported to the client This method can be overridden to provide a custom reverse mapping for the mapping provided by :meth:`map_path`. By default, it hides the portion of the local path associated with the virtual "chroot" if one was specified. :param bytes path: The local path name to reverse map :returns: A byte string containing the path name to report to the client """ if self._chroot: if path == self._chroot: return b'/' elif path.startswith(self._chroot + b'/'): return path[len(self._chroot):] else: raise SFTPError(FX_NO_SUCH_FILE, 'File not found') else: return path def open(self, path, pflags, attrs): """Open a file to serve to a remote client This method returns a file object which can be used to read and write data and get and set file attributes. The possible open mode flags and their meanings are: ========== ====================================================== Mode Description ========== ====================================================== FXF_READ Open the file for reading. If neither FXF_READ nor FXF_WRITE are set, this is the default. FXF_WRITE Open the file for writing. If both this and FXF_READ are set, open the file for both reading and writing. FXF_APPEND Force writes to append data to the end of the file regardless of seek position. FXF_CREAT Create the file if it doesn't exist. Without this, attempts to open a non-existent file will fail. FXF_TRUNC Truncate the file to zero length if it already exists. FXF_EXCL Return an error when trying to open a file which already exists. ========== ====================================================== The attrs argument is used to set initial attributes of the file if it needs to be created. Otherwise, this argument is ignored. :param bytes path: The name of the file to open :param integer pflags: The access mode to use for the file (see above) :param attrs: File attributes to use if the file needs to be created :type attrs: :class:`SFTPAttrs` :returns: A file object to use to access the file :raises: :exc:`SFTPError` to return an error to the client """ if pflags & FXF_EXCL: mode = 'xb' elif pflags & FXF_APPEND: mode = 'ab' elif pflags & FXF_WRITE and not pflags & FXF_READ: mode = 'wb' else: mode = 'rb' if pflags & FXF_READ and pflags & FXF_WRITE: mode += '+' flags = os.O_RDWR elif pflags & FXF_WRITE: flags = os.O_WRONLY else: flags = os.O_RDONLY if pflags & FXF_APPEND: flags |= os.O_APPEND if pflags & FXF_CREAT: flags |= os.O_CREAT if pflags & FXF_TRUNC: flags |= os.O_TRUNC if pflags & FXF_EXCL: flags |= os.O_EXCL flags |= getattr(os, 'O_BINARY', 0) perms = 0o666 if attrs.permissions is None else attrs.permissions return open(self.map_path(path), mode, buffering=0, opener=lambda path, _: os.open(path, flags, perms)) def close(self, file_obj): """Close an open file or directory :param file file_obj: The file or directory object to close :raises: :exc:`SFTPError` to return an error to the client """ file_obj.close() def read(self, file_obj, offset, size): """Read data from an open file :param file file_obj: The file to read from :param integer offset: The offset from the beginning of the file to begin reading :param integer size: The number of bytes to read :returns: bytes read from the file :raises: :exc:`SFTPError` to return an error to the client """ file_obj.seek(offset) return file_obj.read(size) def write(self, file_obj, offset, data): """Write data to an open file :param file file_obj: The file to write to :param integer offset: The offset from the beginning of the file to begin writing :param bytes data: The data to write to the file :returns: number of bytes written :raises: :exc:`SFTPError` to return an error to the client """ file_obj.seek(offset) return file_obj.write(data) def lstat(self, path): """Get attributes of a file, directory, or symlink This method queries the attributes of a file, directory, or symlink. Unlike :meth:`stat`, this method should return the attributes of a symlink itself rather than the target of that link. :param bytes path: The path of the file, directory, or link to get attributes for :returns: An :class:`SFTPAttrs` or an os.stat_result containing the file attributes :raises: :exc:`SFTPError` to return an error to the client """ return os.lstat(self.map_path(path)) def fstat(self, file_obj): """Get attributes of an open file :param file file_obj: The file to get attributes for :returns: An :class:`SFTPAttrs` or an os.stat_result containing the file attributes :raises: :exc:`SFTPError` to return an error to the client """ file_obj.flush() return os.fstat(file_obj.fileno()) def setstat(self, path, attrs): """Set attributes of a file or directory This method sets attributes of a file or directory. If the path provided is a symbolic link, the attributes should be set on the target of the link. A subset of the fields in ``attrs`` can be initialized and only those attributes should be changed. :param bytes path: The path of the remote file or directory to set attributes for :param attrs: File attributes to set :type attrs: :class:`SFTPAttrs` :raises: :exc:`SFTPError` to return an error to the client """ _setstat(self.map_path(path), attrs) def fsetstat(self, file_obj, attrs): """Set attributes of an open file :param attrs: File attributes to set on the file :type attrs: :class:`SFTPAttrs` :raises: :exc:`SFTPError` to return an error to the client """ file_obj.flush() _setstat(file_obj.fileno(), attrs) def listdir(self, path): """List the contents of a directory :param bytes path: The path of the directory to open :returns: A list of names of files in the directory :raises: :exc:`SFTPError` to return an error to the client """ return os.listdir(self.map_path(path)) def remove(self, path): """Remove a file or symbolic link :param bytes path: The path of the file or link to remove :raises: :exc:`SFTPError` to return an error to the client """ return os.remove(self.map_path(path)) def mkdir(self, path, attrs): """Create a directory with the specified attributes :param bytes path: The path of where the new directory should be created :param attrs: The file attributes to use when creating the directory :type attrs: :class:`SFTPAttrs` :raises: :exc:`SFTPError` to return an error to the client """ mode = 0o777 if attrs.permissions is None else attrs.permissions return os.mkdir(self.map_path(path), mode) def rmdir(self, path): """Remove a directory :param bytes path: The path of the directory to remove :raises: :exc:`SFTPError` to return an error to the client """ return os.rmdir(self.map_path(path)) def realpath(self, path): """Return the canonical version of a path :param bytes path: The path of the directory to canonicalize :returns: A byte string containing the canonical path :raises: :exc:`SFTPError` to return an error to the client """ return self.reverse_map_path(os.path.realpath(self.map_path(path))) def stat(self, path): """Get attributes of a file or directory, following symlinks This method queries the attributes of a file or directory. If the path provided is a symbolic link, the returned attributes should correspond to the target of the link. :param bytes path: The path of the remote file or directory to get attributes for :returns: An :class:`SFTPAttrs` or an os.stat_result containing the file attributes :raises: :exc:`SFTPError` to return an error to the client """ return os.stat(self.map_path(path)) def rename(self, oldpath, newpath): """Rename a file, directory, or link This method renames a file, directory, or link. .. note:: This is a request for the standard SFTP version of rename which will not overwrite the new path if it already exists. The :meth:`posix_rename` method will be called if the client requests the POSIX behavior where an existing instance of the new path is removed before the rename. :param bytes oldpath: The path of the file, directory, or link to rename :param bytes newpath: The new name for this file, directory, or link :raises: :exc:`SFTPError` to return an error to the client """ oldpath = self.map_path(oldpath) newpath = self.map_path(newpath) if os.path.exists(newpath): raise SFTPError(FX_FAILURE, 'File already exists') return os.rename(oldpath, newpath) def readlink(self, path): """Return the target of a symbolic link :param bytes path: The path of the symbolic link to follow :returns: A byte string containing the target path of the link :raises: :exc:`SFTPError` to return an error to the client """ target = os.readlink(self.map_path(path)) if os.path.isabs(target): return self.reverse_map_path(target) else: return target def symlink(self, oldpath, newpath): """Create a symbolic link :param bytes oldpath: The path the link should point to :param bytes newpath: The path of where to create the symbolic link :raises: :exc:`SFTPError` to return an error to the client """ if posixpath.isabs(oldpath): oldpath = self.map_path(oldpath) else: newdir = posixpath.dirname(newpath) abspath1 = self.map_path(posixpath.join(newdir, oldpath)) mapped_newdir = self.map_path(newdir) abspath2 = os.path.join(mapped_newdir, oldpath) # Make sure the symlink doesn't point outside the chroot if os.path.realpath(abspath1) != os.path.realpath(abspath2): oldpath = os.path.relpath(abspath1, start=mapped_newdir) newpath = self.map_path(newpath) return os.symlink(oldpath, newpath) def posix_rename(self, oldpath, newpath): """Rename a file, directory, or link with POSIX semantics This method renames a file, directory, or link, removing the prior instance of new path if it previously existed. :param bytes oldpath: The path of the file, directory, or link to rename :param bytes newpath: The new name for this file, directory, or link :raises: :exc:`SFTPError` to return an error to the client """ return os.rename(self.map_path(oldpath), self.map_path(newpath)) def statvfs(self, path): """Get attributes of the file system containing a file :param bytes path: The path of the file system to get attributes for :returns: An :class:`SFTPVFSAttrs` or an os.statvfs_result containing the file system attributes :raises: :exc:`SFTPError` to return an error to the client """ return os.statvfs(self.map_path(path)) def fstatvfs(self, file_obj): """Return attributes of the file system containing an open file :param file file_obj: The open file to get file system attributes for :returns: An :class:`SFTPVFSAttrs` or an os.statvfs_result containing the file system attributes :raises: :exc:`SFTPError` to return an error to the client """ return os.statvfs(file_obj.fileno()) def link(self, oldpath, newpath): """Create a hard link :param bytes oldpath: The path of the file the hard link should point to :param bytes newpath: The path of where to create the hard link :raises: :exc:`SFTPError` to return an error to the client """ return os.link(self.map_path(oldpath), self.map_path(newpath)) def fsync(self, file_obj): """Force file data to be written to disk :param file file_obj: The open file containing the data to flush to disk :raises: :exc:`SFTPError` to return an error to the client """ os.fsync(file_obj.fileno()) def exit(self): """Shut down this SFTP server""" pass asyncssh-1.3.0/asyncssh/stream.py000066400000000000000000000356151260630620200170550ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """SSH stream handlers""" import asyncio from .constants import EXTENDED_DATA_STDERR from .misc import BreakReceived, SignalReceived, TerminalSizeChanged from .session import SSHClientSession, SSHServerSession, SSHTCPSession class SSHReader: """SSH read stream handler""" def __init__(self, session, chan, datatype=None): self._session = session self._chan = chan self._datatype = datatype @property def channel(self): """The SSH channel associated with this stream""" return self._chan def get_extra_info(self, name, default=None): """Return additional information about this stream This method returns extra information about the channel associated with this stream. See :meth:`get_extra_info() ` on :class:`SSHClientChannel` for additional information. """ return self._chan.get_extra_info(name, default) def read(self, n=-1): """Read data from the stream This method is a coroutine which reads up to ``n`` bytes or characters from the stream. If ``n`` is not provided or set to ``-1``, it reads until EOF or until a signal is received on the stream. If EOF was received and the receive buffer is empty, an empty ``bytes`` or ``string`` object is returned. .. note:: Unlike traditional ``asyncio`` stream readers, the data will be delivered as either bytes or a string depending on whether an encoding was specified when the underlying channel was opened. """ return self._session.read(n, self._datatype, exact=False) def readline(self): """Read one line from the stream This method is a coroutine which reads one line, ending in ``'\\n'``. If EOF was received before ``'\\n'`` was found, the partial line is returned. If EOF was received and the receive buffer is empty, an empty ``bytes`` or ``string`` object is returned. """ return self._session.readline(self._datatype) def readexactly(self, n): """Read an exact amount of data from the stream This method is a coroutine which reads exactly n bytes or characters from the stream. If EOF is received before ``n`` bytes are read, an :exc:`IncompleteReadError ` is raised and its ``partial`` attribute contains the partially read data. """ return self._session.read(n, self._datatype, exact=True) def at_eof(self): """Return whether the stream is at EOF This method returns ``True`` when EOF has been received and all data in the stream has been read. """ return self._session.at_eof(self._datatype) class SSHWriter: """SSH write stream handler""" def __init__(self, session, chan, datatype=None): self._session = session self._chan = chan self._datatype = datatype @property def channel(self): """The SSH channel associated with this stream""" return self._chan def get_extra_info(self, name, default=None): """Return additional information about this stream This method returns extra information about the channel associated with this stream. See :meth:`get_extra_info() ` on :class:`SSHClientChannel` for additional information. """ return self._chan.get_extra_info(name, default) def can_write_eof(self): """Return whether the stream supports :meth:`write_eof`""" return self._chan.can_write_eof() def close(self): """Close the channel .. note:: After this is called, no data can be read or written from any of the streams associated with this channel. """ return self._chan.close() @asyncio.coroutine def drain(self): """Wait until the write buffer on the channel is flushed This method is a coroutine which blocks the caller if the stream is currently paused for writing, returning when enough data has been sent on the channel to allow writing to resume. This can be used to avoid buffering an excessive amount of data in the channel's send buffer. """ return (yield from self._session.drain()) def write(self, data): """Write data to the stream This method writes bytes or characters to the stream. .. note:: Unlike traditional ``asyncio`` stream writers, the data must be supplied as either bytes or a string depending on whether an encoding was specified when the underlying channel was opened. """ return self._chan.write(data, self._datatype) def writelines(self, list_of_data): """Write a collection of data to the stream""" return self._chan.writelines(list_of_data, self._datatype) def write_eof(self): """Write EOF on the channel This method sends an end-of-file indication on the channel, after which no more data can be written. .. note:: On an :class:`SSHServerChannel` where multiple output streams are created, writing EOF on one stream signals EOF for all of them, since it applies to the channel as a whole. """ return self._chan.write_eof() class SSHStreamSession: """SSH stream session handler""" def __init__(self): self._chan = None self._loop = None self._limit = None self._exception = None self._eof_received = False self._connection_lost = False self._recv_buf = {None: []} self._recv_buf_len = 0 self._read_waiter = {None: None} self._write_paused = False self._drain_waiters = [] @asyncio.coroutine def _block_read(self, datatype): """Wait for more data to arrive on the stream""" if self._read_waiter[datatype]: raise RuntimeError('read called while another coroutine is ' 'already waiting to read') waiter = asyncio.Future(loop=self._loop) self._read_waiter[datatype] = waiter yield from waiter def _unblock_read(self, datatype): """Signal that more data has arrived on the stream""" waiter = self._read_waiter[datatype] if waiter: if not waiter.cancelled(): waiter.set_result(None) self._read_waiter[datatype] = None def _unblock_drain(self): """Signal that more data can be written on the stream""" for waiter in self._drain_waiters: if not waiter.cancelled(): waiter.set_result(None) self._drain_waiters = [] def connection_made(self, chan): """Handle a newly opened channel""" self._chan = chan self._loop = chan.get_loop() self._limit = self._chan.get_recv_window() for datatype in chan.get_read_datatypes(): self._recv_buf[datatype] = [] self._read_waiter[datatype] = None def connection_lost(self, exc): """Handle an incoming channel close""" self._connection_lost = True self._exception = exc if not self._eof_received: if exc: for datatype in self._read_waiter.keys(): self._recv_buf[datatype].append(exc) self.eof_received() if self._write_paused: self._unblock_drain() def data_received(self, data, datatype): """Handle incoming data on the channel""" self._recv_buf[datatype].append(data) self._recv_buf_len += len(data) self._unblock_read(datatype) if self._recv_buf_len >= self._limit: self._chan.pause_reading() def eof_received(self): """Handle an incoming end of file on the channel""" self._eof_received = True for datatype in self._read_waiter.keys(): self._unblock_read(datatype) return True def at_eof(self, datatype): """Return whether end of file has been received on the channel""" return self._eof_received and not self._recv_buf[datatype] def pause_writing(self): """Handle a request to pause writing on the channel""" self._write_paused = True def resume_writing(self): """Handle a request to resume writing on the channel""" self._write_paused = False self._unblock_drain() @asyncio.coroutine def read(self, n, datatype, exact): """Read data from the channel""" recv_buf = self._recv_buf[datatype] buf = '' if self._chan.get_encoding() else b'' data = [] while True: while recv_buf and n != 0: if isinstance(recv_buf[0], Exception): if data: break else: raise recv_buf.pop(0) l = len(recv_buf[0]) if n > 0 and l > n: data.append(recv_buf[0][:n]) recv_buf[0] = recv_buf[0][n:] self._recv_buf_len -= n n = 0 break data.append(recv_buf.pop(0)) self._recv_buf_len -= l n -= l if self._recv_buf_len < self._limit: self._chan.resume_reading() if n == 0 or (n > 0 and data and not exact) or self._eof_received: break yield from self._block_read(datatype) buf = buf.join(data) if n > 0 and exact: raise asyncio.IncompleteReadError(buf, len(buf) + n) return buf @asyncio.coroutine def readline(self, datatype): """Read a line from the channel""" recv_buf = self._recv_buf[datatype] buf, sep = ('', '\n') if self._chan.get_encoding() else (b'', b'\n') data = [] while True: while recv_buf: if isinstance(recv_buf[0], Exception): if data: return buf.join(data) else: raise recv_buf.pop(0) idx = recv_buf[0].find(sep) + 1 if idx > 0: data.append(recv_buf[0][:idx]) recv_buf[0] = recv_buf[0][idx:] self._recv_buf_len -= idx if self._recv_buf_len < self._limit: self._chan.resume_reading() return buf.join(data) l = len(recv_buf[0]) data.append(recv_buf.pop(0)) self._recv_buf_len -= l if self._recv_buf_len < self._limit: self._chan.resume_reading() if self._eof_received: return buf.join(data) yield from self._block_read(datatype) @asyncio.coroutine def drain(self): """Wait for data written to the channel to drain""" if self._write_paused and not self._connection_lost: waiter = asyncio.Future(loop=self._loop) self._drain_waiters.append(waiter) yield from waiter if self._connection_lost: exc = self._exception if not exc and self._write_paused: exc = BrokenPipeError() if exc: raise exc # pylint: disable=raising-bad-type class SSHClientStreamSession(SSHStreamSession, SSHClientSession): """SSH client stream session handler""" class SSHServerStreamSession(SSHStreamSession, SSHServerSession): """SSH server stream session handler""" def __init__(self, allow_pty, session_factory, sftp_factory): super().__init__() self._allow_pty = allow_pty self._session_factory = session_factory self._sftp_factory = sftp_factory def pty_requested(self, term_type, term_size, term_modes): """Return whether a pseudo-tty can be requested""" return self._allow_pty def shell_requested(self): """Return whether a shell can be requested""" return bool(self._session_factory) def exec_requested(self, command): """Return whether execution of a command can be requested""" return bool(self._session_factory) def subsystem_requested(self, subsystem): """Return whether starting a subsystem can be requested""" if subsystem == 'sftp': return bool(self._sftp_factory) else: return bool(self._session_factory) def session_started(self): """Start a session for this newly opened server channel""" if self._chan.get_subsystem() == 'sftp': self._chan.start_sftp_server(self._sftp_factory) else: handler = self._session_factory(SSHReader(self, self._chan), SSHWriter(self, self._chan), SSHWriter(self, self._chan, EXTENDED_DATA_STDERR)) if asyncio.iscoroutine(handler): asyncio.async(handler) def break_received(self, msec): """Handle an incoming break on the channel""" self._recv_buf[None].append(BreakReceived(msec)) self._unblock_read(None) return True def signal_received(self, signal): """Handle an incoming signal on the channel""" self._recv_buf[None].append(SignalReceived(signal)) self._unblock_read(None) def terminal_size_changed(self, *args): """Handle an incoming terminal size change on the channel""" self._recv_buf[None].append(TerminalSizeChanged(*args)) self._unblock_read(None) class SSHTCPStreamSession(SSHStreamSession, SSHTCPSession): """SSH TCP stream session handler""" def __init__(self, handler_factory=None): super().__init__() self._handler_factory = handler_factory def session_started(self): """Start a session for this newly opened TCP channel""" if self._handler_factory: handler = self._handler_factory(SSHReader(self, self._chan), SSHWriter(self, self._chan)) if asyncio.iscoroutine(handler): asyncio.async(handler) asyncssh-1.3.0/asyncssh/version.py000066400000000000000000000011011260630620200172260ustar00rootroot00000000000000# Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """AsyncSSH version information""" __author__ = 'Ron Frederick' __author_email__ = 'ronf@timeheart.net' __url__ = 'http://asyncssh.timeheart.net' __version__ = '1.3.0' asyncssh-1.3.0/docs/000077500000000000000000000000001260630620200142735ustar00rootroot00000000000000asyncssh-1.3.0/docs/_templates/000077500000000000000000000000001260630620200164305ustar00rootroot00000000000000asyncssh-1.3.0/docs/_templates/sidebarbottom.html000066400000000000000000000007101260630620200221520ustar00rootroot00000000000000

Change Log

Contributing

API Documentation

Source on PyPI

Source on GitHub

Issue Tracker

Search

asyncssh-1.3.0/docs/_templates/sidebartop.html000066400000000000000000000001231260630620200214460ustar00rootroot00000000000000 AsyncSSH
Version {{version}}

asyncssh-1.3.0/docs/api.rst000066400000000000000000001052021260630620200155760ustar00rootroot00000000000000.. module:: asyncssh .. _API: API Documentation ***************** Overview ======== The AsyncSSH API is modeled after the new Python ``asyncio`` framework, with a :func:`create_connection` coroutine to create an SSH client and a :func:`create_server` coroutine to create an SSH server. Like the ``asyncio`` framework, these calls take a parameter of a factory which creates protocol objects to manage the connections once they are open. For AsyncSSH, :func:`create_connection` should be passed a ``client_factory`` which returns objects derived from :class:`SSHClient` and :func:`create_server` should be passed a ``server_factory`` which returns objects derived from :class:`SSHServer`. In addition, each connection will have an associated :class:`SSHClientConnection` or :class:`SSHServerConnection` object passed to the protocol objects which can be used to perform actions on the connection. For client connections, authentication can be performed by passing in a username and password or SSH keys as arguments to :func:`create_connection` or by implementing handler methods on the :class:`SSHClient` object which return credentials when the server requests them. If no credentials are provided, AsyncSSH automatically attempts to send the username of the local user and the keys found in their :file:`.ssh` subdirectory. A list of expected server host keys can also be specified, with AsyncSSH defaulting to looking for matching lines in the user's :file:`.ssh/known_hosts` file. For server connections, handlers can be implemented on the :class:`SSHServer` object to return which authentication methods are supported and to validate credentials provided by clients. Once an SSH client connection is established and authentication is successful, multiple simultaneous channels can be opened on it. This is accomplished calling methods such as :meth:`create_session() ` or :meth:`create_connection() ` on the :class:`SSHClientConnection` object. The client can also set up listeners on remote TCP ports by calling :meth:`create_server() `. All of these methods take ``session_factory`` arguments that return :class:`SSHClientSession` or :class:`SSHTCPSession` objects used to manage the channels once they are open. Alternately, channels can be opened using :meth:`open_session() ` or :meth:`open_connection() `, which return :class:`SSHReader` and :class:`SSHWriter` objects which can be used to perform I/O on the channel. The method :meth:`start_server() ` can be used to set up listeners on remote TCP ports and get back these :class:`SSHReader` and :class:`SSHWriter` objects in a callback when new connections are opened. The client can also set up TCP port forwarding by calling :meth:`forward_local_port() ` or :meth:`forward_remote_port() `. In these cases, data transfer on the channels is managed automatically by AsyncSSH whenever new connections are opened, so custom session objects are not required. When an SSH server receives a new connection and authentication is successful, handlers such as :meth:`session_requested() `, :meth:`connection_requested() `, and :meth:`server_requested() ` on the associated :class:`SSHServer` object will be called when clients attempt to open channels or set up listeners. These methods return coroutines which can set up the requested sessions or connections, returning :class:`SSHServerSession` or :class:`SSHTCPSession` objects or handler functions that accept :class:`SSHReader` and :class:`SSHWriter` objects as arguments which manage the channels once they are open. Each session object also has an associated :class:`SSHClientChannel`, :class:`SSHServerChannel`, or :class:`SSHTCPChannel` object passed to it which can be used to perform actions on the channel. These channel objects provide a superset of the functionality found in ``asyncio`` transport objects. In addition to the above functions and classes, helper functions for importing public and private keys can be found below under :ref:`PublicKeyFunctions`, exceptions can be found under :ref:`Exceptions`, supported algorithms can be found under :ref:`SupportedAlgorithms`, and some useful constants can be found under :ref:`Constants`. Main Functions ============== create_connection ----------------- .. autofunction:: create_connection create_server ------------- .. autofunction:: create_server connect ------- .. autofunction:: connect listen ------ .. autofunction:: listen Main Classes ============ SSHClient --------- .. autoclass:: SSHClient ================================== = General connection handlers ================================== = .. automethod:: connection_made .. automethod:: connection_lost .. automethod:: debug_msg_received ================================== = ==================================== = General authentication handlers ==================================== = .. automethod:: auth_banner_received .. automethod:: auth_completed ==================================== = ========================================= = Public key authentication handlers ========================================= = .. automethod:: public_key_auth_requested ========================================= = ========================================= = Password authentication handlers ========================================= = .. automethod:: password_auth_requested .. automethod:: password_change_requested .. automethod:: password_changed .. automethod:: password_change_failed ========================================= = ============================================ = Keyboard-interactive authentication handlers ============================================ = .. automethod:: kbdint_auth_requested .. automethod:: kbdint_challenge_received ============================================ = SSHServer --------- .. autoclass:: SSHServer ================================== = General connection handlers ================================== = .. automethod:: connection_made .. automethod:: connection_lost .. automethod:: debug_msg_received ================================== = =============================== = General authentication handlers =============================== = .. automethod:: begin_auth =============================== = ========================================= = Public key authentication handlers ========================================= = .. automethod:: public_key_auth_supported .. automethod:: validate_public_key .. automethod:: validate_ca_key ========================================= = ======================================= = Password authentication handlers ======================================= = .. automethod:: password_auth_supported .. automethod:: validate_password ======================================= = ============================================ = Keyboard-interactive authentication handlers ============================================ = .. automethod:: kbdint_auth_supported .. automethod:: get_kbdint_challenge .. automethod:: validate_kbdint_response ============================================ = ==================================== = Channel session open handlers ==================================== = .. automethod:: session_requested .. automethod:: connection_requested .. automethod:: server_requested ==================================== = Connection Classes ================== SSHClientConnection ------------------- .. autoclass:: SSHClientConnection() ============================== = General connection methods ============================== = .. automethod:: get_extra_info .. automethod:: send_debug ============================== = ================================= = Client session open methods ================================= = .. automethod:: create_session .. automethod:: open_session .. automethod:: create_connection .. automethod:: open_connection .. automethod:: create_server .. automethod:: start_server .. automethod:: start_sftp_client ================================= = =================================== = Client forwarding methods =================================== = .. automethod:: forward_connection .. automethod:: forward_local_port .. automethod:: forward_remote_port =================================== = ========================== = Connection close methods ========================== = .. automethod:: abort .. automethod:: close .. automethod:: disconnect ========================== = SSHServerConnection ------------------- .. autoclass:: SSHServerConnection() ============================== = General connection methods ============================== = .. automethod:: get_extra_info .. automethod:: send_debug ============================== = ============================================ = Server authentication methods ============================================ = .. automethod:: send_auth_banner .. automethod:: set_authorized_keys .. automethod:: get_key_option .. automethod:: check_key_permission .. automethod:: get_certificate_option .. automethod:: check_certificate_permission ============================================ = ================================== = Server connection open methods ================================== = .. automethod:: create_connection .. automethod:: open_connection ================================== = ================================== = Server forwarding methods ================================== = .. automethod:: forward_connection ================================== = ===================================== = Server channel creation methods ===================================== = .. automethod:: create_server_channel .. automethod:: create_tcp_channel ===================================== = ========================== = Connection close methods ========================== = .. automethod:: abort .. automethod:: close .. automethod:: disconnect ========================== = Session Classes =============== SSHClientSession ---------------- .. autoclass:: SSHClientSession =============================== = General session handlers =============================== = .. automethod:: connection_made .. automethod:: connection_lost .. automethod:: session_started =============================== = ============================= = General session read handlers ============================= = .. automethod:: data_received .. automethod:: eof_received ============================= = ============================== = General session write handlers ============================== = .. automethod:: pause_writing .. automethod:: resume_writing ============================== = ==================================== = Other client session handlers ==================================== = .. automethod:: xon_xoff_requested .. automethod:: exit_status_received .. automethod:: exit_signal_received ==================================== = SSHServerSession ---------------- .. autoclass:: SSHServerSession =============================== = General session handlers =============================== = .. automethod:: connection_made .. automethod:: connection_lost .. automethod:: session_started =============================== = =================================== = Server session open handlers =================================== = .. automethod:: pty_requested .. automethod:: shell_requested .. automethod:: exec_requested .. automethod:: subsystem_requested =================================== = ============================= = General session read handlers ============================= = .. automethod:: data_received .. automethod:: eof_received ============================= = ============================== = General session write handlers ============================== = .. automethod:: pause_writing .. automethod:: resume_writing ============================== = ===================================== = Other server session handlers ===================================== = .. automethod:: break_received .. automethod:: signal_received .. automethod:: terminal_size_changed ===================================== = SSHTCPSession ------------- .. autoclass:: SSHTCPSession =============================== = General session handlers =============================== = .. automethod:: connection_made .. automethod:: connection_lost .. automethod:: session_started =============================== = ============================= = General session read handlers ============================= = .. automethod:: data_received .. automethod:: eof_received ============================= = ============================== = General session write handlers ============================== = .. automethod:: pause_writing .. automethod:: resume_writing ============================== = Channel Classes =============== SSHClientChannel ---------------- .. autoclass:: SSHClientChannel() ============================== = General channel methods ============================== = .. automethod:: get_extra_info ============================== = ============================== = Client channel read methods ============================== = .. automethod:: pause_reading .. automethod:: resume_reading ============================== = ======================================= = Client channel write methods ======================================= = .. automethod:: can_write_eof .. automethod:: get_write_buffer_size .. automethod:: set_write_buffer_limits .. automethod:: write .. automethod:: writelines .. automethod:: write_eof ======================================= = ===================================== = Other client channel methods ===================================== = .. automethod:: get_exit_status .. automethod:: get_exit_signal .. automethod:: change_terminal_size .. automethod:: send_break .. automethod:: send_signal .. automethod:: kill .. automethod:: terminate ===================================== = ============================= = General channel close methods ============================= = .. automethod:: abort .. automethod:: close .. automethod:: wait_closed ============================= = SSHServerChannel ---------------- .. autoclass:: SSHServerChannel() ============================== = General channel methods ============================== = .. automethod:: get_extra_info ============================== = ================================= = Server channel info methods ================================= = .. automethod:: get_environment .. automethod:: get_command .. automethod:: get_subsystem .. automethod:: get_terminal_type .. automethod:: get_terminal_size .. automethod:: get_terminal_mode ================================= = ============================== = Server channel read methods ============================== = .. automethod:: pause_reading .. automethod:: resume_reading ============================== = ======================================= = Server channel write methods ======================================= = .. automethod:: can_write_eof .. automethod:: get_write_buffer_size .. automethod:: set_write_buffer_limits .. automethod:: write .. automethod:: writelines .. automethod:: write_stderr .. automethod:: writelines_stderr .. automethod:: write_eof ======================================= = ================================= = Other server channel methods ================================= = .. automethod:: start_sftp_server .. automethod:: set_xon_xoff .. automethod:: exit .. automethod:: exit_with_signal ================================= = ============================= = General channel close methods ============================= = .. automethod:: abort .. automethod:: close .. automethod:: wait_closed ============================= = SSHTCPChannel ------------- .. autoclass:: SSHTCPChannel() ============================== = General channel methods ============================== = .. automethod:: get_extra_info ============================== = ============================== = General channel read methods ============================== = .. automethod:: pause_reading .. automethod:: resume_reading ============================== = ======================================= = General channel write methods ======================================= = .. automethod:: can_write_eof .. automethod:: get_write_buffer_size .. automethod:: set_write_buffer_limits .. automethod:: write .. automethod:: writelines .. automethod:: write_eof ======================================= = ============================= = General channel close methods ============================= = .. automethod:: abort .. automethod:: close .. automethod:: wait_closed ============================= = Listener Classes ================ SSHListener ----------- .. autoclass:: SSHListener =========================== = .. automethod:: get_port .. automethod:: close .. automethod:: wait_closed =========================== = Stream Classes ============== SSHReader --------- .. autoclass:: SSHReader ============================== = .. autoattribute:: channel .. automethod:: get_extra_info .. automethod:: at_eof .. automethod:: read .. automethod:: readline .. automethod:: readexactly ============================== = SSHWriter --------- .. autoclass:: SSHWriter ============================== = .. autoattribute:: channel .. automethod:: get_extra_info .. automethod:: can_write_eof .. automethod:: close .. automethod:: drain .. automethod:: write .. automethod:: writelines .. automethod:: write_eof ============================== = SFTP Support ============ SFTPClient ---------- .. autoclass:: SFTPClient() ===================== = File transfer methods ===================== = .. automethod:: get .. automethod:: put .. automethod:: copy .. automethod:: mget .. automethod:: mput .. automethod:: mcopy ===================== = ========================================================================================== = File access methods ========================================================================================== = .. automethod:: open(path, mode='r', attrs=SFTPAttrs(), encoding='utf-8', errors='strict') .. automethod:: truncate .. automethod:: rename .. automethod:: posix_rename .. automethod:: remove .. automethod:: unlink .. automethod:: readlink .. automethod:: symlink .. automethod:: link .. automethod:: realpath ========================================================================================== = ============================= = File attribute access methods ============================= = .. automethod:: stat .. automethod:: lstat .. automethod:: setstat .. automethod:: statvfs .. automethod:: chown .. automethod:: chmod .. automethod:: utime .. automethod:: exists .. automethod:: lexists .. automethod:: getatime .. automethod:: getmtime .. automethod:: getsize .. automethod:: isdir .. automethod:: isfile .. automethod:: islink ============================= = ============================================== = Directory access methods ============================================== = .. automethod:: chdir .. automethod:: getcwd .. automethod:: mkdir(path, attrs=SFTPAttrs()) .. automethod:: rmdir .. automethod:: readdir .. automethod:: listdir .. automethod:: glob ============================================== = SFTPServer ---------- .. autoclass:: SFTPServer ================================== = Path remapping and display methods ================================== = .. automethod:: format_longname .. automethod:: map_path .. automethod:: reverse_map_path ================================== = ============================ = File access methods ============================ = .. automethod:: open .. automethod:: close .. automethod:: read .. automethod:: write .. automethod:: rename .. automethod:: posix_rename .. automethod:: remove .. automethod:: readlink .. automethod:: symlink .. automethod:: link .. automethod:: realpath ============================ = ============================= = File attribute access methods ============================= = .. automethod:: stat .. automethod:: lstat .. automethod:: fstat .. automethod:: setstat .. automethod:: fsetstat .. automethod:: statvfs .. automethod:: fstatvfs ============================= = ======================== = Directory access methods ======================== = .. automethod:: listdir .. automethod:: mkdir .. automethod:: rmdir ======================== = ===================== = Cleanup methods ===================== = .. automethod:: exit ===================== = SFTPFile -------- .. autoclass:: SFTPFile() ================================================ = .. automethod:: read .. automethod:: write .. automethod:: seek(offset, from_what=SEEK_SET) .. automethod:: tell .. automethod:: stat .. automethod:: setstat .. automethod:: statvfs .. automethod:: truncate .. automethod:: chown .. automethod:: chmod .. automethod:: utime .. automethod:: fsync .. automethod:: close ================================================ = SFTPAttrs --------- .. autoclass:: SFTPAttrs() SFTPVFSAttrs ------------ .. autoclass:: SFTPVFSAttrs() SFTPName -------- .. autoclass:: SFTPName() .. index:: Public key support .. _PublicKeyFunctions: Public Key Support ================== AsyncSSH has extensive public key and certificate support. Supported public key types include DSA, RSA, and ECDSA. In addition, ed25519 keys are supported if the libnacl package and libsodium library are installed. Supported certificate types include version 00 certificates for DSA and RSA keys and version 01 certificates for DSA, RSA, ECDSA, and ed25519 keys. Support is also available for the certificate critical options of force-command and source-address and the extensions permit-pty and permit-port-forwarding. Several public key and certificate formats are supported including PKCS#1 and PKCS#8 DER and PEM, OpenSSH, and RFC4716 formats. PEM and PKCS#8 password-based encryption of private keys is supported, as is OpenSSH private key encryption when the bcrypt package is installed. .. index:: Specifying private keys .. _SpecifyingPrivateKeys: Specifying private keys ----------------------- Private keys may be passed into AsyncSSH in a variety of forms. The simplest option is to pass the name of a file containing the list of private keys to read in using :func:`read_private_key_list`. However, this form can only be used for unencrypted private keys and does not allow any of the private keys to have associated certificates. An alternate form involves passing in a list of values which can be either a reference to a private key or a tuple containing a reference to a private key and a reference to a matching certificate. Key references can either be the name of a file to load a key from, a byte string to import it from, or an already loaded :class:`SSHKey` private key. See the function :func:`import_private_key` for the list of supported private key formats. Certificate references can be the name of a file to load the certificate from, a byte string to import it from, an already loaded :class:`SSHCertificate`, or ``None`` if no certificate should be associated with the key. When a filename is provided as a value in the list, an attempt is made to load a private key from that file and a certificate from a file constructed by appending '-cert.pub' to the end of the name. Encrypted private keys can be loaded by making an explicit call to :func:`import_private_key` or :func:`read_private_key` with the correct passphrase. The resulting :class:`SSHKey` objects can then be included in thie list, each with an optional matching certificate. .. index:: Specifying private keys .. _SpecifyingPublicKeys: Specifying public keys ---------------------- Public keys may be passed into AsyncSSH in a variety of forms. The simplest option is to pass the name of a file containing the list of public keys to read in using :func:`read_public_key_list`. An alternate form involves passing in a list of values each of which can be either the name of a file to load a key from, a byte string to import it from, or an already loaded :class:`SSHKey` public key. See the function :func:`import_public_key` for the list of supported public key formats. SSHKey ------ .. autoclass:: SSHKey() ================================== = .. automethod:: export_private_key .. automethod:: export_public_key .. automethod:: write_private_key .. automethod:: write_public_key ================================== = SSHCertificate -------------- .. autoclass:: SSHCertificate() .. automethod:: validate import_private_key ------------------ .. autofunction:: import_private_key import_public_key ----------------- .. autofunction:: import_public_key import_certificate ------------------ .. autofunction:: import_certificate read_private_key ---------------- .. autofunction:: read_private_key read_public_key --------------- .. autofunction:: read_public_key read_certificate ---------------- .. autofunction:: read_certificate read_private_key_list --------------------- .. autofunction:: read_private_key_list read_public_key_list -------------------- .. autofunction:: read_public_key_list read_certificate_list --------------------- .. autofunction:: read_certificate_list .. index:: Exceptions .. _Exceptions: Known hosts =========== .. index:: Specifying known hosts .. _SpecifyingKnownHosts: Specifying known hosts ---------------------- Known hosts may be passed into AsyncSSH via the ``known_hosts`` argument to :func:`create_connection`. This value can be provided in a couple of different forms. The simplest option is to pass the name of a file containing a list of known hosts in OpenSSH known hosts format. AsyncSSH supports both plain and hashed host entries and both regular and negated host patterns in plain entries. It also supports the ``@cert-authority`` and ``@revoked`` markers on entries. Alternately, known hosts can be passed into AsyncSSH as a sequence of three public key lists containing trusted host keys, trusted CA keys, and revoked keys which should no longer be trusted. See :ref:`SpecifyingPublicKeys` for the allowed form of each of these values. Authorized keys =============== AsyncSSH supports OpenSSH-style authorized_keys files, including the cert-authority option to validate user certificates, enforcement of from and principals options to restrict key matching, enforcement of no-pty, no-port-forwarding, and permitopen options, and support for command and environment options. .. index:: Specifying authorized keys .. _SpecifyingAuthorizedKeys: Specifying authorized keys -------------------------- Authorized keys may be passed into AsyncSSH via the ``authorized_client_keys`` argument to :func:`create_server` or by calling :meth:`set_authorized_keys() ` on the :class:`SSHServerConnection` from within the :meth:`begin_auth() ` method in :class:`SSHServer`. Authorized keys can be provided as either the name of a file to read the keys from or an :class:`SSHAuthorizedKeys` object which was previously imported from a string by calling :func:`import_authorized_keys` or read from a file by calling :func:`read_authorized_keys`. SSHAuthorizedKeys ----------------- .. autoclass:: SSHAuthorizedKeys() import_authorized_keys ---------------------- .. autofunction:: import_authorized_keys read_authorized_keys -------------------- .. autofunction:: read_authorized_keys Exceptions ========== BreakReceived ------------- .. autoexception:: BreakReceived SignalReceived -------------- .. autoexception:: SignalReceived TerminalSizeChanged ------------------- .. autoexception:: TerminalSizeChanged DisconnectError --------------- .. autoexception:: DisconnectError ChannelOpenError ---------------- .. autoexception:: ChannelOpenError SFTPError --------- .. autoexception:: SFTPError KeyImportError -------------- .. autoexception:: KeyImportError KeyExportError -------------- .. autoexception:: KeyExportError KeyEncryptionError ------------------ .. autoexception:: KeyEncryptionError .. index:: Supported algorithms .. _SupportedAlgorithms: Supported Algorithms ==================== .. index:: Key exchange algorithms .. _KexAlgs: Key exchange algorithms ----------------------- The following are the key exchange algorithms currently supported by AsyncSSH: | curve25519-sha256\@libssh.org | ecdh-sha2-nistp521 | ecdh-sha2-nistp384 | ecdh-sha2-nistp256 | diffie-hellman-group-exchange-sha256 | diffie-hellman-group-exchange-sha1 | diffie-hellman-group14-sha1 | diffie-hellman-group1-sha1 Curve25519 support is only available when the libnacl package and libsodium library are installed. .. index:: Encryption algorithms .. _EncryptionAlgs: Encryption algorithms --------------------- The following are the encryption algorithms currently supported by AsyncSSH: | chacha20-poly1305\@openssh.com | aes256-ctr | aes192-ctr | aes128-ctr | aes256-gcm\@openssh.com | aes128-gcm\@openssh.com | aes256-cbc | aes192-cbc | aes128-cbc | 3des-cbc | blowfish-cbc | cast128-cbc | arcfour256 | arcfour128 | arcfour Chacha20-poly1305 support is only available when the libnacl package and libsodium library are installed. .. index:: MAC algorithms .. _MACAlgs: MAC algorithms -------------- The following are the MAC algorithms currently supported by AsyncSSH: | hmac-sha2-256-etm\@openssh.com | hmac-sha2-512-etm\@openssh.com | hmac-sha1-etm\@openssh.com | hmac-md5-etm\@openssh.com | hmac-sha2-256-96-etm\@openssh.com | hmac-sha2-512-96-etm\@openssh.com | hmac-sha1-96-etm\@openssh.com | hmac-md5-96-etm\@openssh.com | hmac-sha2-256 | hmac-sha2-512 | hmac-sha1 | hmac-md5 | hmac-sha2-256-96 | hmac-sha2-512-96 | hmac-sha1-96 | hmac-md5-96 .. index:: Compression algorithms .. _CompressionAlgs: Compression algorithms ---------------------- The following are the compression algorithms currently supported by AsyncSSH: | zlib\@openssh.com | zlib | none .. index:: Public key & certificate algorithms .. _PublicKeyAlgs: Public key & certificate algorithms ----------------------------------- The following are the public key and certificate algorithms currently supported by AsyncSSH: | ssh-ed25519-cert-v01\@openssh.com | ecdsa-sha2-nistp521-cert-v01\@openssh.com | ecdsa-sha2-nistp384-cert-v01\@openssh.com | ecdsa-sha2-nistp256-cert-v01\@openssh.com | ssh-rsa-cert-v01\@openssh.com | ssh-rsa-cert-v00\@openssh.com | ssh-dss-cert-v01\@openssh.com | ssh-dss-cert-v00\@openssh.com | ssh-ed25519 | ecdsa-sha2-nistp521 | ecdsa-sha2-nistp384 | ecdsa-sha2-nistp256 | ssh-rsa | ssh-dss Ed25519 support is only available when the libnacl package and libsodium library are installed. .. index:: Constants .. _Constants: Constants ========= .. index:: Certificate types .. _CertificateTypes: Certificate types ----------------- The following values can be specified as certificate types: | CERT_TYPE_USER | CERT_TYPE_HOST .. index:: Disconnect reasons .. _DisconnectReasons: Disconnect reasons ------------------ The following values defined in section 11.1 of :rfc:`4253#section-11.1` can be specified as disconnect reason codes: | DISC_HOST_NOT_ALLOWED_TO_CONNECT | DISC_PROTOCOL_ERROR | DISC_KEY_EXCHANGE_FAILED | DISC_RESERVED | DISC_MAC_ERROR | DISC_COMPRESSION_ERROR | DISC_SERVICE_NOT_AVAILABLE | DISC_PROTOCOL_VERSION_NOT_SUPPORTED | DISC_HOST_KEY_NOT_VERIFYABLE | DISC_CONNECTION_LOST | DISC_BY_APPLICATION | DISC_TOO_MANY_CONNECTIONS | DISC_AUTH_CANCELLED_BY_USER | DISC_NO_MORE_AUTH_METHODS_AVAILABLE | DISC_ILLEGAL_USER_NAME .. index:: Channel open failure reasons .. _ChannelOpenFailureReasons: Channel open failure reasons ---------------------------- The following values defined in section 5.1 of :rfc:`4254#section-5.1` can be specified as channel open failure reason codes: | OPEN_ADMINISTRATIVELY_PROHIBITED | OPEN_CONNECT_FAILED | OPEN_UNKNOWN_CHANNEL_TYPE | OPEN_RESOURCE_SHORTAGE | OPEN_REQUEST_PTY_FAILED | OPEN_REQUEST_SESSION_FAILED .. index:: SFTP error codes .. _SFTPErrorCodes: SFTP error codes ---------------- The following values defined in the `SSH File Transfer Internet Draft `_ can be specified as SFTP error codes: | FX_OK | FX_EOF | FX_NO_SUCH_FILE | FX_PERMISSION_DENIED | FX_FAILURE | FX_BAD_MESSAGE | FX_NO_CONNECTION | FX_CONNECTION_LOST | FX_OP_UNSUPPORTED .. index:: Extended data types .. _ExtendedDataTypes: Extended data types ------------------- The following values defined in section 5.2 of :rfc:`4254#section-5.2` can be specified as SSH extended channel data types: | EXTENDED_DATA_STDERR .. index:: POSIX terminal modes .. _PTYModes: POSIX terminal modes -------------------- The following values defined in section 8 of :rfc:`4254#section-8` can be specified as PTY mode opcodes: | PTY_OP_END | PTY_VINTR | PTY_VQUIT | PTY_VERASE | PTY_VKILL | PTY_VEOF | PTY_VEOL | PTY_VEOL2 | PTY_VSTART | PTY_VSTOP | PTY_VSUSP | PTY_VDSUSP | PTY_VREPRINT | PTY_WERASE | PTY_VLNEXT | PTY_VFLUSH | PTY_VSWTCH | PTY_VSTATUS | PTY_VDISCARD | PTY_IGNPAR | PTY_PARMRK | PTY_INPCK | PTY_ISTRIP | PTY_INLCR | PTY_IGNCR | PTY_ICRNL | PTY_IUCLC | PTY_IXON | PTY_IXANY | PTY_IXOFF | PTY_IMAXBEL | PTY_ISIG | PTY_ICANON | PTY_XCASE | PTY_ECHO | PTY_ECHOE | PTY_ECHOK | PTY_ECHONL | PTY_NOFLSH | PTY_TOSTOP | PTY_IEXTEN | PTY_ECHOCTL | PTY_ECHOKE | PTY_PENDIN | PTY_OPOST | PTY_OLCUC | PTY_ONLCR | PTY_OCRNL | PTY_ONOCR | PTY_ONLRET | PTY_CS7 | PTY_CS8 | PTY_PARENB | PTY_PARODD | PTY_OP_ISPEED | PTY_OP_OSPEED asyncssh-1.3.0/docs/changes.rst000066400000000000000000000457761260630620200164600ustar00rootroot00000000000000.. currentmodule:: asyncssh Change Log ========== Release 1.3.0 (10 Oct 2015) --------------------------- * Updated AsyncSSH dependencies to make PyCA version 1.0.0 or later mandatory and remove the older PyCrypto support. This change also adds support for the PyCA implementation of ECDSA and removes support for RC2-based private key encryption that was only supported by PyCrypto. * Refactored ECDH and Curve25519 key exchange code so they can share an implementation, and prepared the code for adding a PyCA shim for this as soon as support for that is released. * Hardened the DSA and RSA implementations to do stricter checking of the key exchange response, and sped up the RSA implementation by taking advantage of optional RSA private key parameters when they are present. * Added support for asynchronous client and server authentication, allowing auth-related callbacks in SSHClient and SSHServer to optionally be defined as coroutines. * Added support for asynchronous SFTP server processing, allowing callbacks in SFTPServer to optionally be defined as coroutines. * Added support for a broader set of open mode flags in the SFTP server. Note that this change is not completely backward compatible with previous releases. If you have application code which expects a Python mode string as an argument to SFTPServer open method, it will need to be changed to expect a pflags value instead. * Fixed a bug related to disabling public key auth in SSHClient. Passing client_keys=None when opening a client connection should now properly disable the use of public key authentication. * Fixed handling of eof_received() when it returns false to close the half-open connection but still allow sending or receiving of exit status and exit signals. * Added unit tests for the asn1, cipher, compression, ec, kex, known_hosts, mac, and saslprep modules and expended the set of pbe and public_key unit tests. * Fixed a set of issues uncovered by ASN.1 unit tests: * Removed extra 0xff byte when encoding integers of the form -128*256^n * Fixed decoding error for OIDs beginning with 2.n where n >= 40 * Fixed range check for second component of ObjectIdentifier * Added check for extraneous 0x80 bytes in ObjectIdentifier components * Added check for negative component values in ObjectIdentifier * Added error handling for ObjectIdentifier components being non-integer * Added handling for missing length byte after extended tag * Raised ASN1EncodeError instead of TypeError on unsupported types * Added validation on asn1_class argument, and equality and hash methods to BitString, RawDERObject, and TaggedDERObject. Also, reordered RawDERObject arguments to be consistent with TaggedDERObject and added str method to ObjectIdentifier. * Fixed a set of issues uncovered by additional pbe unit tests: * Encoding and decoding of PBES2-encrypted keys with a PRF other than SHA1 is now handled correctly. * Some exception messages were made more specific. * Additional checks were put in for empty salt or zero iteration count in encryption parameters. * Fixed a set of issues uncovered by additional public key unit tests: * Properly handle PKCS#8 keys with invalid ASN.1 data * Properly handle PKCS#8 DSA & RSA keys with non-sequence for arg_params * Properly handle attempts to import empty string as a public key * Properly handle encrypted PEM keys with missing DEK-Info header * Report check byte mismatches for encrypted OpenSSH keys as bad passphrase * Return KeyImportError instead of KeyEncryptionError when passphrase is needed but not provided * Added information about branches to CONTRIBUTING guide. * Performed a bunch of code cleanup suggested by pylint. Release 1.2.1 (26 Aug 2015) --------------------------- * Fixed a problem with passing in client_keys=None to disable public key authentication in the SSH client. * Updated Unicode handling to allow multi-byte Unicode characters to be split across successive SSH data messages. * Added a note to the documentation for AsyncSSH create_connection() explaining how to perform the equivalent of a connect with a timeout. Release 1.2.0 (6 Jun 2015) -------------------------- * Fixed a problem with the SSHConnection context manager on Python versions older than 3.4.2. * Updated the documentation for get_extra_info() in the SSHConnection, SSHChannel, SSHReader, and SSHWriter classes to contain pointers to get_extra_info() in their parent transports to make it easier to see all of the attributes which can be queried. * Clarified the legal return values for the session_requested(), connection_requested(), and server_requested() methods in SSHServer. * Eliminated calls to the deprecated importlib.find_loader() method. * Made improvements to README suggested by Nicholas Chammas. * Fixed a number of issues identified by pylint. Release 1.1.1 (25 May 2015) --------------------------- * Added new start_sftp_server method on SSHChannel to allow applications using the non-streams API to start an SFTP server. * Enhanced the default format_longname() method in SFTPServer to properly handle the case where not all of the file attributes are returned by stat(). * Fixed a bug related to the new allow_pty parameter in create_server. * Fixed a bug in the hashed known_hosts support introduced in some recent refactoring of the host pattern matching code. Release 1.1.0 (22 May 2015) --------------------------- * SFTP is now supported! * Both client and server support is available. * SFTP version 3 is supported, with OpenSSH extensions. * Recursive transfers and glob matching are supported in the client. * File I/O APIs allow files to be accessed without downloading them. * New simplified connect and listen APIs have been added. * SSHConnection can now be used as a context manager. * New arguments to create_server now allow the specification of a session_factory and encoding or sftp_factory as well as controls over whether a pty is allowed and the window and max packet size, avoiding the need to create custom SSHServer subclasses or custom SSHServerChannel instances. * New examples have been added for SFTP and to show the use of the new connect and listen APIs. * Copyrights in changed files have all been updated to 2015. Release 1.0.1 (13 Apr 2015) --------------------------- * Fixed a bug in OpenSSH private key encryption introduced in some recent cipher refactoring. * Added bcrypt and libnacl as optional dependencies in setup.py. * Changed test_keys test to work properly when bcrypt or libnacl aren't installed. Release 1.0.0 (11 Apr 2015) --------------------------- * This release finishes adding a number of major features, finally making it worthy of being called a "1.0" release. * Host and user certificates are now supported! * Enforcement is done on principals in certificates. * Enforcement is done on force-command and source-address critical options. * Enforcement is done on permit-pty and permit-port-forwarding extensions. * OpenSSH-style known hosts files are now supported! * Positive and negative wildcard and CIDR-style patterns are supported. * HMAC-SHA1 hashed host entries are supported. * The @cert-authority and @revoked markers are supported. * OpenSSH-style authorized keys files are now supported! * Both client keys and certificate authorities are supported. * Enforcement is done on from and principals options during key matching. * Enforcement is done on no-pty, no-port-forwarding, and permitopen. * The command and environment options are supported. * Applications can query for their own non-standard options. * Support has been added for OpenSSH format private keys. * DSA, RSA, and ECDSA keys in this format are now supported. * Ed25519 keys are supported when libnacl and libsodium are installed. * OpenSSH private key encryption is supported when bcrypt is installed. * Curve25519 Diffie-Hellman key exchange is now available via either the curve25519-donna or libnacl and libsodium packages. * ECDSA key support has been enhanced. * Support is now available for PKCS#8 ECDSA v2 keys. * Support is now available for both NamedCurve and explicit ECParameter versions of keys, as long as the parameters match one of the supported curves (nistp256, nistp384, or nistp521). * Support is now available for the OpenSSH chacha20-poly1305 cipher when libnacl and libsodium are installed. * Cipher names specified in private key encryption have been changed to be consistent with OpenSSH cipher naming, and all SSH ciphers can now be used for encryption of keys in OpenSSH private key format. * A couple of race conditions in SSHChannel have been fixed and channel cleanup is now delayed to allow outstanding message handling to finish. * Channel exceptions are now properly delivered in the streams API. * A bug in SSHStream read() where it could sometimes return more data than requested has been fixed. Also, read() has been changed to properly block and return all data until EOF or a signal is received when it is called with no length. * A bug in the default implementation of keyboard-interactive authentication has been fixed, and the matching of a password prompt has been loosened to allow it to be used for password authentication on more devices. * Missing code to resume reading after a stream is paused has been added. * Improvements have been made in the handling of canceled requests. * The test code has been updated to test Ed25519 and OpenSSH format private keys. * Examples have been updated to reflect some of the new capabilities. Release 0.9.2 (26 Jan 2015) --------------------------- * Fixed a bug in PyCrypto CipherFactory introduced during PyCA refactoring. Release 0.9.1 (3 Dec 2014) -------------------------- * Added some missing items in setup.py and MANIFEST.in. * Fixed the install to work even when cryptographic dependencies aren't yet installed. * Fixed an issue where get_extra_info calls could fail if called when a connection or session was shutting down. Release 0.9.0 (14 Nov 2014) --------------------------- * Added support to use PyCA (0.6.1 or later) for cryptography. AsyncSSH will automatically detect and use either PyCA, PyCrypto, or both depending on which is installed and which algorithms are requested. * Added support for AES-GCM ciphers when PyCA is installed. Release 0.8.4 (12 Sep 2014) --------------------------- * Fixed an error in the encode/decode functions for PKCS#1 DSA public keys. * Fixed a bug in the unit test code for import/export of RFC4716 public keys. Release 0.8.3 (16 Aug 2014) --------------------------- * Added a missing import in the curve25519 implementation. Release 0.8.2 (16 Aug 2014) --------------------------- * Provided a better long description for PyPI. * Added link to PyPI in documentation sidebar. Release 0.8.1 (15 Aug 2014) --------------------------- * Added a note in the :meth:`validate_public_key() ` documentation clarifying that AsyncSSH will verify that the client possesses the corresponding private key before authentication is allowed to succeed. * Switched from setuptools to distutils and added an initial set of unit tests. * Prepared the package to be uploaded to PyPI. Release 0.8.0 (15 Jul 2014) --------------------------- * Added support for Curve25519 Diffie Hellman key exchange on systems with the curve25519-donna Python package installed. * Updated the examples to more clearly show what values are returned even when not all of the return values are used. Release 0.7.0 (7 Jun 2014) -------------------------- * This release adds support for the "high-level" ``asyncio`` streams API, in the form of the :class:`SSHReader` and :class:`SSHWriter` classes and wrapper methods such as :meth:`open_session() `, :meth:`open_connection() `, and :meth:`start_server() `. It also allows the callback methods on :class:`SSHServer` to return either SSH session objects or handler functions that take :class:`SSHReader` and :class:`SSHWriter` objects as arguments. See :meth:`session_requested() `, :meth:`connection_requested() `, and :meth:`server_requested() ` for more information. * Added new exceptions :exc:`BreakReceived`, :exc:`SignalReceived`, and :exc:`TerminalSizeChanged` to report when these messages are received while trying to read from an :class:`SSHServerChannel` using the new streams API. * Changed :meth:`create_server() ` to accept either a callable or a coroutine for its ``session_factory`` argument, to allow asynchronous operations to be used when deciding whether to accept a forwarded TCP connection. * Renamed ``accept_connection()`` to :meth:`create_connection() ` in the :class:`SSHServerConnection` class for consistency with :class:`SSHClientConnection`, and added a corresponding :meth:`open_connection() ` method as part of the streams API. * Added :meth:`get_exit_status() ` and :meth:`get_exit_signal() ` methods to the :class:`SSHClientChannel` class. * Added :meth:`get_command() ` and :meth:`get_subsystem() ` methods to the :class:`SSHServerChannel` class. * Fixed the name of the :meth:`write_stderr() ` method and added the missing :meth:`writelines_stderr() ` method to the :class:`SSHServerChannel` class for outputting data to the stderr channel. * Added support for a return value in the :meth:`eof_received() ` of :class:`SSHClientSession`, :class:`SSHServerSession`, and :class:`SSHTCPSession` to support half-open channels. By default, the channel is automatically closed after :meth:`eof_received() ` returns, but returning ``True`` will now keep the channel open, allowing output to still be sent on the half-open channel. This is done automatically when the new streams API is used. * Added values ``'local_peername'`` and ``'remote_peername'`` to the set of information available from the :meth:`get_extra_info() ` method in the :class:`SSHTCPChannel` class. * Updated functions returning :exc:`IOError` or :exc:`socket.error` to return the new :exc:`OSError` exception introduced in Python 3.3. * Cleaned up some errors in the documentation. * The :ref:`API`, :ref:`ClientExamples`, and :ref:`ServerExamples` have all been updated to reflect these changes, and new examples showing the streams API have been added. Release 0.6.0 (11 May 2014) --------------------------- * This release is a major revamp of the code to migrate from the ``asyncore`` framework to the new ``asyncio`` framework in Python 3.4. All the APIs have been adapted to fit the new ``asyncio`` paradigm, using coroutines wherever possible to avoid the need for callbacks when performing asynchronous operations. So far, this release only supports the "low-level" ``asyncio`` API. * The :ref:`API`, :ref:`ClientExamples`, and :ref:`ServerExamples` have all been updated to reflect these changes. Release 0.5.0 (11 Oct 2013) --------------------------- * Added the following new classes to support fully asynchronous connection forwarding, replacing the methods previously added in release 0.2.0: * :class:`SSHClientListener` * :class:`SSHServerListener` * :class:`SSHClientLocalPortForwarder` * :class:`SSHClientRemotePortForwarder` * :class:`SSHServerPortForwarder` These new classes allow for DNS lookups and other operations to be performed fully asynchronously when new listeners are set up. As with the asynchronous connect changes below, methods are now available to report when the listener is opened or when an error occurs during the open rather than requiring the listener to be fully set up in a single call. * Updated examples in :ref:`ClientExamples` and :ref:`ServerExamples` to reflect the above changes. Release 0.4.0 (28 Sep 2013) --------------------------- * Added support in :class:`SSHTCPConnection` for the following methods to allow asynchronous operations to be used when accepting inbound connection requests: * :meth:`handle_open_request() ` * :meth:`report_open() ` * :meth:`report_open_error() ` These new methods are used to implement asynchronous connect support for local and remote port forwarding, and to support trying multiple destination addresses when connection failures occur. * Cleaned up a few minor documentation errors. Release 0.3.0 (26 Sep 2013) --------------------------- * Added support in :class:`SSHClient` and :class:`SSHServer` for setting the key exchange, encryption, MAC, and compression algorithms allowed in the SSH handshake. * Refactored the algorithm selection code to pull a common matching function back into ``_SSHConnection`` and simplify other modules. * Extended the listener class to open multiple listening sockets when necessary, fixing a bug where sockets opened to listen on ``localhost`` were not properly accepting both IPv4 and IPv6 connections. Now, any listen request which resolves to multiple addresses will open listening sockets for each address. * Fixed a bug related to tracking of listeners opened on dynamic ports. Release 0.2.0 (21 Sep 2013) --------------------------- * Added support in :class:`SSHClient` for the following methods related to performing standard SSH port forwarding: * :meth:`forward_local_port() ` * :meth:`cancel_local_port_forwarding() ` * :meth:`forward_remote_port() ` * :meth:`cancel_remote_port_forwarding() ` * :meth:`handle_remote_port_forwarding() ` * :meth:`handle_remote_port_forwarding_error() ` * Added support in :class:`SSHServer` for new return values in :meth:`handle_direct_connection() ` and :meth:`handle_listen() ` to activate standard SSH server-side port forwarding. * Added a client_addr argument and member variable to :class:`SSHServer` to hold the client's address information. * Added and updated examples related to port forwarding and using :class:`SSHTCPConnection` to open direct and forwarded TCP connections in :ref:`ClientExamples` and :ref:`ServerExamples`. * Cleaned up some of the other documentation. * Removed a debug print statement accidentally left in related to SSH rekeying. Release 0.1.0 (14 Sep 2013) --------------------------- * Initial release asyncssh-1.3.0/docs/conf.py000066400000000000000000000173071260630620200156020ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # AsyncSSH documentation build configuration file, created by # sphinx-quickstart on Sun Sep 1 17:36:31 2013. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('..')) from asyncssh import __author__, __version__ # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'AsyncSSH' copyright = '2013-2015, ' + __author__ # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The full version, including alpha/beta/rc tags. release = __version__ # The short X.Y version. version = '.'.join(release.split('.')[:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'rftheme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { "sidebarwidth": 305, "stickysidebar": "true" } # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ['.'] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = {'**': ['sidebartop.html', 'localtoc.html', 'sidebarbottom.html']} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. html_domain_indices = False # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'AsyncSSHdoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} asyncssh-1.3.0/docs/contributing.rst000066400000000000000000000000411260630620200175270ustar00rootroot00000000000000.. include:: ../CONTRIBUTING.rst asyncssh-1.3.0/docs/index.rst000066400000000000000000000375601260630620200161470ustar00rootroot00000000000000.. toctree:: :hidden: changes contributing api .. currentmodule:: asyncssh .. include:: ../README.rst .. _ClientExamples: Client Examples =============== Simple client ------------- The following code shows an example of a simple SSH client which logs into localhost and lists files in a directory named 'abc' under the user's home directory. The username provided is the logged in user, and the user's default SSH client keys or certificates are presented during authentication. The server's host key is checked against the user's SSH known_hosts file and the connection will fail if there's no entry for localhost there or if the key doesn't match. .. include:: ../examples/sample_client.py :literal: :start-line: 14 To check against a different set of server host keys, they can be read and provided in the known_hosts argument when the :class:`SSHClient` instance is created: .. code:: conn, client = yield from asyncssh.create_connection(MySSHClient, 'localhost', known_hosts='my_known_hosts') Server host key checking can be disabled by setting the known_hosts argument to ``None``, but that's not recommended as it makes the connection vulnerable to a man-in-the-middle attack. To log in as a different remote user, the username argument can be provided: .. code:: conn, client = yield from asyncssh.create_connection(MySSHClient, 'localhost', username='user123') To use a different set of client keys for authentication, they can be read and provided in the client_keys argument: .. code:: conn, client = yield from asyncssh.create_connection(MySSHClient, 'localhost', client_keys=['my_ssh_key']) Password authentication can be used by providing a password argument: .. code:: conn, client = yield from asyncssh.create_connection(MySSHClient, 'localhost', password='secretpw') Any of the arguments above can be combined together as needed. If client keys and a password are both provided, either may be used depending on what forms of authentication the server supports and whether the authentication with them is successful. In cases where you don't need to customize callbacks on the SSHClient class, this code can be simplified somewhat to: .. include:: ../examples/simple_client.py :literal: :start-line: 14 Handling of stderr ------------------ The above code doesn't distinguish output going to stdout vs. stderr, but that's easy to do with the following change: .. include:: ../examples/stderr_client.py :literal: :start-line: 14 Simple client with input ------------------------ The following example demonstrates sending input to a remote program. It executes the calculator program ``bc`` and performs some basic math calculations. .. include:: ../examples/math_client.py :literal: :start-line: 14 Note that input is not sent on the channel until the :meth:`session_started() ` method is called, and :meth:`write_eof() ` is used to signal the end of input, causing the 'bc' program to exit. This example can be simplified by using the higher-level "streams" API. With that, callbacks aren't needed. Here's the streams version of the above example, using :meth:`open_session ` instead of :meth:`create_session `: .. include:: ../examples/stream_math_client.py :literal: :start-line: 14 When run, this program should produce the following output: .. code:: 2+2 = 4 1*2*3*4 = 24 2^32 = 4294967296 Checking exit status -------------------- The following example is a variation of the simple client which shows how to receive the remote program's exit status using the :meth:`exit_status_received ` callback. .. include:: ../examples/check_exit_status.py :literal: :start-line: 14 From servers that support it, exit signals can also be received using :meth:`exit_signal_received `. Exit status can be also queried after the channel has closed by using the methods :meth:`get_exit_status ` and :meth:`get_exit_signal `. This is how it is done when using the streams API, since callbacks aren't available there. Setting environment variables ----------------------------- The following example demonstrates setting environment variables for the remote session and displaying them by executing the 'env' command. .. include:: ../examples/set_environment.py :literal: :start-line: 14 Any number of environment variables can be passed in the dictionary given to :meth:`create_session() `. Note that SSH servers may restrict which environment variables (if any) are accepted, so this feature may require setting options on the SSH server before it will work. Setting terminal information ---------------------------- The following example demonstrates setting the terminal type and size passed to the remote session. .. include:: ../examples/set_terminal.py :literal: :start-line: 14 Note that this will cause AsyncSSH to request a pseudo-tty from the server. When a pseudo-tty is used, the server will no longer send output going to stderr with a different data type. Instead, it will be mixed with output going to stdout (unless it is redirected elsewhere by the remote command). Port forwarding --------------- The following example demonstrates the client setting up a local TCP listener on port 8080 and requesting that connections which arrive on that port be forwarded across SSH to the server and on to port 80 on ``www.google.com``: .. include:: ../examples/local_forwarding_client.py :literal: :start-line: 14 To listen on a dynamically assigned port, the client can pass in ``0`` as the listening port. If the listener is successfully opened, the selected port will be available via the :meth:`get_port() ` method on the returned listener object: .. include:: ../examples/local_forwarding_client2.py :literal: :start-line: 14 The client can also request remote port forwarding from the server. The following example shows the client requesting that the server listen on port 8080 and that connections arriving there be forwarded across SSH and on to port 80 on ``localhost``: .. include:: ../examples/remote_forwarding_client.py :literal: :start-line: 14 To limit which connections are accepted or dynamically select where to forward traffic to, the client can implement their own session factory and call :meth:`forward_connection() ` on the connections they wish to forward and raise an error on those they wish to reject: .. include:: ../examples/remote_forwarding_client2.py :literal: :start-line: 14 Just as with local listeners, the client can request remote port forwarding from a dynamic port by passing in ``0`` as the listening port and then call :meth:`get_port() ` on the returned listener to determine which port was selected. Direct TCP connections ---------------------- The client can also ask the server to open a TCP connection and directly send and receive data on it by using the :meth:`create_connection() ` method on the :class:`SSHClientConnection` object. In this example, a connection is attempted to port 80 on ``www.google.com`` and an HTTP HEAD request is sent for the document root. Note that unlike sessions created with :meth:`create_session() `, the I/O on these connections defaults to sending and receiving bytes rather than strings, allowing arbitrary binary data to be exchanged. However, this can be changed by setting the encoding to use when the connection is created. .. include:: ../examples/direct_client.py :literal: :start-line: 14 To use the streams API to open a direct connection, you can use :meth:`open_connection ` instead of :meth:`create_connection `: .. include:: ../examples/stream_direct_client.py :literal: :start-line: 14 Forwarded TCP connections ------------------------- The client can also directly process data from incoming TCP connections received on the server. The following example demonstrates the client requesting that the server listen on port 8888 and forward any received connections back to it over SSH. It then has a simple handler which echoes any data it receives back to the sender. As in the direct TCP connection example above, the default would be to send and receive bytes on this connection rather than strings, but here we set the encoding explicitly so all data is sent and received as strings: .. include:: ../examples/listening_client.py :literal: :start-line: 14 To use the streams API to open a listening connection, you can use :meth:`start_server ` instead of :meth:`create_server `: .. include:: ../examples/stream_listening_client.py :literal: :start-line: 14 SFTP client ----------- AsyncSSH also provides SFTP support. The following code shows an example of starting an SFTP client and requestng the download of a file: .. include:: ../examples/sftp_client.py :literal: :start-line: 14 To recursively download a directory, preserving access and modification times and permissions on the files, the preserve and recurse arguments can be included: .. code:: yield from sftp.get('example_dir', preserve=True, recurse=True) Wild card pattern matching is supported by the :meth:`mget `, :meth:`mput `, and :meth:`mcopy ` methods. The following downloads all files with extension "txt": .. code:: yield from sftp.mget('*.txt') See the :class:`SFTPClient` documentation for the full list of available actions. .. _ServerExamples: Server Examples =============== Simple server ------------- The following code shows an example of a simple SSH server which listens for connections on port 8022, does password authentication, and prints a message when users authenticate successfully and start a shell. .. include:: ../examples/simple_server.py :literal: :start-line: 14 To authenticate with SSH client keys or certificates, the server would look something like the following. Client and certificate authority keys for each user need to be placed in a file in authorized_keys format named based on the username in a directory called ``authorized_keys``. .. include:: ../examples/simple_keyed_server.py :literal: :start-line: 22 It is also possible to use a single authorized_keys file for all users. This is common when using certificates, as AsyncSSH can automatically enforce that the certificates presented have a principal in them which matches the username. This would look something like the following. .. include:: ../examples/simple_cert_server.py :literal: :start-line: 21 Simple server with input ------------------------ The following example demonstrates reading input in a server session. It will sum a column of numbers, displaying the total and closing the connection when it receives EOF. Note that this is not an interactive application, so no echoing of user input is provided. You'll need to have the SSH client read from a file or pipe rather than the terminal or tell it not to allocate a pty for this to work right. .. include:: ../examples/math_server.py :literal: :start-line: 21 Here's an example of this server written using the streams API. In this case, :func:`listen` is used in place of :func:`create_server` since a custom subclass of :class:`SSHServer` is not required. The handler coroutine to call to handle new sessions is specified using the ``session_factory`` argument. When a new session is requested, the handler coroutine is called with AsyncSSH stream objects representing stdin, stdout, and stderr that it can use to perform I/O. This example also shows how to catch exceptions thrown when break messages, signals, or terminal size changes are received. .. include:: ../examples/stream_math_server.py :literal: :start-line: 21 Getting environment variables ----------------------------- The following example demonstrates reading environment variables set by the client. It will show all of the variables set by the client, or return an error if none are set. Note that SSH clients may restrict which environment variables (if any) are sent by default, so you may need to set options in the client to get it to do so. .. include:: ../examples/show_environment.py :literal: :start-line: 21 Getting terminal information ---------------------------- The following example demonstrates reading the client's terminal type and window size, and handling window size changes during a session. .. include:: ../examples/show_terminal.py :literal: :start-line: 21 Port forwarding --------------- The following example demonstrates a server accepting port forwarding requests from clients, but only when they are destined to port 80. When such a connection is received, a connection is attempted to the requested host and port and data is bidirectionally forwarded over SSH from the client to this destination. Requests by the client to connect to any other port are rejected. .. include:: ../examples/local_forwarding_server.py :literal: :start-line: 21 The server can also support forwarding inbound TCP connections back to the client. The following example demonstrates a server which will accept requests like this from clients, but only to listen on port 8080. When such a connection is received, the client is notified and data is bidirectionally forwarded from the incoming connection over SSH to the client. .. include:: ../examples/remote_forwarding_server.py :literal: :start-line: 21 Direct TCP connections ---------------------- The server can also accept direct TCP connection requests from the client and process the data on them itself. The following example demonstrates a server which accepts requests to port 7 (the "echo" port) for any host and echoes the data itself rather than forwarding the connection: .. include:: ../examples/direct_server.py :literal: :start-line: 21 Here's an example of this server written using the streams API. In this case, :meth:`connection_requested() ` returns a handler coroutine instead of a session object. When a new direct TCP connection is opened, the handler coroutine is called with AsyncSSH stream objects which can be used to perform I/O on the tunneled connection. .. include:: ../examples/stream_direct_server.py :literal: :start-line: 21 SFTP server ----------- The following example shows how to start an SFTP server with default behavior: .. include:: ../examples/simple_sftp_server.py :literal: :start-line: 21 A subclass of :class:`SFTPServer` can be provided as the value of the SFTP factory to override specific behavior. For example, the following code remaps path names so that each user gets access to only their own individual directory under ``/tmp/sftp``: .. include:: ../examples/chroot_sftp_server.py :literal: :start-line: 21 More complex path remapping can be performed by implementing the :meth:`map_path ` and :meth:`reverse_map_path ` methods. Individual SFTP actions can also be overridden as needed. See the :class:`SFTPServer` documentation for the full list of methods to override. asyncssh-1.3.0/docs/rftheme/000077500000000000000000000000001260630620200157255ustar00rootroot00000000000000asyncssh-1.3.0/docs/rftheme/layout.html000066400000000000000000000002631260630620200201310ustar00rootroot00000000000000{% extends "basic/layout.html" %} {# Omit the top navigation bar. #} {% block relbar1 %} {% endblock %} {# Omit the bottom navigation bar. #} {% block relbar2 %} {% endblock %} asyncssh-1.3.0/docs/rftheme/static/000077500000000000000000000000001260630620200172145ustar00rootroot00000000000000asyncssh-1.3.0/docs/rftheme/static/rftheme.css_t000066400000000000000000000006141260630620200217040ustar00rootroot00000000000000@import url("classic.css"); .tight-list * { line-height: 110% !important; margin: 0 0 3px !important; } div.sphinxsidebar { top: 0; } div.sphinxsidebarwrapper { padding-top: 8px; } div.body p, div.body dd, div.body li { text-align: left; } tt, .note tt { font-size: 1.15em; background: none; } div.body p.rubric { font-size: 1.3em; margin: 15px 0 5px; } asyncssh-1.3.0/docs/rftheme/theme.conf000066400000000000000000000001131260630620200176710ustar00rootroot00000000000000[theme] inherit = classic stylesheet = rftheme.css pygments_style = sphinx asyncssh-1.3.0/docs/rtd-req.txt000066400000000000000000000000261260630620200164100ustar00rootroot00000000000000cryptography >= 0.6.1 asyncssh-1.3.0/examples/000077500000000000000000000000001260630620200151615ustar00rootroot00000000000000asyncssh-1.3.0/examples/check_exit_status.py000077500000000000000000000026121260630620200212500ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation import asyncio, asyncssh, sys class MySSHClientSession(asyncssh.SSHClientSession): def data_received(self, data, datatype): if datatype == asyncssh.EXTENDED_DATA_STDERR: print(data, end='', file=sys.stderr) else: print(data, end='') def exit_status_received(self, status): if status: print('Program exited with status %d' % status, file=sys.stderr) else: print('Program exited successfully') def connection_lost(self, exc): if exc: print('SSH session error: ' + str(exc), file=sys.stderr) @asyncio.coroutine def run_client(): with (yield from asyncssh.connect('localhost')) as conn: chan, session = yield from conn.create_session(MySSHClientSession, 'ls abc') yield from chan.wait_closed() try: asyncio.get_event_loop().run_until_complete(run_client()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH connection failed: ' + str(exc)) asyncssh-1.3.0/examples/chroot_sftp_server.py000077500000000000000000000027011260630620200214560ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation # To run this program, the file ``ssh_host_key`` must exist with an SSH # private key in it to use as a server host key. An SSH host certificate # can optionally be provided in the file ``ssh_host_key-cert.pub``. # # The file ``ssh_user_ca`` must exist with a cert-authority entry of # the certificate authority which can sign valid client certificates. import asyncio, asyncssh, os, sys class MySFTPServer(asyncssh.SFTPServer): def __init__(self, conn): root = '/tmp/sftp/' + conn.get_extra_info('username') os.makedirs(root, exist_ok=True) super().__init__(conn, chroot=root) @asyncio.coroutine def start_server(): yield from asyncssh.listen('', 8022, server_host_keys=['ssh_host_key'], authorized_client_keys='ssh_user_ca', sftp_factory=MySFTPServer) loop = asyncio.get_event_loop() try: loop.run_until_complete(start_server()) except (OSError, asyncssh.Error) as exc: sys.exit('Error starting server: ' + str(exc)) loop.run_forever() asyncssh-1.3.0/examples/direct_client.py000077500000000000000000000025361260630620200203540ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation import asyncio, asyncssh, sys class MySSHTCPSession(asyncssh.SSHTCPSession): def data_received(self, data, datatype): # We use sys.stdout.buffer here because we're writing bytes sys.stdout.buffer.write(data) def connection_lost(self, exc): if exc: print('Direct connection error:', str(exc), file=sys.stderr) @asyncio.coroutine def run_client(): with (yield from asyncssh.connect('localhost')) as conn: chan, session = yield from conn.create_connection(MySSHTCPSession, 'www.google.com', 80) # By default, TCP connections send and receive bytes chan.write(b'HEAD / HTTP/1.0\r\n\r\n') chan.write_eof() yield from chan.wait_closed() try: asyncio.get_event_loop().run_until_complete(run_client()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH connection failed: ' + str(exc)) asyncssh-1.3.0/examples/direct_server.py000077500000000000000000000034211260630620200203760ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation # To run this program, the file ``ssh_host_key`` must exist with an SSH # private key in it to use as a server host key. An SSH host certificate # can optionally be provided in the file ``ssh_host_key-cert.pub``. # # The file ``ssh_user_ca`` must exist with a cert-authority entry of # the certificate authority which can sign valid client certificates. import asyncio, asyncssh, sys class MySSHTCPSession(asyncssh.SSHTCPSession): def connection_made(self, chan): self._chan = chan def data_received(self, data, datatype): self._chan.write(data) class MySSHServer(asyncssh.SSHServer): def connection_requested(self, dest_host, dest_port, orig_host, orig_port): if dest_port == 7: return MySSHTCPSession() else: raise asyncssh.ChannelOpenError( asyncssh.OPEN_ADMINISTRATIVELY_PROHIBITED, 'Only echo connections allowed') @asyncio.coroutine def start_server(): yield from asyncssh.create_server(MySSHServer, '', 8022, server_host_keys=['ssh_host_key'], authorized_client_keys='ssh_user_ca') loop = asyncio.get_event_loop() try: loop.run_until_complete(start_server()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH server failed: ' + str(exc)) loop.run_forever() asyncssh-1.3.0/examples/listening_client.py000077500000000000000000000025101260630620200210660ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation import asyncio, asyncssh, sys class MySSHTCPSession(asyncssh.SSHTCPSession): def connection_made(self, chan): self._chan = chan def data_received(self, data, datatype): self._chan.write(data) def connection_requested(orig_host, orig_port): print('Connection received from %s, port %s' % (orig_host, orig_port)) return MySSHTCPSession() @asyncio.coroutine def run_client(): with (yield from asyncssh.connect('localhost')) as conn: server = yield from conn.create_server(connection_requested, '', 8888, encoding='utf-8') if server: yield from server.wait_closed() else: print('Listener couldn''t be opened.', file=sys.stderr) try: asyncio.get_event_loop().run_until_complete(run_client()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH connection failed: ' + str(exc)) asyncssh-1.3.0/examples/local_forwarding_client.py000077500000000000000000000015211260630620200224070ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation import asyncio, asyncssh, sys @asyncio.coroutine def run_client(): with (yield from asyncssh.connect('localhost')) as conn: listener = yield from conn.forward_local_port('', 8080, 'www.google.com', 80) yield from listener.wait_closed() try: asyncio.get_event_loop().run_until_complete(run_client()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH connection failed: ' + str(exc)) asyncssh-1.3.0/examples/local_forwarding_client2.py000077500000000000000000000016151260630620200224750ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation import asyncio, asyncssh, sys @asyncio.coroutine def run_client(): with (yield from asyncssh.connect('localhost')) as conn: listener = yield from conn.forward_local_port('', 0, 'www.google.com', 80) print('Listening on port %s...' % listener.get_port()) yield from listener.wait_closed() try: asyncio.get_event_loop().run_until_complete(run_client()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH connection failed: ' + str(exc)) asyncssh-1.3.0/examples/local_forwarding_server.py000077500000000000000000000031231260630620200224370ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation # To run this program, the file ``ssh_host_key`` must exist with an SSH # private key in it to use as a server host key. An SSH host certificate # can optionally be provided in the file ``ssh_host_key-cert.pub``. # # The file ``ssh_user_ca`` must exist with a cert-authority entry of # the certificate authority which can sign valid client certificates. import asyncio, asyncssh, sys class MySSHServer(asyncssh.SSHServer): def connection_requested(self, dest_host, dest_port, orig_host, orig_port): if dest_port == 80: return True else: raise asyncssh.ChannelOpenError( asyncssh.OPEN_ADMINISTRATIVELY_PROHIBITED, 'Only connections to port 80 are allowed') @asyncio.coroutine def start_server(): yield from asyncssh.create_server(MySSHServer, '', 8022, server_host_keys=['ssh_host_key'], authorized_client_keys='ssh_user_ca') loop = asyncio.get_event_loop() try: loop.run_until_complete(start_server()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH server failed: ' + str(exc)) loop.run_forever() asyncssh-1.3.0/examples/math_client.py000077500000000000000000000030501260630620200200230ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation import asyncio, asyncssh, sys class MySSHClientSession(asyncssh.SSHClientSession): def next_operation(self): if self._operations: operation = self._operations.pop(0) print(operation, '= ', end='') self._chan.write(operation + '\n') else: self._chan.write_eof() def connection_made(self, chan): self._chan = chan def session_started(self): self._operations = ['2+2', '1*2*3*4', '2^32'] self.next_operation() def data_received(self, data, datatype): print(data, end='') if '\n' in data: self.next_operation() def connection_lost(self, exc): if exc: print('SSH session error: ' + str(exc), file=sys.stderr) @asyncio.coroutine def run_client(): with (yield from asyncssh.connect('localhost')) as conn: chan, session = yield from conn.create_session(MySSHClientSession, 'bc') yield from chan.wait_closed() try: asyncio.get_event_loop().run_until_complete(run_client()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH connection failed: ' + str(exc)) asyncssh-1.3.0/examples/math_server.py000077500000000000000000000040621260630620200200570ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation # To run this program, the file ``ssh_host_key`` must exist with an SSH # private key in it to use as a server host key. An SSH host certificate # can optionally be provided in the file ``ssh_host_key-cert.pub``. # # The file ``ssh_user_ca`` must exist with a cert-authority entry of # the certificate authority which can sign valid client certificates. import asyncio, asyncssh, sys class MySSHServerSession(asyncssh.SSHServerSession): def __init__(self): self._input = '' self._total = 0 def connection_made(self, chan): self._chan = chan def shell_requested(self): return True def data_received(self, data, datatype): self._input += data lines = self._input.split('\n') for line in lines[:-1]: try: if line: self._total += int(line) except ValueError: self._chan.write_stderr('Invalid number: %s\r\n' % line) self._input = lines[-1] def eof_received(self): self._chan.write('Total = %s\r\n' % self._total) self._chan.exit(0) class MySSHServer(asyncssh.SSHServer): def session_requested(self): return MySSHServerSession() @asyncio.coroutine def start_server(): yield from asyncssh.create_server(MySSHServer, '', 8022, server_host_keys=['ssh_host_key'], authorized_client_keys='ssh_user_ca') loop = asyncio.get_event_loop() try: loop.run_until_complete(start_server()) except (OSError, asyncssh.Error) as exc: sys.exit('Error starting server: ' + str(exc)) loop.run_forever() asyncssh-1.3.0/examples/remote_forwarding_client.py000077500000000000000000000015151260630620200226130ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation import asyncio, asyncssh, sys @asyncio.coroutine def run_client(): with (yield from asyncssh.connect('localhost')) as conn: listener = yield from conn.forward_remote_port('', 8080, 'localhost', 80) yield from listener.wait_closed() try: asyncio.get_event_loop().run_until_complete(run_client()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH connection failed: ' + str(exc)) asyncssh-1.3.0/examples/remote_forwarding_client2.py000077500000000000000000000022421260630620200226730ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation import asyncio, asyncssh, sys def connection_requested(orig_host, orig_port): global conn if orig_host in ('127.0.0.1', '::1'): return conn.forward_connection('localhost', 80) else: raise asyncssh.ChannelOpenError( asyncssh.OPEN_ADMINISTRATIVELY_PROHIBITED, 'Connections only allowed from localhost') @asyncio.coroutine def run_client(): global conn with (yield from asyncssh.connect('localhost')) as conn: listener = yield from conn.create_server(connection_requested, '', 8080) yield from listener.wait_closed() try: asyncio.get_event_loop().run_until_complete(run_client()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH connection failed: ' + str(exc)) asyncssh-1.3.0/examples/remote_forwarding_server.py000077500000000000000000000025571260630620200226520ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation # To run this program, the file ``ssh_host_key`` must exist with an SSH # private key in it to use as a server host key. An SSH host certificate # can optionally be provided in the file ``ssh_host_key-cert.pub``. # # The file ``ssh_user_ca`` must exist with a cert-authority entry of # the certificate authority which can sign valid client certificates. import asyncio, asyncssh, sys class MySSHServer(asyncssh.SSHServer): def server_requested(self, listen_host, listen_port): return listen_port == 8080 @asyncio.coroutine def start_server(): yield from asyncssh.create_server(MySSHServer, '', 8022, server_host_keys=['ssh_host_key'], authorized_client_keys='ssh_user_ca') loop = asyncio.get_event_loop() try: loop.run_until_complete(start_server()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH server failed: ' + str(exc)) loop.run_forever() asyncssh-1.3.0/examples/sample_client.py000077500000000000000000000025201260630620200203540ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation import asyncio, asyncssh, sys class MySSHClientSession(asyncssh.SSHClientSession): def data_received(self, data, datatype): print(data, end='') def connection_lost(self, exc): if exc: print('SSH session error: ' + str(exc), file=sys.stderr) class MySSHClient(asyncssh.SSHClient): def connection_made(self, conn): print('Connection made to %s.' % conn.get_extra_info('peername')[0]) def auth_completed(self): print('Authentication successful.') @asyncio.coroutine def run_client(): conn, client = yield from asyncssh.create_connection(MySSHClient, 'localhost') with conn: chan, session = yield from conn.create_session(MySSHClientSession, 'ls abc') yield from chan.wait_closed() try: asyncio.get_event_loop().run_until_complete(run_client()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH connection failed: ' + str(exc)) asyncssh-1.3.0/examples/set_environment.py000077500000000000000000000023411260630620200207550ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation import asyncio, asyncssh, sys class MySSHClientSession(asyncssh.SSHClientSession): def data_received(self, data, datatype): print(data, end='') def connection_lost(self, exc): if exc: print('SSH session error: ' + str(exc), file=sys.stderr) @asyncio.coroutine def run_client(): with (yield from asyncssh.connect('localhost')) as conn: chan, session = yield from conn.create_session(MySSHClientSession, 'env', env={ 'LANG': 'en_GB', 'LC_COLLATE': 'C'}) yield from chan.wait_closed() try: asyncio.get_event_loop().run_until_complete(run_client()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH connection failed: ' + str(exc)) asyncssh-1.3.0/examples/set_terminal.py000077500000000000000000000024461260630620200202320ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation import asyncio, asyncssh, sys class MySSHClientSession(asyncssh.SSHClientSession): def data_received(self, data, datatype): print(data, end='') def connection_lost(self, exc): if exc: print('SSH session error: ' + str(exc), file=sys.stderr) @asyncio.coroutine def run_client(): with (yield from asyncssh.connect('localhost')) as conn: chan, session = yield from conn.create_session(MySSHClientSession, 'echo $TERM; stty size', term_type='xterm-color', term_size=(80, 24)) yield from chan.wait_closed() try: asyncio.get_event_loop().run_until_complete(run_client()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH connection failed: ' + str(exc)) asyncssh-1.3.0/examples/sftp_client.py000077500000000000000000000014671260630620200200600ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation import asyncio, asyncssh, sys @asyncio.coroutine def run_client(): with (yield from asyncssh.connect('localhost')) as conn: with (yield from conn.start_sftp_client()) as sftp: yield from sftp.get('example.txt') try: asyncio.get_event_loop().run_until_complete(run_client()) except (OSError, asyncssh.Error) as exc: sys.exit('SFTP operation failed: ' + str(exc)) asyncssh-1.3.0/examples/show_environment.py000077500000000000000000000032471260630620200211500ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation # To run this program, the file ``ssh_host_key`` must exist with an SSH # private key in it to use as a server host key. An SSH host certificate # can optionally be provided in the file ``ssh_host_key-cert.pub``. # # The file ``ssh_user_ca`` must exist with a cert-authority entry of # the certificate authority which can sign valid client certificates. import asyncio, asyncssh, sys @asyncio.coroutine def handle_connection(stdin, stdout, stderr): env = stdout.channel.get_environment() if env: keywidth = max(map(len, env.keys()))+1 stdout.write('Environment:\r\n') for key, value in env.items(): stdout.write(' %-*s %s\r\n' % (keywidth, key+':', value)) stdout.channel.exit(0) else: stderr.write('No environment sent.\r\n') stdout.channel.exit(1) @asyncio.coroutine def start_server(): yield from asyncssh.listen('', 8022, server_host_keys=['ssh_host_key'], authorized_client_keys='ssh_user_ca', session_factory=handle_connection) loop = asyncio.get_event_loop() try: loop.run_until_complete(start_server()) except (OSError, asyncssh.Error) as exc: sys.exit('Error starting server: ' + str(exc)) loop.run_forever() asyncssh-1.3.0/examples/show_terminal.py000077500000000000000000000040161260630620200204120ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation # To run this program, the file ``ssh_host_key`` must exist with an SSH # private key in it to use as a server host key. An SSH host certificate # can optionally be provided in the file ``ssh_host_key-cert.pub``. # # The file ``ssh_user_ca`` must exist with a cert-authority entry of # the certificate authority which can sign valid client certificates. import asyncio, asyncssh, sys @asyncio.coroutine def handle_connection(stdin, stdout, stderr): term_type = stdout.channel.get_terminal_type() width, height, pixwidth, pixheight = stdout.channel.get_terminal_size() stdout.write('Terminal type: %s, size: %sx%s' % (term_type, width, height)) if pixwidth and pixheight: stdout.write(' (%sx%s pixels)' % (pixwidth, pixheight)) stdout.write('\r\nTry resizing your window!\r\n') while not stdin.at_eof(): try: line = yield from stdin.read() except asyncssh.TerminalSizeChanged as exc: stdout.write('New window size: %sx%s' % (exc.width, exc.height)) if exc.pixwidth and exc.pixheight: stdout.write(' (%sx%s pixels)' % (exc.pixwidth, exc.pixheight)) stdout.write('\r\n') @asyncio.coroutine def start_server(): yield from asyncssh.listen('', 8022, server_host_keys=['ssh_host_key'], authorized_client_keys='ssh_user_ca', session_factory=handle_connection) loop = asyncio.get_event_loop() try: loop.run_until_complete(start_server()) except (OSError, asyncssh.Error) as exc: sys.exit('Error starting server: ' + str(exc)) loop.run_forever() asyncssh-1.3.0/examples/simple_cert_server.py000077500000000000000000000033021260630620200214300ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation # To run this program, the file ``ssh_host_key`` must exist with an SSH # private key in it to use as a server host key. An SSH host certificate # can optionally be provided in the file ``ssh_host_key-cert.pub``. # # The file ``ssh_user_ca`` must exist with a cert-authority entry of # the certificate authority which can sign valid client certificates. import asyncio, asyncssh, sys class MySSHServerSession(asyncssh.SSHServerSession): def connection_made(self, chan): self._chan = chan def shell_requested(self): return True def session_started(self): self._chan.write('Welcome to my SSH server, %s!\r\n' % self._chan.get_extra_info('username')) self._chan.exit(0) class MySSHServer(asyncssh.SSHServer): def session_requested(self): return MySSHServerSession() @asyncio.coroutine def start_server(): yield from asyncssh.create_server(MySSHServer, '', 8022, server_host_keys=['ssh_host_key'], authorized_client_keys='ssh_user_ca') loop = asyncio.get_event_loop() try: loop.run_until_complete(start_server()) except (OSError, asyncssh.Error) as exc: sys.exit('Error starting server: ' + str(exc)) loop.run_forever() asyncssh-1.3.0/examples/simple_client.py000077500000000000000000000021051260630620200203630ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation import asyncio, asyncssh, sys class MySSHClientSession(asyncssh.SSHClientSession): def data_received(self, data, datatype): print(data, end='') def connection_lost(self, exc): if exc: print('SSH session error: ' + str(exc), file=sys.stderr) @asyncio.coroutine def run_client(): with (yield from asyncssh.connect('localhost')) as conn: chan, session = yield from conn.create_session(MySSHClientSession, 'ls abc') yield from chan.wait_closed() try: asyncio.get_event_loop().run_until_complete(run_client()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH connection failed: ' + str(exc)) asyncssh-1.3.0/examples/simple_keyed_server.py000077500000000000000000000036651260630620200216100ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation # To run this program, the file ``ssh_host_key`` must exist with an SSH # private key in it to use as a server host key. An SSH host certificate # can optionally be provided in the file ``ssh_host_key-cert.pub``. # # Authentication requires the directory authorized_keys to exist with # files in it named based on the username containing the client keys # and certificate authority keys which are accepted for that user. import asyncio, asyncssh, sys class MySSHServerSession(asyncssh.SSHServerSession): def connection_made(self, chan): self._chan = chan def shell_requested(self): return True def session_started(self): self._chan.write('Welcome to my SSH server, %s!\r\n' % self._chan.get_extra_info('username')) self._chan.exit(0) class MySSHServer(asyncssh.SSHServer): def connection_made(self, conn): self._conn = conn def begin_auth(self, username): try: self._conn.set_authorized_keys('authorized_keys/%s' % username) except IOError: pass return True def session_requested(self): return MySSHServerSession() @asyncio.coroutine def start_server(): yield from asyncssh.create_server(MySSHServer, '', 8022, server_host_keys=['ssh_host_key']) loop = asyncio.get_event_loop() try: loop.run_until_complete(start_server()) except (OSError, asyncssh.Error) as exc: sys.exit('Error starting server: ' + str(exc)) loop.run_forever() asyncssh-1.3.0/examples/simple_server.py000077500000000000000000000044721260630620200204240ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation # To run this program, the file ``ssh_host_key`` must exist with an SSH # private key in it to use as a server host key. An SSH host certificate # can optionally be provided in the file ``ssh_host_key-cert.pub``. import asyncio, asyncssh, crypt, sys passwords = {'guest': '', # guest account with no password 'user123': 'qV2iEadIGV2rw' # password of 'secretpw' } class MySSHServerSession(asyncssh.SSHServerSession): def shell_requested(self): return True def connection_made(self, chan): self._chan = chan def session_started(self): self._chan.write('Welcome to my SSH server, %s!\r\n' % self._chan.get_extra_info('username')) self._chan.exit(0) class MySSHServer(asyncssh.SSHServer): def connection_made(self, conn): print('SSH connection received from %s.' % conn.get_extra_info('peername')[0]) def connection_lost(self, exc): if exc: print('SSH connection error: ' + str(exc), file=sys.stderr) else: print('SSH connection closed.') def begin_auth(self, username): # If the user's password is the empty string, no auth is required return passwords.get(username) != '' def password_auth_supported(self): return True def validate_password(self, username, password): pw = passwords.get(username, '*') return crypt.crypt(password, pw) == pw def session_requested(self): return MySSHServerSession() @asyncio.coroutine def start_server(): yield from asyncssh.create_server(MySSHServer, '', 8022, server_host_keys=['ssh_host_key']) loop = asyncio.get_event_loop() try: loop.run_until_complete(start_server()) except (OSError, asyncssh.Error) as exc: sys.exit('Error starting server: ' + str(exc)) loop.run_forever() asyncssh-1.3.0/examples/simple_sftp_server.py000077500000000000000000000023321260630620200214510ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation # To run this program, the file ``ssh_host_key`` must exist with an SSH # private key in it to use as a server host key. An SSH host certificate # can optionally be provided in the file ``ssh_host_key-cert.pub``. # # The file ``ssh_user_ca`` must exist with a cert-authority entry of # the certificate authority which can sign valid client certificates. import asyncio, asyncssh, sys @asyncio.coroutine def start_server(): yield from asyncssh.listen('', 8022, server_host_keys=['ssh_host_key'], authorized_client_keys='ssh_user_ca', sftp_factory=True) loop = asyncio.get_event_loop() try: loop.run_until_complete(start_server()) except (OSError, asyncssh.Error) as exc: sys.exit('Error starting server: ' + str(exc)) loop.run_forever() asyncssh-1.3.0/examples/stderr_client.py000077500000000000000000000022761260630620200204060ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation import asyncio, asyncssh, sys class MySSHClientSession(asyncssh.SSHClientSession): def data_received(self, data, datatype): if datatype == asyncssh.EXTENDED_DATA_STDERR: print(data, end='', file=sys.stderr) else: print(data, end='') def connection_lost(self, exc): if exc: print('SSH session error: ' + str(exc), file=sys.stderr) @asyncio.coroutine def run_client(): with (yield from asyncssh.connect('localhost')) as conn: chan, session = yield from conn.create_session(MySSHClientSession, 'ls abc') yield from chan.wait_closed() try: asyncio.get_event_loop().run_until_complete(run_client()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH connection failed: ' + str(exc)) asyncssh-1.3.0/examples/stream_direct_client.py000077500000000000000000000021051260630620200217170ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation import asyncio, asyncssh, sys @asyncio.coroutine def run_client(): with (yield from asyncssh.connect('localhost')) as conn: reader, writer = yield from conn.open_connection('www.google.com', 80) # By default, TCP connections send and receive bytes writer.write(b'HEAD / HTTP/1.0\r\n\r\n') writer.write_eof() # We use sys.stdout.buffer here because we're writing bytes response = yield from reader.read() sys.stdout.buffer.write(response) try: asyncio.get_event_loop().run_until_complete(run_client()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH connection failed: ' + str(exc)) asyncssh-1.3.0/examples/stream_direct_server.py000077500000000000000000000035161260630620200217560ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation # To run this program, the file ``ssh_host_key`` must exist with an SSH # private key in it to use as a server host key. An SSH host certificate # can optionally be provided in the file ``ssh_host_key-cert.pub``. # # The file ``ssh_user_ca`` must exist with a cert-authority entry of # the certificate authority which can sign valid client certificates. import asyncio, asyncssh, sys @asyncio.coroutine def handle_connection(reader, writer): while not reader.at_eof(): data = yield from reader.read(8192) try: writer.write(data) except BrokenPipeError: break writer.close() class MySSHServer(asyncssh.SSHServer): def connection_requested(self, dest_host, dest_port, orig_host, orig_port): if dest_port == 7: return handle_connection else: raise asyncssh.ChannelOpenError( asyncssh.OPEN_ADMINISTRATIVELY_PROHIBITED, 'Only echo connections allowed') @asyncio.coroutine def start_server(): yield from asyncssh.create_server(MySSHServer, '', 8022, server_host_keys=['ssh_host_key'], authorized_client_keys='ssh_user_ca') loop = asyncio.get_event_loop() try: loop.run_until_complete(start_server()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH server failed: ' + str(exc)) loop.run_forever() asyncssh-1.3.0/examples/stream_listening_client.py000077500000000000000000000023251260630620200224450ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation import asyncio, asyncssh, sys @asyncio.coroutine def handle_connection(reader, writer): while not reader.at_eof(): data = yield from reader.read(8192) writer.write(data) writer.close() def connection_requested(orig_host, orig_port): print('Connection received from %s, port %s' % (orig_host, orig_port)) return handle_connection @asyncio.coroutine def run_client(): with (yield from asyncssh.connect('localhost')) as conn: server = yield from conn.start_server(connection_requested, '', 8888, encoding='utf-8') yield from server.wait_closed() try: asyncio.get_event_loop().run_until_complete(run_client()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH connection failed: ' + str(exc)) asyncssh-1.3.0/examples/stream_math_client.py000077500000000000000000000017031260630620200214010ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation import asyncio, asyncssh, sys @asyncio.coroutine def run_client(): with (yield from asyncssh.connect('localhost')) as conn: stdin, stdout, stderr = yield from conn.open_session('bc') for op in ['2+2', '1*2*3*4', '2^32']: stdin.write(op + '\n') result = yield from stdout.readline() print(op, '=', result, end='') try: asyncio.get_event_loop().run_until_complete(run_client()) except (OSError, asyncssh.Error) as exc: sys.exit('SSH connection failed: ' + str(exc)) asyncssh-1.3.0/examples/stream_math_server.py000077500000000000000000000041551260630620200214350ustar00rootroot00000000000000#!/usr/bin/env python3.4 # # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation # To run this program, the file ``ssh_host_key`` must exist with an SSH # private key in it to use as a server host key. An SSH host certificate # can optionally be provided in the file ``ssh_host_key-cert.pub``. # # The file ``ssh_user_ca`` must exist with a cert-authority entry of # the certificate authority which can sign valid client certificates. import asyncio, asyncssh, sys @asyncio.coroutine def handle_connection(stdin, stdout, stderr): total = 0 try: while not stdin.at_eof(): try: line = yield from stdin.readline() except (asyncssh.BreakReceived, asyncssh.SignalReceived): # Exit if the client sends a break or signal break except asyncssh.TerminalSizeChanged: # Ignore terminal size changes continue line = line.rstrip('\n') if line: try: total += int(line) except ValueError: stderr.write('Invalid number: %s\r\n' % line) stdout.write('Total = %s\r\n' % total) stdout.channel.exit(0) except BrokenPipeError: # The channel is already closed here, so we can't send an exit status stdout.close() @asyncio.coroutine def start_server(): yield from asyncssh.listen('', 8022, server_host_keys=['ssh_host_key'], authorized_client_keys='ssh_user_ca', session_factory=handle_connection) loop = asyncio.get_event_loop() try: loop.run_until_complete(start_server()) except (OSError, asyncssh.Error) as exc: sys.exit('Error starting server: ' + str(exc)) loop.run_forever() asyncssh-1.3.0/pylintrc000066400000000000000000000272151260630620200151410ustar00rootroot00000000000000[MASTER] # Specify a configuration file. #rcfile= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Profiled execution. profile=no # Add files or directories to the blacklist. They should be base names, not # paths. ignore= # Pickle collected data for later comparisons. persistent=yes # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= # Use multiple processes to speed up Pylint. jobs=1 # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code extension-pkg-whitelist= # Allow optimization of some AST trees. This will activate a peephole AST # optimizer, which will apply various small optimizations. For instance, it can # be used to obtain the result of joining multiple strings with the addition # operator. Joining a lot of strings can lead to a maximum recursion error in # Pylint and this flag can prevent that. It has one side effect, the resulting # AST will be different than the one from reality. optimize-ast=no [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED confidence= # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time. See also the "--disable" option for examples. #enable= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration # file where it should appear only once).You can also use "--disable=all" to # disable everything first and then reenable specific checks. For example, if # you want to run only the similarities checker, you can use "--disable=all # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" disable=fixme,locally-enabled,locally-disabled [REPORTS] # Set the output format. Available formats are text, parseable, colorized, msvs # (visual studio) and html. You can also give a reporter class, eg # mypackage.mymodule.MyReporterClass. output-format=text # Put messages in a separate file for each module / package specified on the # command line instead of printing them on stdout. Reports (if any) will be # written in a file name "pylint_global.[txt|html]". files-output=no # Tells whether to display a full report or only the messages reports=yes # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Add a comment according to your evaluation note. This is used by the global # evaluation report (RP0004). comment=no # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details #msg-template= [BASIC] # Required attributes for module, separated by a comma required-attributes= # List of builtins function names that should not be used, separated by a comma bad-functions=map,filter # Good variable names which should always be accepted, separated by a comma good-names=a,b,c,ca,ch,f,fs,g,h,i,ip,iv,j,k,l,n,r,s,sa,t,v,x,y,_ # Bad variable names which should always be refused, separated by a comma bad-names= # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= # Include a hint for the correct naming format with invalid-name include-naming-hint=no # Regular expression matching correct argument names argument-rgx=[a-z_][a-z0-9_]{2,50}$ # Naming hint for argument names argument-name-hint=[a-z_][a-z0-9_]{2,50}$ # Regular expression matching correct variable names variable-rgx=[a-z_][a-z0-9_]{2,50}$ # Naming hint for variable names variable-name-hint=[a-z_][a-z0-9_]{2,50}$ # Regular expression matching correct class names class-rgx=[A-Z_][a-zA-Z0-9]+$ # Naming hint for class names class-name-hint=[A-Z_][a-zA-Z0-9]+$ # Regular expression matching correct constant names const-rgx=(([A-Za-z_][A-Za-z0-9_]*)|(__.*__))$ # Naming hint for constant names const-name-hint=(([A-Z][A-Z-9_]*)|(__.*__))$ # Regular expression matching correct method names method-rgx=[a-z_][a-z0-9_]{2,50}$ # Naming hint for method names method-name-hint=[a-z_][a-z0-9_]{2,50}$ # Regular expression matching correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Naming hint for module names module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Regular expression matching correct function names function-rgx=[A-Za-z_][A-Za-z0-9_]{2,50}$ # Naming hint for function names function-name-hint=[a-z_][a-z0-9_]{2,50}$ # Regular expression matching correct attribute names attr-rgx=[a-z_][a-z0-9_]{2,50}$ # Naming hint for attribute names attr-name-hint=[a-z_][a-z0-9_]{2,50}$ # Regular expression matching correct inline iteration names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ # Naming hint for inline iteration names inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ # Regular expression matching correct class attribute names class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,50}|(__.*__))$ # Naming hint for class attribute names class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,50}|(__.*__))$ # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=__.*__ # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=-1 [FORMAT] # Maximum number of characters on a single line. max-line-length=100 # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=no # List of optional constructs for which whitespace checking is disabled no-space-check=trailing-comma,dict-separator # Maximum number of lines in a module max-module-lines=5000 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=4 # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. expected-line-ending-format= [LOGGING] # Logging modules to check that the string format arguments are in logging # function parameter format logging-modules=logging [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME,XXX,TODO [SIMILARITIES] # Minimum lines number of a similarity. min-similarity-lines=8 # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes # Ignore imports when computing similarities. ignore-imports=no [SPELLING] # Spelling dictionary name. Available dictionaries: none. To make it working # install python-enchant package. spelling-dict= # List of comma separated words that should not be checked. spelling-ignore-words= # A path to a file that contains private dictionary; one word per line. spelling-private-dict-file= # Tells whether to store unknown words to indicated private dictionary in # --spelling-private-dict-file option instead of raising a message. spelling-store-unknown-words=no [TYPECHECK] # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis ignored-modules= # List of classes names for which member attributes should not be checked # (useful for classes with attributes dynamically set). ignored-classes=SQLObject # When zope mode is activated, add a predefined set of Zope acquired attributes # to generated-members. zope=no # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E0201 when accessed. Python regular # expressions are accepted. generated-members=REQUEST,acl_users,aq_parent [VARIABLES] # Tells whether we should check for unused import in __init__ files. init-import=no # A regular expression matching the name of dummy variables (i.e. expectedly # not used). dummy-variables-rgx=_$|dummy # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins= # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_,_cb [CLASSES] # List of interface methods to ignore, separated by a comma. This is used for # instance to not check methods defines in Zope's Interface base class. ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=mcs # List of member names, which should be excluded from the protected access # warning. exclude-protected=_asdict,_fields,_replace,_source,_make [DESIGN] # Maximum number of arguments for function / method max-args=25 # Argument names that match this expression will be ignored. Default to name # with leading underscore ignored-argument-names=_.* # Maximum number of locals for function / method body max-locals=50 # Maximum number of return / yield for function / method body max-returns=10 # Maximum number of branch for function / method body max-branches=50 # Maximum number of statements in function / method body max-statements=100 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of attributes for a class (see R0902). max-attributes=100 # Minimum number of public methods for a class (see R0903). min-public-methods=0 # Maximum number of public methods for a class (see R0904). max-public-methods=50 [IMPORTS] # Deprecated modules which should not be used, separated by a comma deprecated-modules=stringprep,optparse # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled) import-graph= # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled) ext-import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled) int-import-graph= [EXCEPTIONS] # Exceptions that will emit a warning when being caught. Defaults to # "Exception" overgeneral-exceptions=Exception asyncssh-1.3.0/setup.py000077500000000000000000000042211260630620200150570ustar00rootroot00000000000000#!/usr/bin/env python3.4 # Copyright (c) 2013-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """AsyncSSH: Asynchronous SSHv2 client and server library AsyncSSH is a Python package which provides an asynchronous client and server implementation of the SSHv2 protocol on top of the Python asyncio framework. It requires Python 3.4 or later and either the PyCA library or the PyCrypto library for some cryptographic functions. """ from os import path from setuptools import setup base_dir = path.abspath(path.dirname(__file__)) doclines = __doc__.split('\n', 1) with open(path.join(base_dir, 'README.rst')) as desc: long_description = desc.read() with open(path.join(base_dir, 'asyncssh', 'version.py')) as version: exec(version.read()) setup(name = 'asyncssh', version = __version__, author = __author__, author_email = __author_email__, url = __url__, license = 'Eclipse Public License v1.0', description = doclines[0], long_description = long_description, platforms = 'Any', install_requires = ['cryptography >= 1.0.0'], extras_require = { 'bcrypt': ['py-bcrypt >= 0.4'], 'libnacl': ['libnacl >= 1.4.2'] }, packages = ['asyncssh', 'asyncssh.crypto', 'asyncssh.crypto.pyca'], scripts = [], test_suite = 'tests', classifiers = [ 'Development Status :: 3 - Alpha', 'Environment :: Console', 'Intended Audience :: Developers', 'License :: OSI Approved', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX', 'Programming Language :: Python :: 3.4', 'Topic :: Internet', 'Topic :: Security :: Cryptography', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: System :: Networking']) asyncssh-1.3.0/tests/000077500000000000000000000000001260630620200145055ustar00rootroot00000000000000asyncssh-1.3.0/tests/__init__.py000066400000000000000000000010721260630620200166160ustar00rootroot00000000000000# Copyright (c) 2014-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Unit tests for AsyncSSH""" from . import test_asn1, test_cipher, test_compression, test_known_hosts from . import test_mac, test_public_key, test_saslprep, util asyncssh-1.3.0/tests/test_asn1.py000066400000000000000000000175701260630620200167720ustar00rootroot00000000000000# Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Unit tests for ASN.1 encoding and decoding""" import codecs import unittest from asyncssh.asn1 import der_encode, der_decode from asyncssh.asn1 import ASN1EncodeError, ASN1DecodeError from asyncssh.asn1 import BitString, ObjectIdentifier from asyncssh.asn1 import RawDERObject, TaggedDERObject, PRIVATE class _TestASN1(unittest.TestCase): """Unit tests for ASN.1 module""" # pylint: disable=bad-whitespace tests = [ (None, '0500'), (False, '010100'), (True, '0101ff'), (0, '020100'), (127, '02017f'), (128, '02020080'), (256, '02020100'), (-128, '020180'), (-129, '0202ff7f'), (-256, '0202ff00'), (b'', '0400'), (b'\0', '040100'), (b'abc', '0403616263'), (127*b'\0', '047f' + 127*'00'), (128*b'\0', '048180' + 128*'00'), ('', '0c00'), ('\0', '0c0100'), ('abc', '0c03616263'), ((), '3000'), ((1,), '3003020101'), ((1, 2), '3006020101020102'), (frozenset(), '3100'), (frozenset({1}), '3103020101'), (frozenset({1, 2}), '3106020101020102'), (frozenset({-128, 127}), '310602017f020180'), (BitString(b''), '030100'), (BitString(b'\0', 7), '03020700'), (BitString(b'\x80', 7), '03020780'), (BitString(b'\x80', named=True), '03020780'), (BitString(b'\x81', named=True), '03020081'), (BitString(b'\x81\x00', named=True), '03020081'), (BitString(b'\x80', 6), '03020680'), (BitString(b'\x80'), '03020080'), (BitString(b'\x80\x00', 7), '0303078000'), (BitString(''), '030100'), (BitString('0'), '03020700'), (BitString('1'), '03020780'), (BitString('10'), '03020680'), (BitString('10000000'), '03020080'), (BitString('10000001'), '03020081'), (BitString('100000000'), '0303078000'), (ObjectIdentifier('0.0'), '060100'), (ObjectIdentifier('1.2'), '06012a'), (ObjectIdentifier('1.2.840'), '06032a8648'), (ObjectIdentifier('2.5'), '060155'), (ObjectIdentifier('2.40'), '060178'), (TaggedDERObject(0, None), 'a0020500'), (TaggedDERObject(1, None), 'a1020500'), (TaggedDERObject(32, None), 'bf20020500'), (TaggedDERObject(128, None), 'bf8100020500'), (TaggedDERObject(0, None, PRIVATE), 'e0020500'), (RawDERObject(0, b'', PRIVATE), 'c000') ] encode_errors = [ (range, [1]), # Unsupported type (BitString, [b'', 1]), # Bit count with empty value (BitString, [b'', -1]), # Invalid unused bit count (BitString, [b'', 8]), # Invalid unused bit count (BitString, [b'0c0', 7]), # Unused bits not zero (BitString, ['', 1]), # Unused bits with string (BitString, [0]), # Invalid type (ObjectIdentifier, ['']), # Too few components (ObjectIdentifier, ['1']), # Too few components (ObjectIdentifier, ['-1.1']), # First component out of range (ObjectIdentifier, ['3.1']), # First component out of range (ObjectIdentifier, ['0.-1']), # Second component out of range (ObjectIdentifier, ['0.40']), # Second component out of range (ObjectIdentifier, ['1.-1']), # Second component out of range (ObjectIdentifier, ['1.40']), # Second component out of range (ObjectIdentifier, ['1.1.-1']), # Later component out of range (TaggedDERObject, [0, None, 99]), # Invalid ASN.1 class (RawDERObject, [0, None, 99]), # Invalid ASN.1 class ] decode_errors = [ '', # Incomplete data '01', # Incomplete data '0101', # Incomplete data '1f00', # Incomplete data '1f8000', # Incomplete data '1f0001', # Incomplete data '1f80', # Incomplete tag '0180', # Indefinite length '050001', # Unexpected bytes at end '2500', # Constructed null '050100', # Null with content '2100', # Constructed boolean '010102', # Boolean value not 0x00/0xff '2200', # Constructed integer '2400', # Constructed octet string '2c00', # Constructed UTF-8 string '1000', # Non-constructed sequence '1100', # Non-constructed set '2300', # Constructed bit string '03020800', # Invalid unused bit count '2600', # Constructed object identifier '0600', # Empty object identifier '06020080', # Invalid component '06020081' # Incomplete component ] # pylint: enable=bad-whitespace def test_asn1(self): """Unit test ASN.1 module""" for value, data in self.tests: data = codecs.decode(data, 'hex') with self.subTest(msg='encode', value=value): self.assertEqual(der_encode(value), data) with self.subTest(msg='decode', data=data): decoded_value = der_decode(data) self.assertEqual(decoded_value, value) self.assertEqual(hash(decoded_value), hash(value)) self.assertEqual(repr(decoded_value), repr(value)) self.assertEqual(str(decoded_value), str(value)) for cls, args in self.encode_errors: with self.subTest(msg='encode error', cls=cls.__name__, args=args): with self.assertRaises(ASN1EncodeError): der_encode(cls(*args)) for data in self.decode_errors: with self.subTest(msg='decode error', data=data): with self.assertRaises(ASN1DecodeError): der_decode(codecs.decode(data, 'hex')) asyncssh-1.3.0/tests/test_cipher.py000066400000000000000000000074321260630620200173760ustar00rootroot00000000000000# Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Unit tests for symmetric key encryption""" import os import unittest from asyncssh.cipher import get_encryption_algs, get_encryption_params from asyncssh.cipher import get_cipher class _TestCipher(unittest.TestCase): """Unit tests for cipher module""" def test_encryption_algs(self): """Unit test encryption algorithms""" for alg in get_encryption_algs(): with self.subTest(alg=alg): keysize, ivsize, blocksize, mode = get_encryption_params(alg) key = os.urandom(keysize) iv = os.urandom(ivsize) data = os.urandom(32*blocksize) enc_cipher = get_cipher(alg, key, iv) dec_cipher = get_cipher(alg, key, iv) badkey = bytearray(key) badkey[-1] ^= 0xff bad_cipher = get_cipher(alg, bytes(badkey), iv) hdr = os.urandom(4) if mode == 'chacha': nonce = os.urandom(8) enchdr = enc_cipher.crypt_len(hdr, nonce) encdata, mac = enc_cipher.encrypt_and_sign(hdr, data, nonce) dechdr = dec_cipher.crypt_len(enchdr, nonce) decdata = dec_cipher.verify_and_decrypt(dechdr, encdata, nonce, mac) badhdr = bad_cipher.crypt_len(enchdr, nonce) baddata = bad_cipher.verify_and_decrypt(badhdr, encdata, nonce, mac) self.assertIsNone(baddata) elif mode == 'gcm': dechdr = hdr encdata, mac = enc_cipher.encrypt_and_sign(hdr, data) decdata = dec_cipher.verify_and_decrypt(hdr, encdata, mac) baddata = bad_cipher.verify_and_decrypt(hdr, encdata, mac) self.assertIsNone(baddata) else: dechdr = hdr encdata1 = enc_cipher.encrypt(data[:len(data)//2]) encdata2 = enc_cipher.encrypt(data[len(data)//2:]) decdata = dec_cipher.decrypt(encdata1) decdata += dec_cipher.decrypt(encdata2) baddata = bad_cipher.decrypt(encdata1) baddata += bad_cipher.decrypt(encdata2) self.assertNotEqual(data, baddata) self.assertEqual(hdr, dechdr) self.assertEqual(data, decdata) def test_chacha_errors(self): """Unit test error code paths in chacha cipher""" alg = b'chacha20-poly1305@openssh.com' keysize, ivsize, _, _ = get_encryption_params(alg) key = os.urandom(keysize) iv = os.urandom(ivsize) with self.subTest('Chacha20Poly1305 key size error'): with self.assertRaises(ValueError): get_cipher(alg, key[:-1], iv) with self.subTest('Chacha20Poly1305 nonce size error'): cipher = get_cipher(alg, key, iv) with self.assertRaises(ValueError): cipher.crypt_len(b'', b'') with self.assertRaises(ValueError): cipher.encrypt_and_sign(b'', b'', b'') with self.assertRaises(ValueError): cipher.verify_and_decrypt(b'', b'', b'', b'') asyncssh-1.3.0/tests/test_compression.py000066400000000000000000000022761260630620200204660ustar00rootroot00000000000000# Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Unit tests for compression""" import os import unittest from asyncssh.compression import get_compression_algs, get_compression_params from asyncssh.compression import get_compressor, get_decompressor class TestCompression(unittest.TestCase): """Unit tests for compression module""" def test_compression_algs(self): """Unit test compression algorithms""" for alg in get_compression_algs(): with self.subTest(alg=alg): get_compression_params(alg) data = os.urandom(256) compressor = get_compressor(alg) decompressor = get_decompressor(alg) if compressor: cmpdata = compressor.compress(data) self.assertEqual(decompressor.decompress(cmpdata), data) asyncssh-1.3.0/tests/test_kex.py000066400000000000000000000255751260630620200167230ustar00rootroot00000000000000# Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Unit tests for key exchange""" from hashlib import sha1 from .util import TempDirTestCase, run from asyncssh.dh import MSG_KEXDH_INIT, MSG_KEXDH_REPLY from asyncssh.dh import _KexDHGex, MSG_KEX_DH_GEX_GROUP from asyncssh.dh import MSG_KEX_DH_GEX_INIT, MSG_KEX_DH_GEX_REPLY from asyncssh.ecdh import MSG_KEX_ECDH_INIT, MSG_KEX_ECDH_REPLY from asyncssh.kex import register_kex_alg, get_kex_algs, get_kex from asyncssh.misc import DisconnectError from asyncssh.packet import SSHPacket, Byte, MPInt, String from asyncssh.public_key import decode_ssh_public_key, read_private_key # Short variable names are used here, matching names in the specs # pylint: disable=invalid-name class _Conn: """Stub class for connection object""" _packets = [] @classmethod def process_packets(cls): """Process queued packets""" while cls._packets: peer, data = cls._packets.pop(0) peer.process_packet(data) def __init__(self, alg, peer, server): self._peer = peer self._server = server self._key = None self._kex = get_kex(self, alg) def is_client(self): """Return if this is a client connection""" return not self._server def is_server(self): """Return if this is a server connection""" return self._server def get_hash_prefix(self): """Return the bytes used in calculating unique connection hashes""" # pylint: disable=no-self-use return b'prefix' def process_packet(self, data): """Handle an incoming SSH packet""" packet = SSHPacket(data) pkttype = packet.get_byte() self._kex.process_packet(pkttype, packet) def send_packet(self, *args): """Handle a request to send an SSH packet""" self._packets.append((self._peer, b''.join(args))) def send_newkeys(self, k, h): """Handle a request to send a new keys message""" # TODO self._key = self._kex.compute_key(k, h, b'A', h, 128) def get_key(self): """Return generated key data""" return self._key def get_peer(self): """Return peer""" return self._peer def simulate_dh_init(self, e): """Simulate receiving a DH init packet""" self.process_packet(Byte(MSG_KEXDH_INIT) + MPInt(e)) def simulate_dh_reply(self, host_key_data, f, sig): """Simulate receiving a DH reply packet""" self.process_packet(b''.join((Byte(MSG_KEXDH_REPLY), String(host_key_data), MPInt(f), String(sig)))) def simulate_dh_gex_group(self, p, g): """Simulate receiving a DH GEX group packet""" self.process_packet(Byte(MSG_KEX_DH_GEX_GROUP) + MPInt(p) + MPInt(g)) def simulate_dh_gex_init(self, e): """Simulate receiving a DH GEX init packet""" self.process_packet(Byte(MSG_KEX_DH_GEX_INIT) + MPInt(e)) def simulate_dh_gex_reply(self, host_key_data, f, sig): """Simulate receiving a DH GEX reply packet""" self.process_packet(b''.join((Byte(MSG_KEX_DH_GEX_REPLY), String(host_key_data), MPInt(f), String(sig)))) def simulate_ecdh_init(self, client_pub): """Simulate receiving an ECDH init packet""" self.process_packet(Byte(MSG_KEX_ECDH_INIT) + String(client_pub)) def simulate_ecdh_reply(self, host_key_data, server_pub, sig): """Simulate receiving ab ECDH reply packet""" self.process_packet(b''.join((Byte(MSG_KEX_ECDH_REPLY), String(host_key_data), String(server_pub), String(sig)))) class _ClientConn(_Conn): """Stub class for client connection""" def __init__(self, alg): super().__init__(alg, _ServerConn(alg, self), False) def validate_server_host_key(self, host_key_data): """Validate and return the server's host key""" # pylint: disable=no-self-use return decode_ssh_public_key(host_key_data) class _ServerConn(_Conn): """Stub class for server connection""" def __init__(self, alg, client_conn): super().__init__(alg, client_conn, True) run('openssl genrsa -out priv 2048') priv_key = read_private_key('priv') self._server_host_key = (priv_key, String(priv_key.algorithm) + priv_key.encode_ssh_public()) def get_server_host_key(self): """Return the server host key""" return self._server_host_key class _TestKex(TempDirTestCase): """Unit tests for kex module""" def test_key_exchange_algs(self): """Unit test kex exchange algorithms""" for alg in get_kex_algs(): with self.subTest(alg=alg): client_conn = _ClientConn(alg) server_conn = client_conn.get_peer() with self.subTest('Check matching keys'): _Conn.process_packets() self.assertEqual(client_conn.get_key(), server_conn.get_key()) with self.subTest('Check bad init msg'): with self.assertRaises(DisconnectError): client_conn.process_packet(Byte(MSG_KEXDH_INIT)) with self.subTest('Check bad reply msg'): with self.assertRaises(DisconnectError): server_conn.process_packet(Byte(MSG_KEXDH_REPLY)) def test_dh_gex_old(self): """Unit test old DH group exchange request""" register_kex_alg(b'diffie-hellman-group-exchange-sha1-1024', _KexDHGex, sha1, True, 1024) register_kex_alg(b'diffie-hellman-group-exchange-sha1-2048', _KexDHGex, sha1, True, 1024) for size in (b'1024', b'2048'): with self.subTest('Old DH group exchange', size=size): alg = b'diffie-hellman-group-exchange-sha1-' + size client_conn = _ClientConn(alg) server_conn = client_conn.get_peer() _Conn.process_packets() self.assertEqual(client_conn.get_key(), server_conn.get_key()) def test_dh_errors(self): """Unit test error conditions in DH key exchange""" client_conn = _ClientConn(b'diffie-hellman-group14-sha1') server_conn = client_conn.get_peer() with self.subTest('Invalid e value'): with self.assertRaises(DisconnectError): server_conn.simulate_dh_init(0) with self.subTest('Invalid f value'): with self.assertRaises(DisconnectError): client_conn.simulate_dh_reply(b'', 0, b'') with self.subTest('Invalid signature'): with self.assertRaises(DisconnectError): _, host_key_data = server_conn.get_server_host_key() client_conn.simulate_dh_reply(host_key_data, 1, b'') def test_dh_gex_errors(self): """Unit test error conditions in DH group and key exchange""" client_conn = _ClientConn(b'diffie-hellman-group-exchange-sha1') server_conn = client_conn.get_peer() with self.subTest('Group sent to server'): with self.assertRaises(DisconnectError): server_conn.simulate_dh_gex_group(1, 2) with self.subTest('Init sent to client'): with self.assertRaises(DisconnectError): client_conn.simulate_dh_gex_init(1) with self.subTest('Init sent before group'): with self.assertRaises(DisconnectError): server_conn.simulate_dh_gex_init(1) with self.subTest('Reply sent to server'): with self.assertRaises(DisconnectError): server_conn.simulate_dh_gex_reply(b'', 1, b'') with self.subTest('Reply sent before group'): with self.assertRaises(DisconnectError): client_conn.simulate_dh_gex_reply(b'', 1, b'') def test_ecdh_errors(self): """Unit test error conditions in ECDH key exchange""" try: from asyncssh.crypto import ECDH except ImportError: # pragma: no cover return client_conn = _ClientConn(b'ecdh-sha2-nistp256') server_conn = client_conn.get_peer() with self.subTest('Init sent to client'): with self.assertRaises(DisconnectError): client_conn.simulate_ecdh_init(b'') with self.subTest('Invalid client public key'): with self.assertRaises(DisconnectError): server_conn.simulate_ecdh_init(b'') with self.subTest('Reply sent to server'): with self.assertRaises(DisconnectError): server_conn.simulate_ecdh_reply(b'', b'', b'') with self.subTest('Invalid server host key'): with self.assertRaises(DisconnectError): client_conn.simulate_ecdh_reply(b'', b'', b'') with self.subTest('Invalid server public key'): with self.assertRaises(DisconnectError): _, host_key_data = server_conn.get_server_host_key() client_conn.simulate_ecdh_reply(host_key_data, b'', b'') with self.subTest('Invalid signature'): with self.assertRaises(DisconnectError): _, host_key_data = server_conn.get_server_host_key() server_pub = ECDH(b'nistp256').get_public() client_conn.simulate_ecdh_reply(host_key_data, server_pub, b'') def test_curve25519dh_errors(self): """Unit test error conditions in Curve25519DH key exchange""" try: from asyncssh.crypto import Curve25519DH except ImportError: # pragma: no cover return client_conn = _ClientConn(b'curve25519-sha256@libssh.org') server_conn = client_conn.get_peer() with self.subTest('Invalid client public key'): with self.assertRaises(DisconnectError): server_conn.simulate_ecdh_init(b'') with self.subTest('Invalid server public key'): with self.assertRaises(DisconnectError): _, host_key_data = server_conn.get_server_host_key() client_conn.simulate_ecdh_reply(host_key_data, b'', b'') with self.subTest('Invalid signature'): with self.assertRaises(DisconnectError): _, host_key_data = server_conn.get_server_host_key() server_pub = Curve25519DH().get_public() client_conn.simulate_ecdh_reply(host_key_data, server_pub, b'') asyncssh-1.3.0/tests/test_known_hosts.py000066400000000000000000000146001260630620200204730ustar00rootroot00000000000000# Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Unit tests for matching against known_hosts file Note: These tests assume that the ssh-keygen command is available on the system and in the user's path. """ import binascii import hashlib import hmac import os from asyncssh import import_public_key from asyncssh.known_hosts import match_known_hosts from .util import TempDirTestCase, run def _hash(host): """Return a hashed version of a hostname in a known_hosts file""" salt = os.urandom(20) hosthash = hmac.new(salt, host.encode(), hashlib.sha1).digest() entry = b'|'.join((b'', b'1', binascii.b2a_base64(salt)[:-1], binascii.b2a_base64(hosthash)[:-1])) return entry.decode() class _TestKnownHosts(TempDirTestCase): """Unit tests for known_hosts module""" keylists = ([], [], []) imported_keylists = ([], [], []) @classmethod def setUpClass(cls): """Create public keys needed for test""" super().setUpClass() for keylist, imported_keylist in zip(cls.keylists, cls.imported_keylists): for _ in range(3): run('ssh-keygen -t rsa -N "" -f key') with open('key.pub', 'r') as f: k = f.read() keylist.append(k) imported_keylist.append(import_public_key(k)) run('rm key key.pub') def check_match(self, known_hosts, results=None, host='host', addr='1.2.3.4', port=22): """Check the result of calling match_known_hosts""" if results: results = tuple([kl[r] for r in result] for kl, result in zip(self.imported_keylists, results)) matches = match_known_hosts(known_hosts, host, addr, port) self.assertEqual(matches, results) def check_hosts(self, patlists, results=None, host='host', addr='1.2.3.4', port=22, from_file=False): """Check a known_hosts file built from the specified patterns""" prefixes = ('', '@cert-authority ', '@revoked ') known_hosts = '# Comment line\n # Comment line with whitespace\n\n' for prefix, patlist, key in zip(prefixes, patlists, self.keylists): for pattern, key in zip(patlist, key): known_hosts += '%s%s %s' % (prefix, pattern, key) if from_file: with open('known_hosts', 'w') as f: f.write(known_hosts) known_hosts = 'known_hosts' else: known_hosts = known_hosts.encode() return self.check_match(known_hosts, results, host, addr, port) def test_match(self): """Test known host matching""" matches = ( ('Empty file', ([], [], []), ([], [], [])), ('Exact host and port', (['[host]:22'], [], []), ([0], [], [])), ('Exact host', (['host'], [], []), ([0], [], [])), ('Exact host CA', ([], ['host'], []), ([], [0], [])), ('Exact host revoked', ([], [], ['host']), ([], [], [0])), ('Wildcard host', (['hos*'], [], []), ([0], [], [])), ('Mismatched port', (['[host]:23'], [], []), ([], [], [])), ('Negative host', (['hos*,!host'], [], []), ([], [], [])), ('Exact addr and port', (['[1.2.3.4]:22'], [], []), ([0], [], [])), ('Exact addr', (['1.2.3.4'], [], []), ([0], [], [])), ('Subnet', (['1.2.3.0/24'], [], []), ([0], [], [])), ('Negative addr', (['1.2.3.0/24,!1.2.3.4'], [], []), ([], [], [])), ('Hashed host', ([_hash('host')], [], []), ([0], [], [])), ('Hashed addr', ([_hash('1.2.3.4')], [], []), ([0], [], [])) ) for testname, patlists, result in matches: with self.subTest(testname): self.check_hosts(patlists, result) def test_no_addr(self): """Test match without providing addr""" self.check_hosts((['host'], [], []), ([0], [], []), addr=None) self.check_hosts((['1.2.3.4'], [], []), ([], [], []), addr=None) def test_no_port(self): """Test match without providing port""" self.check_hosts((['host'], [], []), ([0], [], []), port=None) self.check_hosts((['[host]:22'], [], []), ([], [], []), port=None) def test_no_match(self): """Test for cases where no match is found""" no_match = (([], [], []), (['host1', 'host2'], [], []), (['2.3.4.5', '3.4.5.6'], [], []), (['[host]:2222', '[host]:22222'], [], [])) for patlists in no_match: self.check_hosts(patlists, ([], [], [])) def test_missing_key(self): """Test for line with missing key data""" with self.assertRaises(ValueError): self.check_match(b'xxx\n') def test_missing_key_with_tag(self): """Test for line with tag with missing key data""" with self.assertRaises(ValueError): self.check_match(b'@cert-authority xxx\n') def test_invalid_key(self): """Test for line with invaid key""" self.check_match(b'xxx yyy\n', ([], [], [])) def test_invalid_marker(self): """Test for line with invaid marker""" with self.assertRaises(ValueError): self.check_match(b'@xxx yyy zzz\n') def test_incomplete_hash(self): """Test for line with incomplete host hash""" with self.assertRaises(ValueError): self.check_hosts((['|1|aaaa'], [], [])) def test_invalid_hash(self): """Test for line with invalid host hash""" with self.assertRaises(ValueError): self.check_hosts((['|1|aaa'], [], [])) def test_unknown_hash_type(self): """Test for line with unknown host hash type""" with self.assertRaises(ValueError): self.check_hosts((['|2|aaaa|'], [], [])) def test_file(self): """Test match against file""" self.check_hosts((['host'], [], []), ([0], [], []), from_file=True) asyncssh-1.3.0/tests/test_mac.py000066400000000000000000000025351260630620200166630ustar00rootroot00000000000000# Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Unit tests for message authentication""" import os import unittest from asyncssh.mac import get_mac_algs, get_mac_params, get_mac class _TestMAC(unittest.TestCase): """Unit tests for mac module""" def test_mac_algs(self): """Unit test MAC algorithms""" for alg in get_mac_algs(): with self.subTest(alg=alg): keysize, _, _ = get_mac_params(alg) key = os.urandom(keysize) data = os.urandom(256) enc_mac = get_mac(alg, key) dec_mac = get_mac(alg, key) baddata = bytearray(data) baddata[-1] ^= 0xff mac = enc_mac.sign(data) badmac = bytearray(mac) badmac[-1] ^= 0xff self.assertTrue(dec_mac.verify(data, mac)) self.assertFalse(dec_mac.verify(bytes(baddata), mac)) self.assertFalse(dec_mac.verify(data, bytes(badmac))) asyncssh-1.3.0/tests/test_native_ec.py000066400000000000000000000114311260630620200200530ustar00rootroot00000000000000# Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Unit tests for native Python elliptic curve implementation""" import unittest from asyncssh.crypto.ec import PrimePoint, register_prime_curve from asyncssh.crypto.ec import decode_ec_point, encode_ec_point from asyncssh.crypto.ec import get_ec_curve_params, lookup_ec_curve_by_params from asyncssh.crypto.ecdh import ECDH # Short variable names are used here, matching names in the specs # pylint: disable=invalid-name class _TestNativeEC(unittest.TestCase): """Unit tests for native Python elliptic curve modules""" def test_register_errors(self): """Unit test of native Python EC registration errors""" G, n = get_ec_curve_params(b'nistp256') p, a, b, Gx, Gy = G.curve.p, G.curve.a, G.curve.b, G.x, G.y with self.subTest('Bad prime'): with self.assertRaises(ValueError): register_prime_curve(b'bad', p+1, a, b, Gx, Gy, n) with self.subTest('Bad a, b pair'): with self.assertRaises(ValueError): register_prime_curve(b'bad', p, a+1, b, Gx, Gy, n) with self.subTest('Bad generator point'): with self.assertRaises(ValueError): register_prime_curve(b'bad', p, a, b, Gx+1, Gy, n) with self.subTest('Bad order'): with self.assertRaises(ValueError): register_prime_curve(b'bad', p, a, b, Gx, Gy, n+1) with self.subTest('Weak prime'): with self.assertRaises(ValueError): register_prime_curve(b'bad', 263, 2, 3, 200, 39, 270) def test_get_params(self): """Test errors getting EC curve params""" with self.subTest('Get params'): with self.assertRaises(ValueError): get_ec_curve_params(b'xxx') def test_lookup_by_params(self): """Test errors in EC curve lookup by params""" G, n = get_ec_curve_params(b'nistp256') with self.subTest('Bad curve'): with self.assertRaises(ValueError): lookup_ec_curve_by_params(G.curve.p+1, G.curve.a, G.curve.b, G.encode(), n) with self.subTest('Unknown curve'): with self.assertRaises(ValueError): lookup_ec_curve_by_params(263, 2, 3, encode_ec_point(2, 200, 39), 270) def test_encode(self): """Unit test native Python EC point encoding""" G, _ = get_ec_curve_params(b'nistp256') with self.subTest('Encode infinity'): self.assertEqual(encode_ec_point(None, None, None), b'\x00') with self.subTest('Decode infinity'): point = PrimePoint.decode(G.curve, b'\x00') self.assertEqual((point.curve, point.x, point.y), (None, None, None)) with self.subTest('Encode and decode'): self.assertEqual(PrimePoint.decode(G.curve, G.encode()), G) with self.subTest('Bad point type'): with self.assertRaises(ValueError): decode_ec_point(0, b'\x05') with self.subTest('Bad point length'): with self.assertRaises(ValueError): decode_ec_point(G.curve.keylen, G.encode()[:-1]) def test_math(self): """Unit test native Python EC point math""" G, n = get_ec_curve_params(b'nistp256') G2, _ = get_ec_curve_params(b'nistp521') Inf = PrimePoint.construct(G.curve, None, None) with self.subTest('Add to infinity'): self.assertEqual(G + Inf, G) with self.subTest('Negate'): negG = -G self.assertEqual(-negG, G) with self.subTest('Negate infinity'): self.assertEqual(-Inf, Inf) with self.subTest('Multiply returning infinity'): self.assertEqual(n * G, Inf) with self.subTest('Add from different curves'): with self.assertRaises(ValueError): _ = G + G2 def test_ecdh(self): """Unit test native Python implementation of ECDH key exchange""" for curve_id in (b'nistp256', b'nistp384', b'nistp521'): client_priv = ECDH(curve_id) server_priv = ECDH(curve_id) client_pub = client_priv.get_public() server_pub = server_priv.get_public() client_k = client_priv.get_shared(server_pub) server_k = server_priv.get_shared(client_pub) self.assertEqual(client_k, server_k) asyncssh-1.3.0/tests/test_public_key.py000066400000000000000000001413361260630620200202540ustar00rootroot00000000000000# Copyright (c) 2014-2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Unit tests for reading and writing public and private keys Note: These tests assume that the openssl and ssh-keygen commands are available on the system and in the user's path. """ import binascii import importlib.util import os from .util import TempDirTestCase, run from asyncssh import import_private_key, import_public_key, import_certificate from asyncssh import read_private_key, read_public_key, read_certificate from asyncssh import read_private_key_list, read_public_key_list from asyncssh import read_certificate_list from asyncssh import KeyImportError, KeyExportError, KeyEncryptionError from asyncssh.asn1 import der_encode, BitString, ObjectIdentifier from asyncssh.asn1 import TaggedDERObject from asyncssh.packet import MPInt, String, UInt32, UInt64 from asyncssh.pbe import pkcs1_decrypt from asyncssh.public_key import CERT_TYPE_USER, CERT_TYPE_HOST, SSHKey from asyncssh.public_key import get_public_key_algs, get_certificate_algs bcrypt_available = importlib.util.find_spec('bcrypt') libnacl_available = importlib.util.find_spec('libnacl') _ES1_SHA1_DES = ObjectIdentifier('1.2.840.113549.1.5.10') _P12_RC4_40 = ObjectIdentifier('1.2.840.113549.1.12.1.2') _ES2 = ObjectIdentifier('1.2.840.113549.1.5.13') _ES2_PBKDF2 = ObjectIdentifier('1.2.840.113549.1.5.12') _ES2_AES128 = ObjectIdentifier('2.16.840.1.101.3.4.1.2') _ES2_DES3 = ObjectIdentifier('1.2.840.113549.3.7') # pylint: disable=bad-whitespace pkcs1_ciphers = (('aes128-cbc', '-aes128'), ('aes192-cbc', '-aes192'), ('aes256-cbc', '-aes256'), ('des-cbc', '-des'), ('des3-cbc', '-des3')) pkcs8_ciphers = (('des-cbc', 'md5', 1, '-v1 PBE-MD5-DES'), ('des-cbc', 'sha1', 1, '-v1 PBE-SHA1-DES'), ('des2-cbc', 'sha1', 1, '-v1 PBE-SHA1-2DES'), ('des3-cbc', 'sha1', 1, '-v1 PBE-SHA1-3DES'), ('rc4-40', 'sha1', 1, '-v1 PBE-SHA1-RC4-40'), ('rc4-128', 'sha1', 1, '-v1 PBE-SHA1-RC4-128'), ('aes128-cbc', 'sha1', 2, '-v2 aes-128-cbc'), ('aes128-cbc', 'sha224', 2, '-v2 aes-128-cbc ' '-v2prf hmacWithSHA224'), ('aes128-cbc', 'sha256', 2, '-v2 aes-128-cbc ' '-v2prf hmacWithSHA256'), ('aes128-cbc', 'sha384', 2, '-v2 aes-128-cbc ' '-v2prf hmacWithSHA384'), ('aes128-cbc', 'sha512', 2, '-v2 aes-128-cbc ' '-v2prf hmacWithSHA512'), ('aes192-cbc', 'sha1', 2, '-v2 aes-192-cbc'), ('aes256-cbc', 'sha1', 2, '-v2 aes-256-cbc'), ('blowfish-cbc', 'sha1', 2, '-v2 bf-cbc'), ('cast128-cbc', 'sha1', 2, '-v2 cast-cbc'), ('des-cbc', 'sha1', 2, '-v2 des-cbc'), ('des3-cbc', 'sha1', 2, '-v2 des-ede3-cbc')) openssh_ciphers = ('aes128-cbc', 'aes192-cbc', 'aes256-cbc', 'aes128-ctr', 'aes192-ctr', 'aes256-ctr', 'arcfour', 'arcfour128', 'arcfour256', 'blowfish-cbc', 'cast128-cbc', '3des-cbc') def select_passphrase(cipher, pbe_version=0): """Randomize between string and bytes version of passphrase""" if cipher is None: return None elif os.urandom(1)[0] & 1: return 'passphrase' elif pbe_version == 1 and cipher in ('des2-cbc', 'des3-cbc', 'rc4-40', 'rc4-128'): return 'passphrase'.encode('utf-16-be') else: return 'passphrase'.encode('utf-8') # pylint: enable=bad-whitespace if run('ssh -V') >= b'OpenSSH_6.9': # pragma: no branch # GCM & Chacha tests require OpenSSH 6.9 due to a bug in earlier versions: # https://bugzilla.mindrot.org/show_bug.cgi?id=2366 openssh_ciphers = openssh_ciphers + ('aes128-gcm@openssh.com', 'aes256-gcm@openssh.com', 'chacha20-poly1305@openssh.com') class _TestPublicKey(TempDirTestCase): """Unit tests for public key modules""" keyclass = None keytypes = () base_format = None private_formats = () public_formats = () default_cert_version = '' cert_versions = () def __init__(self, methodName='runTest'): super().__init__(methodName) self.privkey = None self.pubkey = None self.privca = None self.pubca = None def make_keypair(self, privfile, pubfile, keytype): """Method to make a keypair defined by subclasses""" raise NotImplementedError def encode_options(self, options): """Encode SSH certificate critical options and extensions""" # pylint: disable=no-self-use return b''.join((String(k) + String(v) for k, v in options.items())) def make_certificate(self, cert_type, key, signing_key, principals, valid_after=0, valid_before=0xffffffffffffffff, options=None, extensions=None, bad_signature=False): """Construct an SSH certificate""" keydata = key.encode_ssh_public() principals = b''.join((String(p) for p in principals)) options = self.encode_options(options) if options else b'' extensions = self.encode_options(extensions) if extensions else b'' signing_keydata = b''.join((String(signing_key.algorithm), signing_key.encode_ssh_public())) data = b''.join((String(self.default_cert_version), String(os.urandom(8)), keydata, UInt64(0), UInt32(cert_type), String(''), String(principals), UInt64(valid_after), UInt64(valid_before), String(options), String(extensions), String(''), String(signing_keydata))) if bad_signature: data += String('') else: data += String(signing_key.sign(data)) return b''.join((self.default_cert_version.encode('ascii'), b' ', binascii.b2a_base64(data))) def check_private(self, passphrase=None): """Check for a private key match""" newkey = read_private_key('new', passphrase) self.assertEqual(newkey, self.privkey) self.assertEqual(hash(newkey), hash(self.privkey)) if passphrase: with self.assertRaises((KeyEncryptionError, KeyImportError)): read_private_key('new', 'xxx') else: run('cat new new > list') keylist = read_private_key_list('list', passphrase) self.assertEqual(keylist[0], newkey) self.assertEqual(keylist[1], newkey) def check_public(self): """Check for a public key match""" newkey = read_public_key('new') self.assertEqual(newkey, self.pubkey) self.assertEqual(hash(newkey), hash(self.pubkey)) run('cat new new > list') keylist = read_public_key_list('list') self.assertEqual(keylist[0], newkey) self.assertEqual(keylist[1], newkey) def import_pkcs1_private(self, fmt, cipher=None, args=None): """Check import of a PKCS#1 private key""" if cipher: run('openssl %s %s -in priv -inform pem -out new -outform %s ' '-passout pass:passphrase' % (self.keyclass, args, fmt)) else: run('openssl %s -in priv -inform pem -out new -outform %s' % (self.keyclass, fmt)) self.check_private(select_passphrase(cipher)) def export_pkcs1_private(self, fmt, cipher=None): """Check export of a PKCS#1 private key""" self.privkey.write_private_key('privout', 'pkcs1-%s' % fmt, select_passphrase(cipher), cipher) if cipher: run('openssl %s -in privout -inform %s -out new -outform pem ' '-passin pass:passphrase' % (self.keyclass, fmt)) else: run('openssl %s -in privout -inform %s -out new -outform pem' % (self.keyclass, fmt)) self.check_private() def import_pkcs1_public(self, fmt): """Check import of a PKCS#1 public key""" if self.keyclass == 'dsa': # OpenSSL no longer has support for PKCS#1 DSA, so we can # only test against ourselves. self.pubkey.write_public_key('new', 'pkcs1-%s' % fmt) else: run('openssl %s -pubin -in pub -inform pem -RSAPublicKey_out ' '-out new -outform %s' % (self.keyclass, fmt)) self.check_public() def export_pkcs1_public(self, fmt): """Check export of a PKCS#1 public key""" self.privkey.write_public_key('pubout', 'pkcs1-%s' % fmt) if self.keyclass == 'dsa': # OpenSSL no longer has support for PKCS#1 DSA, so we can # only test against ourselves. read_public_key('pubout').write_public_key('new', 'pkcs1-%s' % fmt) else: run('openssl %s -RSAPublicKey_in -in pubout -inform %s -out new ' '-outform pem' % (self.keyclass, fmt)) self.check_public() def import_pkcs8_private(self, fmt, cipher=None, pbe_version=None, args=None): """Check import of a PKCS#8 private key""" if cipher: run('openssl pkcs8 -topk8 %s -in priv -inform pem -out new ' '-outform %s -passout pass:passphrase' % (args, fmt)) else: run('openssl pkcs8 -topk8 -nocrypt -in priv -inform pem -out new ' '-outform %s' % fmt) self.check_private(select_passphrase(cipher, pbe_version)) def export_pkcs8_private(self, fmt, cipher=None, hash_alg=None, pbe_version=None): """Check export of a PKCS#8 private key""" self.privkey.write_private_key('privout', 'pkcs8-%s' % fmt, select_passphrase(cipher, pbe_version), cipher, hash_alg, pbe_version) if cipher: run('openssl pkcs8 -in privout -inform %s -out new ' '-outform pem -passin pass:passphrase' % fmt) else: run('openssl pkcs8 -nocrypt -in privout -inform %s -out new ' '-outform pem' % fmt) self.check_private() def import_pkcs8_public(self, fmt): """Check import of a PKCS#8 public key""" run('openssl %s -pubin -in pub -inform pem -out new -outform %s' % (self.keyclass, fmt)) self.check_public() def export_pkcs8_public(self, fmt): """Check export of a PKCS#8 public key""" self.privkey.write_public_key('pubout', 'pkcs8-%s' % fmt) run('openssl %s -pubin -in pubout -inform %s -out new -outform pem' % (self.keyclass, fmt)) self.check_public() def import_openssh_private(self, cipher=None): """Check import of an OpenSSH private key""" run('cp -p priv new') if cipher: run('ssh-keygen -p -N passphrase -Z %s -o -f new' % cipher) else: run('ssh-keygen -p -N "" -o -f new') self.check_private(select_passphrase(cipher)) def export_openssh_private(self, cipher=None): """Check export of an OpenSSH private key""" self.privkey.write_private_key('new', 'openssh', select_passphrase(cipher), cipher) run('chmod 600 new') if cipher: run('ssh-keygen -p -P passphrase -N "" -o -f new') else: run('ssh-keygen -p -N "" -o -f new') self.check_private() def import_openssh_public(self): """Check import of an OpenSSH public key""" run('cp -p sshpub new') self.check_public() def export_openssh_public(self): """Check export of an OpenSSH public key""" self.privkey.write_public_key('pubout', 'openssh') run('ssh-keygen -e -f pubout -m rfc4716 > new') self.check_public() def import_rfc4716_public(self): """Check import of an RFC4716 public key""" run('ssh-keygen -e -f sshpub -m rfc4716 > new') self.check_public() def export_rfc4716_public(self): """Check export of an RFC4716 public key""" self.privkey.write_public_key('pubout', 'rfc4716') run('ssh-keygen -i -f pubout -m rfc4716 > new') self.check_public() def check_encode_errors(self): """Check error code paths in key encoding""" for fmt in ('pkcs1-der', 'pkcs1-pem', 'pkcs8-der', 'pkcs8-pem', 'openssh', 'rfc4716', 'xxx'): with self.subTest('Encode private from public (%s)' % fmt): with self.assertRaises(KeyExportError): self.pubkey.export_private_key(fmt) with self.subTest('Encode with unknown key format'): with self.assertRaises(KeyExportError): self.privkey.export_public_key('xxx') with self.subTest('Encode encrypted pkcs1-der'): with self.assertRaises(KeyExportError): self.privkey.export_private_key('pkcs1-der', 'x') if self.keyclass == 'ec': with self.subTest('Encode EC public key with PKCS#1'): with self.assertRaises(KeyExportError): self.privkey.export_public_key('pkcs1-pem') if 'pkcs1' in self.private_formats: with self.subTest('Encode with unknown PKCS#1 cipher'): with self.assertRaises(KeyEncryptionError): self.privkey.export_private_key('pkcs1-pem', 'x', 'xxx') if 'pkcs8' in self.private_formats: with self.subTest('Encode with unknown PKCS#8 cipher'): with self.assertRaises(KeyEncryptionError): self.privkey.export_private_key('pkcs8-pem', 'x', 'xxx') with self.subTest('Encode with unknown PKCS#8 hash'): with self.assertRaises(KeyEncryptionError): self.privkey.export_private_key('pkcs8-pem', 'x', 'aes128-cbc', 'xxx') with self.subTest('Encode with unknown PKCS#8 version'): with self.assertRaises(KeyEncryptionError): self.privkey.export_private_key('pkcs8-pem', 'x', 'aes128-cbc', 'sha1', 3) if 'openssh' in self.private_formats: # pragma: no branch with self.subTest('Encode with unknown openssh cipher'): with self.assertRaises(KeyEncryptionError): self.privkey.export_private_key('openssh', 'x', 'xxx') def check_decode_errors(self): """Check error code paths in key decoding""" private_errors = [ ('Non-ASCII', '\xff'), ('Incomplete ASN.1', b''), ('Invalid PKCS#1', der_encode(None)), ('Invalid PKCS#1 params', der_encode((1, b'', TaggedDERObject(0, b'')))), ('Invalid PKCS#1 EC named curve OID', der_encode((1, b'', TaggedDERObject(0, ObjectIdentifier('1.1'))))), ('Invalid PKCS#8', der_encode((0, (self.privkey.pkcs8_oid, ()), der_encode(None)))), ('Invalid PKCS#8 ASN.1', der_encode((0, (self.privkey.pkcs8_oid, None), b''))), ('Invalid PKCS#8 params', der_encode((1, (self.privkey.pkcs8_oid, b''), der_encode((1, b''))))), ('Invalid PEM header', b'-----BEGIN XXX-----\n'), ('Missing PEM footer', b'-----BEGIN PRIVATE KEY-----\n'), ('Invalid PEM key type', b'-----BEGIN XXX PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode(None)) + b'-----END XXX PRIVATE KEY-----'), ('Invalid PEM Base64', b'-----BEGIN PRIVATE KEY-----\n' b'X\n' b'-----END PRIVATE KEY-----'), ('Missing PKCS#1 passphrase', b'-----BEGIN DSA PRIVATE KEY-----\n' b'Proc-Type: 4,ENCRYPTED\n' b'-----END DSA PRIVATE KEY-----'), ('Incomplete PEM ASN.1', b'-----BEGIN PRIVATE KEY-----\n' b'-----END PRIVATE KEY-----'), ('Missing PEM PKCS#8 passphrase', b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode(None)) + b'-----END ENCRYPTED PRIVATE KEY-----'), ('Invalid PEM PKCS#1 key', b'-----BEGIN DSA PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode(None)) + b'-----END DSA PRIVATE KEY-----'), ('Invalid PEM PKCS#8 key', b'-----BEGIN PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode(None)) + b'-----END PRIVATE KEY-----'), ('Unknown format OpenSSH key', b'-----BEGIN OPENSSH PRIVATE KEY-----\n' + binascii.b2a_base64(b'XXX') + b'-----END OPENSSH PRIVATE KEY-----'), ('Incomplete OpenSSH key', b'-----BEGIN OPENSSH PRIVATE KEY-----\n' + binascii.b2a_base64(b'openssh-key-v1\0') + b'-----END OPENSSH PRIVATE KEY-----'), ('Invalid OpenSSH nkeys', b'-----BEGIN OPENSSH PRIVATE KEY-----\n' + binascii.b2a_base64(b''.join( (b'openssh-key-v1\0', String(''), String(''), String(''), UInt32(2), String(''), String('')))) + b'-----END OPENSSH PRIVATE KEY-----'), ('Missing OpenSSH passphrase', b'-----BEGIN OPENSSH PRIVATE KEY-----\n' + binascii.b2a_base64(b''.join( (b'openssh-key-v1\0', String('xxx'), String(''), String(''), UInt32(1), String(''), String('')))) + b'-----END OPENSSH PRIVATE KEY-----'), ('Mismatched OpenSSH check bytes', b'-----BEGIN OPENSSH PRIVATE KEY-----\n' + binascii.b2a_base64(b''.join( (b'openssh-key-v1\0', String('none'), String(''), String(''), UInt32(1), String(''), String(b''.join((UInt32(1), UInt32(2))))))) + b'-----END OPENSSH PRIVATE KEY-----'), ('Invalid OpenSSH algorithm', b'-----BEGIN OPENSSH PRIVATE KEY-----\n' + binascii.b2a_base64(b''.join( (b'openssh-key-v1\0', String('none'), String(''), String(''), UInt32(1), String(''), String(b''.join((UInt32(1), UInt32(1), String('xxx'))))))) + b'-----END OPENSSH PRIVATE KEY-----'), ('Invalid OpenSSH pad', b'-----BEGIN OPENSSH PRIVATE KEY-----\n' + binascii.b2a_base64(b''.join( (b'openssh-key-v1\0', String('none'), String(''), String(''), UInt32(1), String(''), String(b''.join((UInt32(1), UInt32(1), String('ssh-dss'), 5*MPInt(0), String(''), b'\0')))))) + b'-----END OPENSSH PRIVATE KEY-----') ] decrypt_errors = [ ('Invalid PKCS#1', der_encode(None)), ('Invalid PKCS#8', der_encode((0, (self.privkey.pkcs8_oid, ()), der_encode(None)))), ('Invalid PEM params', b'-----BEGIN DSA PRIVATE KEY-----\n' b'Proc-Type: 4,ENCRYPTED\n' b'DEK-Info: XXX\n' b'-----END DSA PRIVATE KEY-----'), ('Invalid PEM cipher', b'-----BEGIN DSA PRIVATE KEY-----\n' b'Proc-Type: 4,ENCRYPTED\n' b'DEK-Info: XXX,00\n' b'-----END DSA PRIVATE KEY-----'), ('Invalid PEM IV', b'-----BEGIN DSA PRIVATE KEY-----\n' b'Proc-Type: 4,ENCRYPTED\n' b'DEK-Info: AES-256-CBC,XXX\n' b'-----END DSA PRIVATE KEY-----'), ('Invalid PEM PKCS#8 encrypted data', b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode(None)) + b'-----END ENCRYPTED PRIVATE KEY-----'), ('Invalid PEM PKCS#8 encrypted header', b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode((None, None))) + b'-----END ENCRYPTED PRIVATE KEY-----'), ('Invalid PEM PKCS#8 encryption algorithm', b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode(((None, None), b''))) + b'-----END ENCRYPTED PRIVATE KEY-----'), ('Invalid PEM PKCS#8 PBES1 encryption parameters', b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode(((_ES1_SHA1_DES, None), b''))) + b'-----END ENCRYPTED PRIVATE KEY-----'), ('Invalid PEM PKCS#8 PBES1 PKCS#12 encryption parameters', b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode(((_P12_RC4_40, None), b''))) + b'-----END ENCRYPTED PRIVATE KEY-----'), ('Invalid PEM PKCS#8 PBES1 PKCS#12 salt', b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode(((_P12_RC4_40, (b'', 0)), b''))) + b'-----END ENCRYPTED PRIVATE KEY-----'), ('Invalid PEM PKCS#8 PBES1 PKCS#12 iteration count', b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode(((_P12_RC4_40, (b'x', 0)), b''))) + b'-----END ENCRYPTED PRIVATE KEY-----'), ('Invalid PEM PKCS#8 PBES2 encryption parameters', b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode(((_ES2, None), b''))) + b'-----END ENCRYPTED PRIVATE KEY-----'), ('Invalid PEM PKCS#8 PBES2 KDF algorithm', b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode( ((_ES2, ((None, None), (None, None))), b''))) + b'-----END ENCRYPTED PRIVATE KEY-----'), ('Invalid PEM PKCS#8 PBES2 encryption algorithm', b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode( ((_ES2, ((_ES2_PBKDF2, None), (None, None))), b''))) + b'-----END ENCRYPTED PRIVATE KEY-----'), ('Invalid PEM PKCS#8 PBES2 PBKDF2 parameters', b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode( ((_ES2, ((_ES2_PBKDF2, None), (_ES2_AES128, None))), b''))) + b'-----END ENCRYPTED PRIVATE KEY-----'), ('Invalid PEM PKCS#8 PBES2 PBKDF2 salt', b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode( ((_ES2, ((_ES2_PBKDF2, (None, None)), (_ES2_AES128, None))), b''))) + b'-----END ENCRYPTED PRIVATE KEY-----'), ('Invalid PEM PKCS#8 PBES2 PBKDF2 iteration count', b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode( ((_ES2, ((_ES2_PBKDF2, (b'', None)), (_ES2_AES128, None))), b''))) + b'-----END ENCRYPTED PRIVATE KEY-----'), ('Invalid PEM PKCS#8 PBES2 PBKDF2 PRF', b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode( ((_ES2, ((_ES2_PBKDF2, (b'', 0, None)), (_ES2_AES128, None))), b''))) + b'-----END ENCRYPTED PRIVATE KEY-----'), ('Unknown PEM PKCS#8 PBES2 PBKDF2 PRF', b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode( ((_ES2, ((_ES2_PBKDF2, (b'', 0, (ObjectIdentifier('1.1'), None))), (_ES2_AES128, None))), b''))) + b'-----END ENCRYPTED PRIVATE KEY-----'), ('Invalid PEM PKCS#8 PBES2 encryption parameters', b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode( ((_ES2, ((_ES2_PBKDF2, (b'', 0)), (_ES2_AES128, None))), b''))) + b'-----END ENCRYPTED PRIVATE KEY-----'), ('Invalid length PEM PKCS#8 PBES2 IV', b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + binascii.b2a_base64(der_encode( ((_ES2, ((_ES2_PBKDF2, (b'', 0)), (_ES2_AES128, b''))), b''))) + b'-----END ENCRYPTED PRIVATE KEY-----'), ('Invalid OpenSSH cipher', b'-----BEGIN OPENSSH PRIVATE KEY-----\n' + binascii.b2a_base64(b''.join( (b'openssh-key-v1\0', String('xxx'), String(''), String(''), UInt32(1), String(''), String('')))) + b'-----END OPENSSH PRIVATE KEY-----'), ('Invalid OpenSSH kdf', b'-----BEGIN OPENSSH PRIVATE KEY-----\n' + binascii.b2a_base64(b''.join( (b'openssh-key-v1\0', String('aes256-cbc'), String('xxx'), String(''), UInt32(1), String(''), String('')))) + b'-----END OPENSSH PRIVATE KEY-----'), ('Invalid OpenSSH kdf data', b'-----BEGIN OPENSSH PRIVATE KEY-----\n' + binascii.b2a_base64(b''.join( (b'openssh-key-v1\0', String('aes256-cbc'), String('bcrypt'), String(''), UInt32(1), String(''), String('')))) + b'-----END OPENSSH PRIVATE KEY-----'), ('Invalid OpenSSH salt', b'-----BEGIN OPENSSH PRIVATE KEY-----\n' + binascii.b2a_base64(b''.join( (b'openssh-key-v1\0', String('aes256-cbc'), String('bcrypt'), String(b''.join((String(b''), UInt32(1)))), UInt32(1), String(''), String('')))) + b'-----END OPENSSH PRIVATE KEY-----'), ('Invalid OpenSSH encrypted data', b'-----BEGIN OPENSSH PRIVATE KEY-----\n' + binascii.b2a_base64(b''.join( (b'openssh-key-v1\0', String('aes256-cbc'), String('bcrypt'), String(b''.join((String(16*b'\0'), UInt32(1)))), UInt32(1), String(''), String('')))) + b'-----END OPENSSH PRIVATE KEY-----'), ('Unexpected OpenSSH trailing data', b'-----BEGIN OPENSSH PRIVATE KEY-----\n' + binascii.b2a_base64(b''.join( (b'openssh-key-v1\0', String('aes256-cbc'), String('bcrypt'), String(b''.join((String(16*b'\0'), UInt32(1)))), UInt32(1), String(''), String(''), String('xxx')))) + b'-----END OPENSSH PRIVATE KEY-----') ] public_errors = [ ('Non-ASCII', '\xff'), ('Incomplete ASN.1', b''), ('Invalid ASN.1', b'\x30'), ('Invalid PKCS#1', der_encode(None)), ('Invalid PKCS#8', der_encode(((self.pubkey.pkcs8_oid, ()), BitString(der_encode(None))))), ('Invalid PKCS#8 ASN.1', der_encode(((self.pubkey.pkcs8_oid, None), BitString(b'')))), ('Invalid PEM header', b'-----BEGIN XXX-----\n'), ('Missing PEM footer', b'-----BEGIN PUBLIC KEY-----\n'), ('Invalid PEM key type', b'-----BEGIN XXX PUBLIC KEY-----\n' + binascii.b2a_base64(der_encode(None)) + b'-----END XXX PUBLIC KEY-----'), ('Invalid PEM Base64', b'-----BEGIN PUBLIC KEY-----\n' b'X\n' b'-----END PUBLIC KEY-----'), ('Incomplete PEM ASN.1', b'-----BEGIN PUBLIC KEY-----\n' b'-----END PUBLIC KEY-----'), ('Invalid PKCS#1 key data', b'-----BEGIN DSA PUBLIC KEY-----\n' + binascii.b2a_base64(der_encode(None)) + b'-----END DSA PUBLIC KEY-----'), ('Invalid PKCS#8 key data', b'-----BEGIN PUBLIC KEY-----\n' + binascii.b2a_base64(der_encode(None)) + b'-----END PUBLIC KEY-----'), ('Invalid OpenSSH', b'xxx'), ('Invalid OpenSSH Base64', b'ssh-dss X'), ('Unknown OpenSSH algorithm', b'ssh-dss ' + binascii.b2a_base64(String('xxx'))), ('Invalid OpenSSH body', b'ssh-dss ' + binascii.b2a_base64(String('ssh-dss'))), ('Invalid RFC4716 header', b'---- XXX ----\n'), ('Missing RFC4716 footer', b'---- BEGIN SSH2 PUBLIC KEY ----\n'), ('Invalid RFC4716 header', b'---- BEGIN SSH2 PUBLIC KEY ----\n' b'XXX:\\\n' b'---- END SSH2 PUBLIC KEY ----\n'), ('Invalid RFC4716 Base64', b'---- BEGIN SSH2 PUBLIC KEY ----\n' b'X\n' b'---- END SSH2 PUBLIC KEY ----\n') ] for fmt, data in private_errors: with self.subTest('Decode private (%s)' % fmt): with self.assertRaises(KeyImportError): import_private_key(data) for fmt, data in decrypt_errors: with self.subTest('Decrypt private (%s)' % fmt): with self.assertRaises((KeyImportError, KeyEncryptionError)): import_private_key(data, 'x') for fmt, data in public_errors: with self.subTest('Decode public (%s)' % fmt): with self.assertRaises(KeyImportError): import_public_key(data) def check_sshkey_base_errors(self): """Check SSHKey base class errors""" key = SSHKey() with self.subTest('SSHKey base class errors'): with self.assertRaises(KeyExportError): key.encode_pkcs1_private() with self.assertRaises(KeyExportError): key.encode_pkcs1_public() with self.assertRaises(KeyExportError): key.encode_pkcs8_private() with self.assertRaises(KeyExportError): key.encode_pkcs8_public() with self.assertRaises(KeyExportError): key.encode_ssh_private() with self.assertRaises(KeyExportError): key.encode_ssh_public() def check_sign_and_verify(self): """Check key signing and verification""" with self.subTest('Sign/verify test'): pubkey = read_public_key('pub') data = os.urandom(8) sig = self.privkey.sign(data) with self.subTest('Good signature'): self.assertTrue(pubkey.verify(data, sig)) badsig = bytearray(sig) badsig[-1] ^= 0xff badsig = bytes(badsig) with self.subTest('Bad signature'): self.assertFalse(pubkey.verify(data, badsig)) with self.subTest('Empty signature'): self.assertFalse(pubkey.verify(data, String(pubkey.algorithm) + String(b''))) badalg = String('xxx') with self.subTest('Bad algorithm'): self.assertFalse(pubkey.verify(data, badalg)) with self.subTest('Sign with public key'): with self.assertRaises(ValueError): pubkey.sign(data) def check_pkcs1_private(self): """Check PKCS#1 private key format""" with self.subTest('Import PKCS#1 PEM private'): self.import_pkcs1_private('pem') with self.subTest('Export PKCS#1 PEM private'): self.export_pkcs1_private('pem') with self.subTest('Import PKCS#1 DER private'): self.import_pkcs1_private('der') with self.subTest('Export PKCS#1 DER private'): self.export_pkcs1_private('der') for cipher, args in pkcs1_ciphers: with self.subTest('Import PKCS#1 PEM private (%s)' % cipher): self.import_pkcs1_private('pem', cipher, args) with self.subTest('Export PKCS#1 PEM private (%s)' % cipher): self.export_pkcs1_private('pem', cipher) def check_pkcs1_public(self): """Check PKCS#1 public key format""" with self.subTest('Import PKCS#1 PEM public'): self.import_pkcs1_public('pem') with self.subTest('Export PKCS#1 PEM public'): self.export_pkcs1_public('pem') with self.subTest('Import PKCS#1 DER public'): self.import_pkcs1_public('der') with self.subTest('Export PKCS#1 DER public'): self.export_pkcs1_public('der') def check_pkcs8_private(self): """Check PKCS#8 private key format""" with self.subTest('Import PKCS#8 PEM private'): self.import_pkcs8_private('pem') with self.subTest('Export PKCS#8 PEM private'): self.export_pkcs8_private('pem') with self.subTest('Import PKCS#8 DER private'): self.import_pkcs8_private('der') with self.subTest('Export PKCS#8 DER private'): self.export_pkcs8_private('der') for cipher, hash_alg, pbe_version, args in pkcs8_ciphers: with self.subTest('Import PKCS#8 PEM private (%s-%s-v%s)' % (cipher, hash_alg, pbe_version)): self.import_pkcs8_private('pem', cipher, pbe_version, args) with self.subTest('Export PKCS#8 PEM private (%s-%s-v%s)' % (cipher, hash_alg, pbe_version)): self.export_pkcs8_private('pem', cipher, hash_alg, pbe_version) with self.subTest('Import PKCS#8 DER private (%s-%s-v%s)' % (cipher, hash_alg, pbe_version)): self.import_pkcs8_private('der', cipher, pbe_version, args) with self.subTest('Export PKCS#8 DER private (%s-%s-v%s)' % (cipher, hash_alg, pbe_version)): self.export_pkcs8_private('der', cipher, hash_alg, pbe_version) def check_pkcs8_public(self): """Check PKCS#8 public key format""" with self.subTest('Import PKCS#8 PEM public'): self.import_pkcs8_public('pem') with self.subTest('Export PKCS#8 PEM public'): self.export_pkcs8_public('pem') with self.subTest('Import PKCS#8 DER public'): self.import_pkcs8_public('der') with self.subTest('Export PKCS#8 DER public'): self.export_pkcs8_public('der') def check_openssh_private(self): """Check OpenSSH private key format""" with self.subTest('Import OpenSSH private'): self.import_openssh_private() with self.subTest('Export OpenSSH private'): self.export_openssh_private() if bcrypt_available: # pragma: no branch for cipher in openssh_ciphers: with self.subTest('Import OpenSSH private (%s)' % cipher): self.import_openssh_private(cipher) with self.subTest('Export OpenSSH private (%s)' % cipher): self.export_openssh_private(cipher) def check_openssh_public(self): """Check OpenSSH public key format""" with self.subTest('Import OpenSSH public'): self.import_openssh_public() with self.subTest('Export OpenSSH public'): self.export_openssh_public() def check_rfc4716_public(self): """Check RFC4716 public key format""" with self.subTest('Import RFC4716 public'): self.import_rfc4716_public() with self.subTest('Export RFC4716 public'): self.export_rfc4716_public() def check_certificate(self, cert_type, version, fmt): """Check SSH certificate import""" with self.subTest('Import certificate'): typearg = '-h ' if cert_type == CERT_TYPE_HOST else '' if version: version = '-t ' + version + ' ' if cert_type == CERT_TYPE_USER: options = '-O force-command=xxx -O source-address=127.0.0.1 ' else: options = '' run('ssh-keygen -s privca %s%s%s-I name sshpub' % (typearg, version, options)) if fmt == 'openssh': run('mv sshpub-cert.pub cert') else: run('ssh-keygen -e -m %s -f sshpub-cert.pub > cert' % fmt) cert = read_certificate('cert') self.assertEqual(cert.key, self.pubkey) with self.subTest('Validate certificate'): self.assertIsNone(cert.validate(cert_type, 'name')) with self.subTest('Import certificate list'): run('cat cert cert > list') certlist = read_certificate_list('list') self.assertEqual(certlist[0].key, cert.key) self.assertEqual(certlist[1].key, cert.key) def check_certificate_errors(self, cert_type): """Check SSH certificate error cases""" with self.subTest('Non-ASCII certificate'): with self.assertRaises(KeyImportError): import_certificate('\u0080\n') with self.subTest('Invalid SSH format'): with self.assertRaises(KeyImportError): import_certificate('xxx\n') with self.subTest('Invalid certificate packetization'): with self.assertRaises(KeyImportError): import_certificate(b'xxx ' + binascii.b2a_base64(b'\x00')) with self.subTest('Invalid certificate algorithm'): with self.assertRaises(KeyImportError): import_certificate(b'xxx ' + binascii.b2a_base64(String(b'xxx'))) with self.subTest('Invalid certificate critical option'): with self.assertRaises(KeyImportError): cert = self.make_certificate(cert_type, self.pubkey, self.privca, 'name', options={b'xxx': b''}) import_certificate(cert) with self.subTest('Ignored certificate extension'): cert = self.make_certificate(cert_type, self.pubkey, self.privca, 'name', extensions={b'xxx': b''}) self.assertIsNotNone(import_certificate(cert)) with self.subTest('Invalid certificate signature'): with self.assertRaises(KeyImportError): cert = self.make_certificate(cert_type, self.pubkey, self.privca, 'name', bad_signature=True) import_certificate(cert) with self.subTest('Invalid characters in certificate principal'): with self.assertRaises(KeyImportError): cert = self.make_certificate(cert_type, self.pubkey, self.privca, (b'\xff',)) import_certificate(cert) if cert_type == CERT_TYPE_USER: with self.subTest('Invalid characters in force-command'): with self.assertRaises(KeyImportError): cert = self.make_certificate(cert_type, self.pubkey, self.privca, ('name',), options={'force-command': String(b'\xff')}) import_certificate(cert) with self.subTest('Invalid characters in source-address'): with self.assertRaises(KeyImportError): cert = self.make_certificate(cert_type, self.pubkey, self.privca, ('name',), options={'source-address': String(b'\xff')}) import_certificate(cert) with self.subTest('Invalid IP network in source-address'): with self.assertRaises(KeyImportError): cert = self.make_certificate(cert_type, self.pubkey, self.privca, ('name',), options={'source-address': String('1.1.1.256')}) import_certificate(cert) with self.subTest('Invalid certificate type'): with self.assertRaises(KeyImportError): cert = self.make_certificate(0, self.pubkey, self.privca, ('name',)) import_certificate(cert) with self.subTest('Mismatched certificate type'): with self.assertRaises(ValueError): cert = self.make_certificate(cert_type, self.pubkey, self.privca, ('name',)) cert = import_certificate(cert) cert.validate(cert_type ^ 3, 'name') with self.subTest('Certificate not yet valid'): with self.assertRaises(ValueError): cert = self.make_certificate(cert_type, self.pubkey, self.privca, ('name',), valid_after=0xffffffffffffffff) cert = import_certificate(cert) cert.validate(cert_type, 'name') with self.subTest('Certificate expired'): with self.assertRaises(ValueError): cert = self.make_certificate(cert_type, self.pubkey, self.privca, ('name',), valid_before=0) cert = import_certificate(cert) cert.validate(cert_type, 'name') with self.subTest('Certificate principal mismatch'): with self.assertRaises(ValueError): cert = self.make_certificate(cert_type, self.pubkey, self.privca, ('name',)) cert = import_certificate(cert) cert.validate(cert_type, 'name2') def test_key(self): """Check key import and export""" for keytype in self.keytypes: with self.subTest(keytype=keytype): self.make_keypair('priv', 'pub', keytype) self.make_keypair('privca', 'pubca', keytype) run('chmod 600 priv privca') if self.base_format == 'openssh': run('cp -p pub sshpub') else: run('ssh-keygen -i -f pub -m %s > sshpub' % self.base_format) self.privkey = read_private_key('priv') self.pubkey = read_public_key('pub') self.privca = read_private_key('privca') self.pubca = read_public_key('pubca') self.check_encode_errors() self.check_decode_errors() self.check_sshkey_base_errors() self.check_sign_and_verify() if 'pkcs1' in self.private_formats: self.check_pkcs1_private() if 'pkcs1' in self.public_formats: self.check_pkcs1_public() if 'pkcs8' in self.private_formats: self.check_pkcs8_private() if 'pkcs8' in self.public_formats: self.check_pkcs8_public() if 'openssh' in self.private_formats: # pragma: no branch self.check_openssh_private() if 'openssh' in self.public_formats: # pragma: no branch self.check_openssh_public() if 'rfc4716' in self.public_formats: # pragma: no branch self.check_rfc4716_public() for cert_type in (CERT_TYPE_USER, CERT_TYPE_HOST): for version in self.cert_versions: for fmt in ('openssh', 'rfc4716'): with self.subTest(cert_type=cert_type, version=version, fmt=fmt): self.check_certificate(cert_type, version, fmt) self.check_certificate_errors(cert_type) class TestDSA(_TestPublicKey): """Test DSA public keys""" keyclass = 'dsa' keytypes = (1024,) base_format = 'pkcs8' private_formats = ('pkcs1', 'pkcs8', 'openssh') public_formats = ('pkcs1', 'pkcs8', 'openssh', 'rfc4716') default_cert_version = 'ssh-dss-cert-v01@openssh.com' cert_versions = ('ssh-dss-cert-v00@openssh.com', '') def make_keypair(self, privfile, pubfile, keytype): """Make a DSA key pair""" # pylint: disable=no-self-use run('openssl dsaparam -out %s -noout -genkey %s' % (privfile, keytype)) run('openssl dsa -pubout -in %s -out %s' % (privfile, pubfile)) class TestRSA(_TestPublicKey): """Test RSA public keys""" keyclass = 'rsa' keytypes = (1024, 2048) base_format = 'pkcs8' private_formats = ('pkcs1', 'pkcs8', 'openssh') public_formats = ('pkcs1', 'pkcs8', 'openssh', 'rfc4716') default_cert_version = 'ssh-rsa-cert-v01@openssh.com' cert_versions = ('ssh-rsa-cert-v00@openssh.com', '') def make_keypair(self, privfile, pubfile, keytype): """Make an RSA key pair""" # pylint: disable=no-self-use run('openssl genrsa -out %s %s' % (privfile, keytype)) run('openssl rsa -pubout -in %s -out %s' % (privfile, pubfile)) class TestEC(_TestPublicKey): """Test elliptic curve public keys""" keyclass = 'ec' keytypes = ('secp256r1', 'secp384r1', 'secp521r1') base_format = 'pkcs8' private_formats = ('pkcs1', 'pkcs8', 'openssh') public_formats = ('pkcs8', 'openssh', 'rfc4716') cert_versions = ('',) @property def default_cert_version(self): """Return default SSH certificate version""" return self.privkey.algorithm.decode('ascii') + '-cert-v01@openssh.com' def make_keypair(self, privfile, pubfile, keytype): """Make an elliptic curve key pair""" # pylint: disable=no-self-use run('openssl ecparam -out %s -noout -genkey -name %s' % (privfile, keytype)) run('openssl ec -pubout -in %s -out %s' % (privfile, pubfile)) if libnacl_available: # pragma: no branch class TestEd25519(_TestPublicKey): """Test Ed25519 public keys""" keyclass = 'ed25519' keytypes = (256,) base_format = 'openssh' private_formats = ('openssh') public_formats = ('openssh', 'rfc4716') default_cert_version = 'ssh-ed25519-cert-v01@openssh.com' cert_versions = ('',) def make_keypair(self, privfile, pubfile, keytype): """Make an Ed25519 key pair""" # pylint: disable=no-self-use,unused-argument run('ssh-keygen -t ed25519 -N "" -f %s' % privfile) run('mv %s.pub %s' % (privfile, pubfile)) del _TestPublicKey class _TestPublicKeyTopLevel(TempDirTestCase): """Top-level public key module tests""" def test_public_key(self): """Test public key top-level functions""" self.assertIsNotNone(get_public_key_algs()) self.assertIsNotNone(get_certificate_algs()) def test_pad_error(self): """Test for missing RFC 1423 padding on PBE decrypt""" with self.assertRaises(KeyEncryptionError): pkcs1_decrypt(b'', b'AES-128-CBC', os.urandom(16), 'x') def test_ec_explicit(self): """Test EC certificate with explcit parameters""" with self.subTest('Import EC key with explicit parameters'): run('openssl ecparam -out priv -noout -genkey -name secp256r1 ' '-param_enc explicit') read_private_key('priv') with self.subTest('Import EC key with unknown explicit parameters'): run('openssl ecparam -out priv -noout -genkey -name secp112r1 ' '-param_enc explicit') with self.assertRaises(KeyImportError): read_private_key('priv') asyncssh-1.3.0/tests/test_saslprep.py000066400000000000000000000057261260630620200177610ustar00rootroot00000000000000# Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Unit tests for SASL string preparation""" import unittest from asyncssh.saslprep import saslprep, SASLPrepError class _TestSASLPrep(unittest.TestCase): """Unit tests for saslprep module""" def test_nonstring(self): """Test passing a non-string value""" with self.assertRaises(TypeError): saslprep(b'xxx') def test_unassigned(self): """Test passing strings with unassigned code points""" for s in ('\u0221', '\u038b', '\u0510', '\u070e', '\u0900', '\u0a00'): with self.assertRaises(SASLPrepError, msg='U+%08x' % ord(s)): saslprep('abc' + s + 'def') def test_map_to_nothing(self): """Test passing strings with characters that map to nothing""" for s in ('\u00ad', '\u034f', '\u1806', '\u200c', '\u2060', '\ufe00'): self.assertEqual(saslprep('abc' + s + 'def'), 'abcdef', msg='U+%08x' % ord(s)) def test_map_to_whitespace(self): """Test passing strings with characters that map to whitespace""" for s in ('\u00a0', '\u1680', '\u2000', '\u202f', '\u205f', '\u3000'): self.assertEqual(saslprep('abc' + s + 'def'), 'abc def', msg='U+%08x' % ord(s)) def test_normalization(self): """Test Unicode normalization form KC conversions""" for (s, n) in (('\u00aa', 'a'), ('\u2168', 'IX')): self.assertEqual(saslprep('abc' + s + 'def'), 'abc' + n + 'def', msg='U+%08x' % ord(s)) def test_prohibited(self): """Test passing strings with prohibited characters""" for s in ('\u0000', '\u007f', '\u0080', '\u06dd', '\u180e', '\u200e', '\u2028', '\u202a', '\u206a', '\u2ff0', '\u2ffb', '\ud800', '\udfff', '\ue000', '\ufdd0', '\ufef9', '\ufffc', '\uffff', '\U0001d173', '\U000E0001', '\U00100000', '\U0010fffd'): with self.assertRaises(SASLPrepError, msg='U+%08x' % ord(s)): saslprep('abc' + s + 'def') def test_bidi(self): """Test passing strings with bidirectional characters""" for s in ('\u05be\u05c0\u05c3\u05d0', # RorAL only 'abc\u00c0\u00c1\u00c2', # L only '\u0627\u0031\u0628'): # Mix of RorAL and other self.assertEqual(saslprep(s), s) with self.assertRaises(SASLPrepError): saslprep('abc\u05be\u05c0\u05c3') # Mix of RorAL and L with self.assertRaises(SASLPrepError): saslprep('\u0627\u0031') # RorAL not at both start & end asyncssh-1.3.0/tests/util.py000066400000000000000000000022011260630620200160270ustar00rootroot00000000000000# Copyright (c) 2015 by Ron Frederick . # All rights reserved. # # This program and the accompanying materials are made available under # the terms of the Eclipse Public License v1.0 which accompanies this # distribution and is available at: # # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Ron Frederick - initial implementation, API, and documentation """Utility functions for unit tests""" import os import subprocess import tempfile import unittest class TempDirTestCase(unittest.TestCase): """Unit test class which operates in a temporary directory""" tempdir = None @classmethod def setUpClass(cls): cls.tempdir = tempfile.TemporaryDirectory() os.chdir(cls.tempdir.name) @classmethod def tearDownClass(cls): cls.tempdir.cleanup() def run(cmd): """Run a shell commands and return the output""" try: return subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as exc: # pragma: no cover print(exc.output.decode()) raise