From c1a0974d8886c6a2d46fb69a4a7df382875374c2 Mon Sep 17 00:00:00 2001 From: Dorian Pula Date: Tue, 23 May 2017 16:36:21 -0700 Subject: Fix flake8 for top-level modules. --- setup.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) (limited to 'setup.py') diff --git a/setup.py b/setup.py index 2e0d4041..80d5ea7f 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,13 @@ # along with Paramiko; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Suite 500, Boston, MA 02110-1335 USA. +import sys +from setuptools import setup + +if sys.platform == 'darwin': + import setup_helper + + setup_helper.install_custom_make_tarball() longdesc = ''' This is a library for making SSH2 connections (client or server). @@ -30,14 +37,6 @@ To install the development version, ``pip install -e git+https://github.com/paramiko/paramiko/#egg=paramiko``. ''' -import sys -from setuptools import setup - - -if sys.platform == 'darwin': - import setup_helper - setup_helper.install_custom_make_tarball() - # Version info -- read without importing _locals = {} @@ -45,22 +44,22 @@ with open('paramiko/_version.py') as fp: exec(fp.read(), None, _locals) version = _locals['__version__'] - setup( - name = "paramiko", - version = version, - description = "SSH2 protocol library", - long_description = longdesc, - author = "Jeff Forcier", - author_email = "jeff@bitprophet.org", - url = "https://github.com/paramiko/paramiko/", - packages = [ 'paramiko' ], - license = 'LGPL', - platforms = 'Posix; MacOS X; Windows', - classifiers = [ + name="paramiko", + version=version, + description="SSH2 protocol library", + long_description=longdesc, + author="Jeff Forcier", + author_email="jeff@bitprophet.org", + url="https://github.com/paramiko/paramiko/", + packages=['paramiko'], + license='LGPL', + platforms='Posix; MacOS X; Windows', + classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)', + 'License :: OSI Approved :: ' + 'GNU Library or Lesser General Public License (LGPL)', 'Operating System :: OS Independent', 'Topic :: Internet', 'Topic :: Security :: Cryptography', -- cgit v1.2.3 From b03ebb2d64ec87da589d6fcfa4f1c00ead40c1a7 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 26 May 2017 20:42:22 -0400 Subject: Fixes #325 -- add support for Ed25519 keys --- paramiko/client.py | 35 ++++++------- paramiko/ed25519key.py | 136 +++++++++++++++++++++++++++++++++++++++++++++++++ paramiko/hostkeys.py | 2 + paramiko/transport.py | 3 ++ setup.py | 1 + 5 files changed, 158 insertions(+), 19 deletions(-) create mode 100644 paramiko/ed25519key.py (limited to 'setup.py') diff --git a/paramiko/client.py b/paramiko/client.py index 8325d90f..42b52712 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -32,6 +32,7 @@ from paramiko.common import DEBUG from paramiko.config import SSH_PORT from paramiko.dsskey import DSSKey from paramiko.ecdsakey import ECDSAKey +from paramiko.ed25519key import Ed25519Key from paramiko.hostkeys import HostKeys from paramiko.py3compat import string_types from paramiko.resource import ResourceManager @@ -586,25 +587,21 @@ class SSHClient (ClosingContextManager): if not two_factor: keyfiles = [] - rsa_key = os.path.expanduser('~/.ssh/id_rsa') - dsa_key = os.path.expanduser('~/.ssh/id_dsa') - ecdsa_key = os.path.expanduser('~/.ssh/id_ecdsa') - if os.path.isfile(rsa_key): - keyfiles.append((RSAKey, rsa_key)) - if os.path.isfile(dsa_key): - keyfiles.append((DSSKey, dsa_key)) - if os.path.isfile(ecdsa_key): - keyfiles.append((ECDSAKey, ecdsa_key)) - # look in ~/ssh/ for windows users: - rsa_key = os.path.expanduser('~/ssh/id_rsa') - dsa_key = os.path.expanduser('~/ssh/id_dsa') - ecdsa_key = os.path.expanduser('~/ssh/id_ecdsa') - if os.path.isfile(rsa_key): - keyfiles.append((RSAKey, rsa_key)) - if os.path.isfile(dsa_key): - keyfiles.append((DSSKey, dsa_key)) - if os.path.isfile(ecdsa_key): - keyfiles.append((ECDSAKey, ecdsa_key)) + + for keytype, path in [ + (RSAKey, "rsa"), + (DSSKey, "dsa"), + (ECDSAKey, "ecdsa"), + (Ed25519Key, "ed25519"), + ]: + full_path = os.path.expanduser("~/.ssh/id_%s" % path) + if os.path.isfile(full_path): + keyfiles.append((keytype, full_path)) + + # look in ~/ssh/ for windows users: + full_path = os.path.expanduser("~/ssh/id_%s" % path) + if os.path.isfile(full_path): + keyfiles.append((keytype, full_path)) if not look_for_keys: keyfiles = [] diff --git a/paramiko/ed25519key.py b/paramiko/ed25519key.py new file mode 100644 index 00000000..bf4b9d6e --- /dev/null +++ b/paramiko/ed25519key.py @@ -0,0 +1,136 @@ +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + +import nacl.signing + +import six + +from paramiko.message import Message +from paramiko.pkey import PKey + + +OPENSSH_AUTH_MAGIC = "openssh-key-v1\x00" + +def unpad(data): + padding_length = six.indexbytes(data, -1) + if padding_length > 16: + raise SSHException('Invalid key') + for i in range(1, padding_length + 1): + if six.indexbytes(data, -i) != (padding_length - i + 1): + raise SSHException('Invalid key') + return data[:-padding_length] + + +class Ed25519Key(PKey): + def __init__(self, msg=None, data=None, filename=None, password=None): + verifying_key = signing_key = None + if msg is None and data is not None: + msg = Message(data) + if msg is not None: + if msg.get_text() != "ssh-ed25519": + raise SSHException('Invalid key') + verifying_key = nacl.signing.VerifyKey(msg.get_bytes(32)) + elif filename is not None: + with open(filename, "rb") as f: + data = self._read_private_key("OPENSSH", f) + signing_key = self._parse_signing_key_data(data) + + if signing_key is None and verifying_key is None: + import pdb; pdb.set_trace() + + self._signing_key = signing_key + self._verifying_key = verifying_key + + + def _parse_signing_key_data(self, data): + # We may eventually want this to be usable for other key types, as + # OpenSSH moves to it, but for now this is just for Ed25519 keys. + message = Message(data) + if message.get_bytes(len(OPENSSH_AUTH_MAGIC)) != OPENSSH_AUTH_MAGIC: + raise SSHException('Invalid key') + + ciphername = message.get_string() + kdfname = message.get_string() + kdfoptions = message.get_string() + num_keys = message.get_int() + + if ciphername != "none" or kdfname != "none" or kdfoptions: + raise NotImplementedError("Encrypted keys are not implemented") + + public_keys = [] + for _ in range(num_keys): + # We don't need the public keys, fast-forward through them. + pubkey = Message(message.get_binary()) + if pubkey.get_string() != 'ssh-ed25519': + raise SSHException('Invalid key') + public_keys.append(pubkey.get_binary()) + + message = Message(unpad(message.get_binary())) + if message.get_int() != message.get_int(): + raise SSHException('Invalid key') + + signing_keys = [] + for i in range(num_keys): + if message.get_string() != 'ssh-ed25519': + raise SSHException('Invalid key') + # A copy of the public key, again, ignore. + public = message.get_binary() + key_data = message.get_binary() + # The second half of the key data is yet another copy of the public + # key... + signing_key = nacl.signing.SigningKey(key_data[:32]) + assert ( + signing_key.verify_key.encode() == public == public_keys[i] == key_data[32:] + ) + signing_keys.append(signing_key) + # Comment, ignore. + message.get_string() + + if len(signing_keys) != 1: + raise SSHException('Invalid key') + return signing_keys[0] + + def asbytes(self): + m = Message() + m.add_string('ssh-ed25519') + m.add_bytes(self._signing_key.verify_key.encode()) + return m.asbytes() + + def get_name(self): + return "ssh-ed25519" + + def get_bits(self): + return 256 + + def can_sign(self): + return self._signing_key is not None + + def sign_ssh_data(self, data): + m = Message() + m.add_string('ssh-ed25519') + m.add_string(self._signing_key.sign(data).signature) + return m + + def verify_ssh_sig(self, data, msg): + if msg.get_text() != 'ssh-ed25519': + return False + + try: + self._verifying_key.verify(data, msg.get_binary()) + except nacl.exceptions.BadSignatureError: + return False + else: + return True diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index 18a0d333..7586b903 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -360,6 +360,8 @@ class HostKeyEntry: key = DSSKey(data=decodebytes(key)) elif keytype in ECDSAKey.supported_key_format_identifiers(): key = ECDSAKey(data=decodebytes(key), validate_point=False) + elif keytype == 'ssh-ed25519': + key = Ed25519Key(data=decodebytes(key)) else: log.info("Unable to handle key of type %s" % (keytype,)) return None diff --git a/paramiko/transport.py b/paramiko/transport.py index b61e82c4..acba0b81 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -54,6 +54,7 @@ from paramiko.common import ( ) from paramiko.compress import ZlibCompressor, ZlibDecompressor from paramiko.dsskey import DSSKey +from paramiko.ed25519key import Ed25519Key from paramiko.kex_gex import KexGex, KexGexSHA256 from paramiko.kex_group1 import KexGroup1 from paramiko.kex_group14 import KexGroup14 @@ -123,6 +124,7 @@ class Transport (threading.Thread, ClosingContextManager): 'hmac-sha1', ) _preferred_keys = ( + 'ssh-ed25519', 'ssh-rsa', 'ssh-dss', ) + tuple(ECDSAKey.supported_key_format_identifiers()) @@ -211,6 +213,7 @@ class Transport (threading.Thread, ClosingContextManager): 'ssh-rsa': RSAKey, 'ssh-dss': DSSKey, 'ecdsa-sha2-nistp256': ECDSAKey, + 'ssh-ed25519': Ed25519Key, } _kex_info = { diff --git a/setup.py b/setup.py index 80d5ea7f..2756a76d 100644 --- a/setup.py +++ b/setup.py @@ -75,6 +75,7 @@ setup( ], install_requires=[ 'cryptography>=1.1', + 'pynacl', 'pyasn1>=0.1.7', ], ) -- cgit v1.2.3 From a8ff22322622aff271d39ac849618f7372552619 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 26 May 2017 21:18:58 -0400 Subject: Support decrypting keys --- paramiko/__init__.py | 1 + paramiko/ed25519key.py | 50 +++++++++++++++++++++++++++++++++++------ paramiko/transport.py | 2 +- setup.py | 1 + tests/test_ed25519.key | 8 +++++++ tests/test_ed25519_password.key | 8 +++++++ tests/test_pkey.py | 19 ++++++++-------- 7 files changed, 72 insertions(+), 17 deletions(-) create mode 100644 tests/test_ed25519.key create mode 100644 tests/test_ed25519_password.key (limited to 'setup.py') diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 197f519a..d67ad62f 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -45,6 +45,7 @@ from paramiko.server import ServerInterface, SubsystemHandler, InteractiveQuery from paramiko.rsakey import RSAKey from paramiko.dsskey import DSSKey from paramiko.ecdsakey import ECDSAKey +from paramiko.ed25519key import Ed25519Key from paramiko.sftp import SFTPError, BaseSFTP from paramiko.sftp_client import SFTP, SFTPClient from paramiko.sftp_server import SFTPServer diff --git a/paramiko/ed25519key.py b/paramiko/ed25519key.py index 9638cc01..694b1e15 100644 --- a/paramiko/ed25519key.py +++ b/paramiko/ed25519key.py @@ -14,6 +14,11 @@ # along with Paramiko; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +import bcrypt + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher + import nacl.signing import six @@ -49,7 +54,7 @@ class Ed25519Key(PKey): elif filename is not None: with open(filename, "rb") as f: data = self._read_private_key("OPENSSH", f) - signing_key = self._parse_signing_key_data(data) + signing_key = self._parse_signing_key_data(data, password) if signing_key is None and verifying_key is None: raise ValueError("need a key") @@ -58,7 +63,8 @@ class Ed25519Key(PKey): self._verifying_key = verifying_key - def _parse_signing_key_data(self, data): + def _parse_signing_key_data(self, data, password): + from paramiko.transport import Transport # We may eventually want this to be usable for other key types, as # OpenSSH moves to it, but for now this is just for Ed25519 keys. message = Message(data) @@ -70,10 +76,22 @@ class Ed25519Key(PKey): kdfoptions = message.get_string() num_keys = message.get_int() - if ciphername != "none" or kdfname != "none" or kdfoptions: - # TODO: add support for `kdfname == "bcrypt"` as documented in: - # https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key#L21-L28 - raise NotImplementedError("Encrypted keys are not implemented") + if kdfname == "none": + # kdfname of "none" must have an empty kdfoptions, the ciphername + # must be "none" and there must not be a password. + if kdfoptions or ciphername != "none" or password: + raise SSHException('Invalid key') + elif kdfname == "bcrypt": + if not password: + raise SSHException('Invalid key') + kdf = Message(kdfoptions) + bcrypt_salt = kdf.get_binary() + bcrypt_rounds = kdf.get_int() + else: + raise SSHException('Invalid key') + + if ciphername != "none" and ciphername not in Transport._cipher_info: + raise SSHException('Invalid key') public_keys = [] for _ in range(num_keys): @@ -82,7 +100,25 @@ class Ed25519Key(PKey): raise SSHException('Invalid key') public_keys.append(pubkey.get_binary()) - message = Message(unpad(message.get_binary())) + private_ciphertext = message.get_binary() + if ciphername == "none": + private_data = private_ciphertext + else: + cipher = Transport._cipher_info[ciphername] + key = bcrypt.kdf( + password=password, + salt=bcrypt_salt, + desired_key_bytes=cipher['key-size'] + cipher['block-size'], + rounds=bcrypt_rounds + ) + decryptor = Cipher( + cipher['class'](key[:cipher['key-size']]), + cipher['mode'](key[cipher['key-size']:]), + backend=default_backend() + ).decryptor() + private_data = decryptor.update(private_ciphertext) + decryptor.finalize() + + message = Message(unpad(private_data)) if message.get_int() != message.get_int(): raise SSHException('Invalid key') diff --git a/paramiko/transport.py b/paramiko/transport.py index acba0b81..7e437cc9 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -85,7 +85,7 @@ import atexit atexit.register(_join_lingering_threads) -class Transport (threading.Thread, ClosingContextManager): +class Transport(threading.Thread, ClosingContextManager): """ An SSH Transport attaches to a stream (usually a socket), negotiates an encrypted session, authenticates, and then creates stream tunnels, called diff --git a/setup.py b/setup.py index 2756a76d..458916a6 100644 --- a/setup.py +++ b/setup.py @@ -74,6 +74,7 @@ setup( 'Programming Language :: Python :: 3.5', ], install_requires=[ + 'bcrypt>=3.0.0', 'cryptography>=1.1', 'pynacl', 'pyasn1>=0.1.7', diff --git a/tests/test_ed25519.key b/tests/test_ed25519.key new file mode 100644 index 00000000..eb9f94c2 --- /dev/null +++ b/tests/test_ed25519.key @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACB69SvZKJh/9VgSL0G27b5xVYa8nethH3IERbi0YqJDXwAAAKhjwAdrY8AH +awAAAAtzc2gtZWQyNTUxOQAAACB69SvZKJh/9VgSL0G27b5xVYa8nethH3IERbi0YqJDXw +AAAEA9tGQi2IrprbOSbDCF+RmAHd6meNSXBUQ2ekKXm4/8xnr1K9komH/1WBIvQbbtvnFV +hryd62EfcgRFuLRiokNfAAAAI2FsZXhfZ2F5bm9yQEFsZXhzLU1hY0Jvb2stQWlyLmxvY2 +FsAQI= +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/test_ed25519_password.key b/tests/test_ed25519_password.key new file mode 100644 index 00000000..d178aaae --- /dev/null +++ b/tests/test_ed25519_password.key @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABDaKD4ac7 +kieb+UfXaLaw68AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIOQn7fjND5ozMSV3 +CvbEtIdT73hWCMRjzS/lRdUDw50xAAAAsE8kLGyYBnl9ihJNqv378y6mO3SkzrDbWXOnK6 +ij0vnuTAvcqvWHAnyu6qBbplu/W2m55ZFeAItgaEcV2/V76sh/sAKlERqrLFyXylN0xoOW +NU5+zU08aTlbSKGmeNUU2xE/xfJq12U9XClIRuVUkUpYANxNPbmTRpVrbD3fgXMhK97Jrb +DEn8ca1IqMPiYmd/hpe5+tq3OxyRljXjCUFWTnqkp9VvUdzSTdSGZHsW9i +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 24d78c3e..74330b8d 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -27,7 +27,7 @@ from binascii import hexlify from hashlib import md5 import base64 -from paramiko import RSAKey, DSSKey, ECDSAKey, Message, util +from paramiko import RSAKey, DSSKey, ECDSAKey, Ed25519Key, Message, util from paramiko.py3compat import StringIO, byte_chr, b, bytes, PY2 from tests.util import test_path @@ -112,14 +112,7 @@ TEST_KEY_BYTESTR_2 = '\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01#\x00\x00\x00\x81\x TEST_KEY_BYTESTR_3 = '\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01#\x00\x00\x00\x00ӏV\x07k%<\x1fT$E#>ғfD\x18 \x0cae#̬S#VlE\x1epvo\x17M߉DUXL<\x06\x10דw\u2bd5ٿw˟0)#y{\x10l\tPru\t\x19Π\u070e/f0yFmm\x1f' -class KeyTest (unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - +class KeyTest(unittest.TestCase): def test_1_generate_key_bytes(self): key = util.generate_key_bytes(md5, x1234, 'happy birthday', 30) exp = b'\x61\xE1\xF2\x72\xF4\xC1\xC4\x56\x15\x86\xBD\x32\x24\x98\xC0\xE9\x24\x67\x27\x80\xF4\x7B\xB3\x7D\xDA\x7D\x54\x01\x9E\x64' @@ -436,3 +429,11 @@ class KeyTest (unittest.TestCase): key = RSAKey.from_private_key_file(test_path('test_rsa.key')) comparable = TEST_KEY_BYTESTR_2 if PY2 else TEST_KEY_BYTESTR_3 self.assertEqual(str(key), comparable) + + def test_ed25519(self): + key1 = Ed25519Key.from_private_key_file(test_path('test_ed25519.key')) + key2 = Ed25519Key.from_private_key_file( + test_path('test_ed25519_password.key'), 'abc123' + ) + + self.assertNotEqual(key1.asbytes(), key2.asbytes()) -- cgit v1.2.3 From 089eb214ac62ed8ec271051c47b34e30ca39c1e9 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 3 Jun 2017 02:01:11 -0400 Subject: set a minimum version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'setup.py') diff --git a/setup.py b/setup.py index 458916a6..e2ace96d 100644 --- a/setup.py +++ b/setup.py @@ -76,7 +76,7 @@ setup( install_requires=[ 'bcrypt>=3.0.0', 'cryptography>=1.1', - 'pynacl', + 'pynacl>=1.0.1', 'pyasn1>=0.1.7', ], ) -- cgit v1.2.3