From 4d3e0b711a98c440810004cb599a00d0f72978d7 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 29 Mar 2014 16:55:01 -0700 Subject: Switched hash functions from PyCrypto to hashlib. There's a few advantages to this: 1) It's probably fast, OpenSSL, which typically backs hashlib, receives far more attention for optimizaitons than PyCrypto. 2) It's the first step to supporting PyPy, where PyCrypto doesn't run. --- tests/test_util.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'tests/test_util.py') diff --git a/tests/test_util.py b/tests/test_util.py index 6bde4045..af6eceb5 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -23,7 +23,8 @@ Some unit tests for utility functions. from binascii import hexlify import errno import os -from Crypto.Hash import SHA +from hashlib import sha1 + import paramiko.util from paramiko.util import lookup_ssh_host_config as host_config from paramiko.py3compat import StringIO, byte_ord @@ -136,7 +137,7 @@ class UtilTest(ParamikoTest): ) def test_4_generate_key_bytes(self): - x = paramiko.util.generate_key_bytes(SHA, b'ABCDEFGH', 'This is my secret passphrase.', 64) + x = paramiko.util.generate_key_bytes(sha1, b'ABCDEFGH', 'This is my secret passphrase.', 64) hex = ''.join(['%02x' % byte_ord(c) for c in x]) self.assertEqual(hex, '9110e2f6793b69363e58173e9436b13a5a4b339005741d5c680e505f57d871347b4239f14fb5c46e857d5e100424873ba849ac699cea98d729e57b3e84378e8b') -- cgit v1.2.3 From 6f211115f49edcea7d23b764d7cf3a84ff12f5f0 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 29 Mar 2014 19:22:36 -0700 Subject: Switch from using PyCrypto's Random to using os.urandom. There's several reasons for this change: 1) It's faster for reads up to 1024 bytes (nearly 10x faster for 16 byte reads) 2) It receives considerably more security review since it's in the kernel. 3) It's yet another step towards running on PyPy. 4) Using userspace CSPRNGs is considered something of an anti-pattern. See: http://sockpuppet.org/blog/2014/02/25/safely-generate-random-numbers/ http://webcache.googleusercontent.com/search?q=cache:2nTvpCgKZXIJ:www.2uo.de/myths-about-urandom/+&cd=3&hl=en&ct=clnk&gl=us --- paramiko/agent.py | 2 +- paramiko/auth_handler.py | 2 +- paramiko/channel.py | 5 +++-- paramiko/common.py | 5 ----- paramiko/dsskey.py | 16 +++++++++------- paramiko/ecdsakey.py | 11 +++++++---- paramiko/hostkeys.py | 6 ++++-- paramiko/kex_gex.py | 8 +++++--- paramiko/kex_group1.py | 6 ++++-- paramiko/packet.py | 5 +++-- paramiko/pkey.py | 9 ++++----- paramiko/primes.py | 15 ++++++++------- paramiko/rsakey.py | 14 ++++++++------ paramiko/transport.py | 18 ++++++------------ test.py | 6 +++--- tests/test_kex.py | 17 +++++++++-------- tests/test_pkey.py | 13 +++++++------ tests/test_util.py | 6 ------ 18 files changed, 82 insertions(+), 82 deletions(-) (limited to 'tests/test_util.py') diff --git a/paramiko/agent.py b/paramiko/agent.py index 2b11337f..5a08d452 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -364,7 +364,7 @@ class AgentKey(PKey): def get_name(self): return self.name - def sign_ssh_data(self, rng, data): + def sign_ssh_data(self, data): msg = Message() msg.add_byte(cSSH2_AGENTC_SIGN_REQUEST) msg.add_string(self.blob) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index c00ad41c..57babef0 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -206,7 +206,7 @@ class AuthHandler (object): m.add_string(self.private_key.get_name()) m.add_string(self.private_key) blob = self._get_session_blob(self.private_key, 'ssh-connection', self.username) - sig = self.private_key.sign_ssh_data(self.transport.rng, blob) + sig = self.private_key.sign_ssh_data(blob) m.add_string(sig) elif self.auth_method == 'keyboard-interactive': m.add_string('') diff --git a/paramiko/channel.py b/paramiko/channel.py index e10ddbac..583809d5 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -21,9 +21,10 @@ Abstraction for an SSH2 channel. """ import binascii +import os +import socket import time import threading -import socket from paramiko import util from paramiko.common import cMSG_CHANNEL_REQUEST, cMSG_CHANNEL_WINDOW_ADJUST, \ @@ -358,7 +359,7 @@ class Channel (object): if auth_protocol is None: auth_protocol = 'MIT-MAGIC-COOKIE-1' if auth_cookie is None: - auth_cookie = binascii.hexlify(self.transport.rng.read(16)) + auth_cookie = binascii.hexlify(os.urandom(16)) m = Message() m.add_byte(cMSG_CHANNEL_REQUEST) diff --git a/paramiko/common.py b/paramiko/common.py index 9a5e2ee1..18298922 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -126,11 +126,6 @@ CONNECTION_FAILED_CODE = { DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \ DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14 -from Crypto import Random - -# keep a crypto-strong PRNG nearby -rng = Random.new() - zero_byte = byte_chr(0) one_byte = byte_chr(1) four_byte = byte_chr(4) diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index c26966e8..446353a0 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -20,11 +20,13 @@ DSS keys. """ +import os + from Crypto.PublicKey import DSA from Crypto.Hash import SHA from paramiko import util -from paramiko.common import zero_byte, rng +from paramiko.common import zero_byte from paramiko.py3compat import long from paramiko.ssh_exception import SSHException from paramiko.message import Message @@ -91,17 +93,17 @@ class DSSKey (PKey): def get_bits(self): return self.size - + def can_sign(self): return self.x is not None - def sign_ssh_data(self, rng, data): + def sign_ssh_data(self, data): digest = SHA.new(data).digest() dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q), long(self.x))) # generate a suitable k qsize = len(util.deflate_long(self.q, 0)) while True: - k = util.inflate_long(rng.read(qsize), 1) + k = util.inflate_long(os.urandom(qsize), 1) if (k > 2) and (k < self.q): break r, s = dss.sign(util.inflate_long(digest, 1), k) @@ -163,7 +165,7 @@ class DSSKey (PKey): by ``pyCrypto.PublicKey``). :return: new `.DSSKey` private key """ - dsa = DSA.generate(bits, rng.read, progress_func) + dsa = DSA.generate(bits, os.urandom, progress_func) key = DSSKey(vals=(dsa.p, dsa.q, dsa.g, dsa.y)) key.x = dsa.x return key @@ -174,11 +176,11 @@ class DSSKey (PKey): def _from_private_key_file(self, filename, password): data = self._read_private_key_file('DSA', filename, password) self._decode_key(data) - + def _from_private_key(self, file_obj, password): data = self._read_private_key('DSA', file_obj, password) self._decode_key(data) - + def _decode_key(self, data): # private key file contains: # DSAPrivateKey = { version = 0, p, q, g, y, x } diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index 6ae2d277..bb5b780d 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -21,11 +21,14 @@ L{ECDSAKey} """ import binascii +import os + from ecdsa import SigningKey, VerifyingKey, der, curves -from Crypto.Hash import SHA256 from ecdsa.test_pyecdsa import ECDSA -from paramiko.common import four_byte, one_byte +from Crypto.Hash import SHA256 + +from paramiko.common import four_byte, one_byte from paramiko.message import Message from paramiko.pkey import PKey from paramiko.py3compat import byte_chr, u @@ -97,9 +100,9 @@ class ECDSAKey (PKey): def can_sign(self): return self.signing_key is not None - def sign_ssh_data(self, rpool, data): + def sign_ssh_data(self, data): digest = SHA256.new(data).digest() - sig = self.signing_key.sign_digest(digest, entropy=rpool.read, + sig = self.signing_key.sign_digest(digest, entropy=os.urandom, sigencode=self._sigencode) m = Message() m.add_string('ecdsa-sha2-nistp256') diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index f32fbeb6..743165c7 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -18,8 +18,10 @@ import binascii +import os + from Crypto.Hash import SHA, HMAC -from paramiko.common import rng + from paramiko.py3compat import b, u, encodebytes, decodebytes try: @@ -262,7 +264,7 @@ class HostKeys (MutableMapping): :return: the hashed hostname as a `str` """ if salt is None: - salt = rng.read(SHA.digest_size) + salt = os.urandom(SHA.digest_size) else: if salt.startswith('|1|'): salt = salt.split('|')[2] diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py index 02e507b7..415f58e3 100644 --- a/paramiko/kex_gex.py +++ b/paramiko/kex_gex.py @@ -22,6 +22,8 @@ generator "g" are provided by the server. A bit more work is required on the client side, and a B{lot} more on the server side. """ +import os + from Crypto.Hash import SHA from paramiko import util @@ -101,7 +103,7 @@ class KexGex (object): qhbyte <<= 1 qmask >>= 1 while True: - x_bytes = self.transport.rng.read(byte_count) + x_bytes = os.urandom(byte_count) x_bytes = byte_mask(x_bytes[0], qmask) + x_bytes[1:] x = util.inflate_long(x_bytes, 1) if (x > 1) and (x < q): @@ -206,7 +208,7 @@ class KexGex (object): H = SHA.new(hm.asbytes()).digest() self.transport._set_K_H(K, H) # sign it - sig = self.transport.get_server_key().sign_ssh_data(self.transport.rng, H) + sig = self.transport.get_server_key().sign_ssh_data(H) # send reply m = Message() m.add_byte(c_MSG_KEXDH_GEX_REPLY) @@ -215,7 +217,7 @@ class KexGex (object): m.add_string(sig) self.transport._send_message(m) self.transport._activate_outbound() - + def _parse_kexdh_gex_reply(self, m): host_key = m.get_string() self.f = m.get_mpint() diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py index 3dfb7f18..bc88202c 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -21,6 +21,8 @@ Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of 1024 bit key halves, using a known "p" prime and "g" generator. """ +import os + from Crypto.Hash import SHA from paramiko import util @@ -82,7 +84,7 @@ class KexGroup1(object): # potential x where the first 63 bits are 1, because some of those will be # larger than q (but this is a tiny tiny subset of potential x). while 1: - x_bytes = self.transport.rng.read(128) + x_bytes = os.urandom(128) x_bytes = byte_mask(x_bytes[0], 0x7f) + x_bytes[1:] if (x_bytes[:8] != b7fffffffffffffff and x_bytes[:8] != b0000000000000000): @@ -127,7 +129,7 @@ class KexGroup1(object): H = SHA.new(hm.asbytes()).digest() self.transport._set_K_H(K, H) # sign it - sig = self.transport.get_server_key().sign_ssh_data(self.transport.rng, H) + sig = self.transport.get_server_key().sign_ssh_data(H) # send reply m = Message() m.add_byte(c_MSG_KEXDH_REPLY) diff --git a/paramiko/packet.py b/paramiko/packet.py index 0f51df5e..0e41b851 100644 --- a/paramiko/packet.py +++ b/paramiko/packet.py @@ -21,6 +21,7 @@ Packet handling """ import errno +import os import socket import struct import threading @@ -28,7 +29,7 @@ import time from paramiko import util from paramiko.common import linefeed_byte, cr_byte_value, asbytes, MSG_NAMES, \ - DEBUG, xffffffff, zero_byte, rng + DEBUG, xffffffff, zero_byte from paramiko.py3compat import u, byte_ord from paramiko.ssh_exception import SSHException, ProxyCommandFailure from paramiko.message import Message @@ -455,7 +456,7 @@ class Packetizer (object): # don't waste random bytes for the padding packet += (zero_byte * padding) else: - packet += rng.read(padding) + packet += os.urandom(padding) return packet def _trigger_rekey(self): diff --git a/paramiko/pkey.py b/paramiko/pkey.py index c8f84e0a..1313bdf3 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -28,7 +28,7 @@ from Crypto.Hash import MD5 from Crypto.Cipher import DES3, AES from paramiko import util -from paramiko.common import o600, rng, zero_byte +from paramiko.common import o600, zero_byte from paramiko.py3compat import u, encodebytes, decodebytes, b from paramiko.ssh_exception import SSHException, PasswordRequiredException @@ -138,12 +138,11 @@ class PKey (object): """ return u(encodebytes(self.asbytes())).replace('\n', '') - def sign_ssh_data(self, rng, data): + def sign_ssh_data(self, data): """ Sign a blob of data with this private key, and return a `.Message` representing an SSH signature message. - :param .Crypto.Util.rng.RandomPool rng: a secure random number generator. :param str data: the data to sign. :return: an SSH signature `message <.Message>`. """ @@ -331,11 +330,11 @@ class PKey (object): keysize = self._CIPHER_TABLE[cipher_name]['keysize'] blocksize = self._CIPHER_TABLE[cipher_name]['blocksize'] mode = self._CIPHER_TABLE[cipher_name]['mode'] - salt = rng.read(16) + salt = os.urandom(16) key = util.generate_key_bytes(MD5, salt, password, keysize) if len(data) % blocksize != 0: n = blocksize - len(data) % blocksize - #data += rng.read(n) + #data += os.urandom(n) # that would make more sense ^, but it confuses openssh. data += zero_byte * n data = cipher.new(key, mode, salt).encrypt(data) diff --git a/paramiko/primes.py b/paramiko/primes.py index 58d158c8..33cd6510 100644 --- a/paramiko/primes.py +++ b/paramiko/primes.py @@ -20,6 +20,8 @@ Utility functions for dealing with primes. """ +import os + from Crypto.Util import number from paramiko import util @@ -27,12 +29,12 @@ from paramiko.py3compat import byte_mask, long from paramiko.ssh_exception import SSHException -def _generate_prime(bits, rng): +def _generate_prime(bits): """primtive attempt at prime generation""" hbyte_mask = pow(2, bits % 8) - 1 while True: # loop catches the case where we increment n into a higher bit-range - x = rng.read((bits + 7) // 8) + x = os.urandom((bits + 7) // 8) if hbyte_mask > 0: x = byte_mask(x[0], hbyte_mask) + x[1:] n = util.inflate_long(x, 1) @@ -45,7 +47,7 @@ def _generate_prime(bits, rng): return n -def _roll_random(rng, n): +def _roll_random(n): """returns a random # from 0 to N-1""" bits = util.bit_length(n - 1) byte_count = (bits + 7) // 8 @@ -58,7 +60,7 @@ def _roll_random(rng, n): # fits, so i can't guarantee that this loop will ever finish, but the odds # of it looping forever should be infinitesimal. while True: - x = rng.read(byte_count) + x = os.urandom(byte_count) if hbyte_mask > 0: x = byte_mask(x[0], hbyte_mask) + x[1:] num = util.inflate_long(x, 1) @@ -73,11 +75,10 @@ class ModulusPack (object): on systems that have such a file. """ - def __init__(self, rpool): + def __init__(self): # pack is a hash of: bits -> [ (generator, modulus) ... ] self.pack = {} self.discarded = [] - self.rng = rpool def _parse_modulus(self, line): timestamp, mod_type, tests, tries, size, generator, modulus = line.split() @@ -147,5 +148,5 @@ class ModulusPack (object): if min > good: good = bitsizes[-1] # now pick a random modulus of this bitsize - n = _roll_random(self.rng, len(self.pack[good])) + n = _roll_random(len(self.pack[good])) return self.pack[good][n] diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index c93f3218..a6f97bff 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -20,11 +20,13 @@ RSA keys. """ +import os + from Crypto.PublicKey import RSA from Crypto.Hash import SHA from paramiko import util -from paramiko.common import rng, max_byte, zero_byte, one_byte +from paramiko.common import max_byte, zero_byte, one_byte from paramiko.message import Message from paramiko.ber import BER, BERException from paramiko.pkey import PKey @@ -90,7 +92,7 @@ class RSAKey (PKey): def can_sign(self): return self.d is not None - def sign_ssh_data(self, rpool, data): + def sign_ssh_data(self, data): digest = SHA.new(data).digest() rsa = RSA.construct((long(self.n), long(self.e), long(self.d))) sig = util.deflate_long(rsa.sign(self._pkcs1imify(digest), bytes())[0], 0) @@ -125,7 +127,7 @@ class RSAKey (PKey): def write_private_key_file(self, filename, password=None): self._write_private_key_file('RSA', filename, self._encode_key(), password) - + def write_private_key(self, file_obj, password=None): self._write_private_key('RSA', file_obj, self._encode_key(), password) @@ -140,7 +142,7 @@ class RSAKey (PKey): by ``pyCrypto.PublicKey``). :return: new `.RSAKey` private key """ - rsa = RSA.generate(bits, rng.read, progress_func) + rsa = RSA.generate(bits, os.urandom, progress_func) key = RSAKey(vals=(rsa.e, rsa.n)) key.d = rsa.d key.p = rsa.p @@ -162,11 +164,11 @@ class RSAKey (PKey): def _from_private_key_file(self, filename, password): data = self._read_private_key_file('RSA', filename, password) self._decode_key(data) - + def _from_private_key(self, file_obj, password): data = self._read_private_key('RSA', file_obj, password) self._decode_key(data) - + def _decode_key(self, data): # private key file contains: # RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p } diff --git a/paramiko/transport.py b/paramiko/transport.py index 1471b543..a0a752ef 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -20,6 +20,7 @@ Core protocol implementation """ +import os import socket import sys import threading @@ -30,7 +31,7 @@ import paramiko from paramiko import util from paramiko.auth_handler import AuthHandler from paramiko.channel import Channel -from paramiko.common import rng, xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \ +from paramiko.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \ cMSG_GLOBAL_REQUEST, DEBUG, MSG_KEXINIT, MSG_IGNORE, MSG_DISCONNECT, \ MSG_DEBUG, ERROR, WARNING, cMSG_UNIMPLEMENTED, INFO, cMSG_KEXINIT, \ cMSG_NEWKEYS, MSG_NEWKEYS, cMSG_REQUEST_SUCCESS, cMSG_REQUEST_FAILURE, \ @@ -57,7 +58,6 @@ from paramiko.ssh_exception import (SSHException, BadAuthenticationType, ChannelException, ProxyCommandFailure) from paramiko.util import retry_on_signal -from Crypto import Random from Crypto.Cipher import Blowfish, AES, DES3, ARC4 from Crypto.Hash import SHA, MD5 try: @@ -192,7 +192,6 @@ class Transport (threading.Thread): # okay, normal socket-ish flow here... threading.Thread.__init__(self) self.setDaemon(True) - self.rng = rng self.sock = sock # Python < 2.3 doesn't have the settimeout method - RogerB try: @@ -339,7 +338,6 @@ class Transport (threading.Thread): # synchronous, wait for a result self.completion_event = event = threading.Event() self.start() - Random.atfork() while True: event.wait(0.1) if not self.active: @@ -475,7 +473,7 @@ class Transport (threading.Thread): .. note:: This has no effect when used in client mode. """ - Transport._modulus_pack = ModulusPack(rng) + Transport._modulus_pack = ModulusPack() # places to look for the openssh "moduli" file file_list = ['/etc/ssh/moduli', '/usr/local/etc/moduli'] if filename is not None: @@ -732,8 +730,8 @@ class Transport (threading.Thread): m = Message() m.add_byte(cMSG_IGNORE) if byte_count is None: - byte_count = (byte_ord(rng.read(1)) % 32) + 10 - m.add_bytes(rng.read(byte_count)) + byte_count = (byte_ord(os.urandom(1)) % 32) + 10 + m.add_bytes(os.urandom(byte_count)) self._send_user_message(m) def renegotiate_keys(self): @@ -1402,10 +1400,6 @@ class Transport (threading.Thread): # interpreter shutdown. self.sys = sys - # Required to prevent RNG errors when running inside many subprocess - # containers. - Random.atfork() - # active=True occurs before the thread is launched, to avoid a race _active_threads.append(self) if self.server_mode: @@ -1590,7 +1584,7 @@ class Transport (threading.Thread): m = Message() m.add_byte(cMSG_KEXINIT) - m.add_bytes(rng.read(16)) + m.add_bytes(os.urandom(16)) m.add_list(self._preferred_kex) m.add_list(available_server_keys) m.add_list(self._preferred_ciphers) diff --git a/test.py b/test.py index bd966d1e..2b3d4ed4 100755 --- a/test.py +++ b/test.py @@ -101,12 +101,12 @@ def main(): parser.add_option('-P', '--sftp-passwd', dest='password', type='string', default=default_passwd, metavar='', help='[with -R] (optional) password to unlock the private key for remote sftp tests') - + options, args = parser.parse_args() - + # setup logging paramiko.util.log_to_file('test.log') - + if options.use_sftp: from tests.test_sftp import SFTPTest if options.use_loopback_sftp: diff --git a/tests/test_kex.py b/tests/test_kex.py index c522be46..56f1b7c7 100644 --- a/tests/test_kex.py +++ b/tests/test_kex.py @@ -21,7 +21,9 @@ Some unit tests for the key exchange protocols. """ from binascii import hexlify +import os import unittest + import paramiko.util from paramiko.kex_group1 import KexGroup1 from paramiko.kex_gex import KexGex @@ -29,9 +31,8 @@ from paramiko import Message from paramiko.common import byte_chr -class FakeRng (object): - def read(self, n): - return byte_chr(0xcc) * n +def dummy_urandom(n): + return byte_chr(0xcc) * n class FakeKey (object): @@ -41,7 +42,7 @@ class FakeKey (object): def asbytes(self): return b'fake-key' - def sign_ssh_data(self, rng, H): + def sign_ssh_data(self, H): return b'fake-sig' @@ -53,8 +54,7 @@ class FakeModulusPack (object): return self.G, self.P -class FakeTransport (object): - rng = FakeRng() +class FakeTransport(object): local_version = 'SSH-2.0-paramiko_1.0' remote_version = 'SSH-2.0-lame' local_kex_init = 'local-kex-init' @@ -91,10 +91,11 @@ class KexTest (unittest.TestCase): K = 14730343317708716439807310032871972459448364195094179797249681733965528989482751523943515690110179031004049109375612685505881911274101441415545039654102474376472240501616988799699744135291070488314748284283496055223852115360852283821334858541043710301057312858051901453919067023103730011648890038847384890504 def setUp(self): - pass + self._original_urandom = os.urandom + os.urandom = dummy_urandom def tearDown(self): - pass + os.urandom = self._original_urandom def test_1_group1_client(self): transport = FakeTransport() diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 6ff68fc2..b0ceefe7 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -22,9 +22,10 @@ Some unit tests for public/private key objects. from binascii import hexlify import unittest + from paramiko import RSAKey, DSSKey, ECDSAKey, Message, util from paramiko.py3compat import StringIO, byte_chr, b, bytes -from paramiko.common import rng + from tests.util import test_path # from openssh's ssh-keygen @@ -166,7 +167,7 @@ class KeyTest (unittest.TestCase): def test_8_sign_rsa(self): # verify that the rsa private key can sign and verify key = RSAKey.from_private_key_file(test_path('test_rsa.key')) - msg = key.sign_ssh_data(rng, b'ice weasels') + msg = key.sign_ssh_data(b'ice weasels') self.assertTrue(type(msg) is Message) msg.rewind() self.assertEqual('ssh-rsa', msg.get_text()) @@ -179,7 +180,7 @@ class KeyTest (unittest.TestCase): def test_9_sign_dss(self): # verify that the dss private key can sign and verify key = DSSKey.from_private_key_file(test_path('test_dss.key')) - msg = key.sign_ssh_data(rng, b'ice weasels') + msg = key.sign_ssh_data(b'ice weasels') self.assertTrue(type(msg) is Message) msg.rewind() self.assertEqual('ssh-dss', msg.get_text()) @@ -193,13 +194,13 @@ class KeyTest (unittest.TestCase): def test_A_generate_rsa(self): key = RSAKey.generate(1024) - msg = key.sign_ssh_data(rng, b'jerri blank') + msg = key.sign_ssh_data(b'jerri blank') msg.rewind() self.assertTrue(key.verify_ssh_sig(b'jerri blank', msg)) def test_B_generate_dss(self): key = DSSKey.generate(1024) - msg = key.sign_ssh_data(rng, b'jerri blank') + msg = key.sign_ssh_data(b'jerri blank') msg.rewind() self.assertTrue(key.verify_ssh_sig(b'jerri blank', msg)) @@ -240,7 +241,7 @@ class KeyTest (unittest.TestCase): def test_13_sign_ecdsa(self): # verify that the rsa private key can sign and verify key = ECDSAKey.from_private_key_file(test_path('test_ecdsa.key')) - msg = key.sign_ssh_data(rng, b'ice weasels') + msg = key.sign_ssh_data(b'ice weasels') self.assertTrue(type(msg) is Message) msg.rewind() self.assertEqual('ecdsa-sha2-nistp256', msg.get_text()) diff --git a/tests/test_util.py b/tests/test_util.py index 6bde4045..d3911f49 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -153,12 +153,6 @@ class UtilTest(ParamikoTest): finally: os.unlink('hostfile.temp') - def test_6_random(self): - from paramiko.common import rng - # just verify that we can pull out 32 bytes and not get an exception. - x = rng.read(32) - self.assertEqual(len(x), 32) - def test_7_host_config_expose_issue_33(self): test_config_file = """ Host www13.* -- cgit v1.2.3 From 8a836942d545ffa44ed139099dfea4f96d336add Mon Sep 17 00:00:00 2001 From: Yan Kalchevskiy Date: Tue, 16 Jul 2013 14:02:24 +0700 Subject: Add support quoted values for SSHConfig (#157) --- paramiko/config.py | 54 ++++++++++++++++++++++++++++++++----------- tests/test_util.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 14 deletions(-) (limited to 'tests/test_util.py') diff --git a/paramiko/config.py b/paramiko/config.py index 77fa13d7..28dc5231 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -53,6 +53,27 @@ class SSHConfig (object): :param file file_obj: a file-like object to read the config file from """ + def get_hosts(val): + i, length = 0, len(val) + hosts = [] + while i < length: + if val[i] == '"': + end = val.find('"', i + 1) + if end < 0: + raise Exception("Unparsable host %s" % val) + hosts.append(val[i + 1:end]) + i = end + 1 + elif not val[i].isspace(): + end = i + 1 + while end < length and not val[end].isspace(): + end += 1 + hosts.append(val[i:end]) + i = end + 1 + else: + i += 1 + + return hosts + host = {"host": ['*'], "config": {}} for line in file_obj: line = line.rstrip('\n').lstrip() @@ -75,22 +96,27 @@ class SSHConfig (object): raise Exception('Unparsable line: %r' % line) key = line[:i].lower() value = line[i:].lstrip() - + if key == 'host': self._config.append(host) - value = value.split() - host = {key: value, 'config': {}} - #identityfile, localforward, remoteforward keys are special cases, since they are allowed to be - # specified multiple times and they should be tried in order - # of specification. - - elif key in ['identityfile', 'localforward', 'remoteforward']: - if key in host['config']: - host['config'][key].append(value) - else: - host['config'][key] = [value] - elif key not in host['config']: - host['config'].update({key: value}) + host = { + 'host': get_hosts(value), + 'config': {} + } + else: + if value.startswith('"') and value.endswith('"'): + value = value[1:-1] + + #identityfile, localforward, remoteforward keys are special cases, since they are allowed to be + # specified multiple times and they should be tried in order + # of specification. + if key in ['identityfile', 'localforward', 'remoteforward']: + if key in host['config']: + host['config'][key].append(value) + else: + host['config'][key] = [value] + elif key not in host['config']: + host['config'][key] = value self._config.append(host) def lookup(self, hostname): diff --git a/tests/test_util.py b/tests/test_util.py index 69c75518..4e67e071 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -333,3 +333,71 @@ IdentityFile something_%l_using_fqdn """ config = paramiko.util.parse_ssh_config(StringIO(test_config)) assert config.lookup('meh') # will die during lookup() if bug regresses + + def test_quoted_host_names(self): + test_config_file = """\ +Host "param pam" param "pam" + Port 1111 + +Host "param2" + Port 2222 + +Host param3 parara + Port 3333 + +Host param4 "p a r" "p" "par" para + Port 4444 +""" + res = { + 'param pam': {'hostname': 'param pam', 'port': '1111'}, + 'param': {'hostname': 'param', 'port': '1111'}, + 'pam': {'hostname': 'pam', 'port': '1111'}, + + 'param2': {'hostname': 'param2', 'port': '2222'}, + + 'param3': {'hostname': 'param3', 'port': '3333'}, + 'parara': {'hostname': 'parara', 'port': '3333'}, + + 'param4': {'hostname': 'param4', 'port': '4444'}, + 'p a r': {'hostname': 'p a r', 'port': '4444'}, + 'p': {'hostname': 'p', 'port': '4444'}, + 'par': {'hostname': 'par', 'port': '4444'}, + 'para': {'hostname': 'para', 'port': '4444'}, + } + f = StringIO(test_config_file) + config = paramiko.util.parse_ssh_config(f) + for host, values in res.items(): + self.assertEquals( + paramiko.util.lookup_ssh_host_config(host, config), + values + ) + + def test_quoted_params_in_config(self): + test_config_file = """\ +Host "param pam" param "pam" + IdentityFile id_rsa + +Host "param2" + IdentityFile "test rsa key" + +Host param3 parara + IdentityFile id_rsa + IdentityFile "test rsa key" +""" + res = { + 'param pam': {'hostname': 'param pam', 'identityfile': ['id_rsa']}, + 'param': {'hostname': 'param', 'identityfile': ['id_rsa']}, + 'pam': {'hostname': 'pam', 'identityfile': ['id_rsa']}, + + 'param2': {'hostname': 'param2', 'identityfile': ['test rsa key']}, + + 'param3': {'hostname': 'param3', 'identityfile': ['id_rsa', 'test rsa key']}, + 'parara': {'hostname': 'parara', 'identityfile': ['id_rsa', 'test rsa key']}, + } + f = StringIO(test_config_file) + config = paramiko.util.parse_ssh_config(f) + for host, values in res.items(): + self.assertEquals( + paramiko.util.lookup_ssh_host_config(host, config), + values + ) -- cgit v1.2.3 From f258d1e207a21d4342dbc431fcc4a4dafe893e80 Mon Sep 17 00:00:00 2001 From: Yan Kalchevksiy Date: Sat, 15 Feb 2014 17:20:31 +0700 Subject: Moved get_hosts function into method. --- paramiko/config.py | 46 +++++++++++++++++++++++++--------------------- tests/test_util.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 21 deletions(-) (limited to 'tests/test_util.py') diff --git a/paramiko/config.py b/paramiko/config.py index dce472ee..5abf181f 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -54,26 +54,6 @@ class SSHConfig (object): :param file file_obj: a file-like object to read the config file from """ - def get_hosts(val): - i, length = 0, len(val) - hosts = [] - while i < length: - if val[i] == '"': - end = val.find('"', i + 1) - if end < 0: - raise Exception("Unparsable host %s" % val) - hosts.append(val[i + 1:end]) - i = end + 1 - elif not val[i].isspace(): - end = i + 1 - while end < length and not val[end].isspace(): - end += 1 - hosts.append(val[i:end]) - i = end + 1 - else: - i += 1 - - return hosts host = {"host": ['*'], "config": {}} for line in file_obj: @@ -90,7 +70,7 @@ class SSHConfig (object): if key == 'host': self._config.append(host) host = { - 'host': get_hosts(value), + 'host': self._get_hosts(value), 'config': {} } else: @@ -222,6 +202,30 @@ class SSHConfig (object): config[k] = config[k].replace(find, str(replace)) return config + def _get_hosts(self, host): + """ + Return a list of host_names from host value. + """ + i, length = 0, len(host) + hosts = [] + while i < length: + if host[i] == '"': + end = host.find('"', i + 1) + if end < 0: + raise Exception("Unparsable host %s" % host) + hosts.append(host[i + 1:end]) + i = end + 1 + elif not host[i].isspace(): + end = i + 1 + while end < length and not host[end].isspace() and host[end] != '"': + end += 1 + hosts.append(host[i:end]) + i = end + else: + i += 1 + + return hosts + class LazyFqdn(object): """ diff --git a/tests/test_util.py b/tests/test_util.py index 4e67e071..8142d416 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -401,3 +401,35 @@ Host param3 parara paramiko.util.lookup_ssh_host_config(host, config), values ) + + def test_quoted_host_in_config(self): + conf = SSHConfig() + correct_data = { + 'param': ['param'], + '"param"': ['param'], + + 'param pam': ['param', 'pam'], + '"param" "pam"': ['param', 'pam'], + '"param" pam': ['param', 'pam'], + 'param "pam"': ['param', 'pam'], + + 'param "pam" p': ['param', 'pam', 'p'], + '"param" pam "p"': ['param', 'pam', 'p'], + + '"pa ram"': ['pa ram'], + '"pa ram" pam': ['pa ram', 'pam'], + 'param "p a m"': ['param', 'p a m'], + } + incorrect_data = [ + 'param"', + '"param', + 'param "pam', + 'param "pam" "p a', + ] + for host, values in correct_data.items(): + self.assertEquals( + conf._get_hosts(host), + values + ) + for host in incorrect_data: + self.assertRaises(Exception, conf._get_hosts, host) -- cgit v1.2.3 From 2dec0a7671d83f21a290e1a2b3ea9159da45757a Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Thu, 14 Aug 2014 15:13:58 +0200 Subject: Add a utility method for value clamping. --- paramiko/util.py | 3 +++ tests/test_util.py | 5 +++++ 2 files changed, 8 insertions(+) (limited to 'tests/test_util.py') diff --git a/paramiko/util.py b/paramiko/util.py index f4ee3adc..d029f52e 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -320,3 +320,6 @@ def constant_time_bytes_eq(a, b): for i in (xrange if PY2 else range)(len(a)): res |= byte_ord(a[i]) ^ byte_ord(b[i]) return res == 0 + +def clamp_value(minimum, val, maximum): + return max(minimum, min(val, maximum)) diff --git a/tests/test_util.py b/tests/test_util.py index 69c75518..24f58076 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -333,3 +333,8 @@ IdentityFile something_%l_using_fqdn """ config = paramiko.util.parse_ssh_config(StringIO(test_config)) assert config.lookup('meh') # will die during lookup() if bug regresses + + def test_12_clamp_value(self): + self.assertEqual(32768, paramiko.util.clamp_value(32767, 32768, 32769)) + self.assertEqual(32767, paramiko.util.clamp_value(32767, 32765, 32769)) + self.assertEqual(32769, paramiko.util.clamp_value(32767, 32770, 32769)) -- cgit v1.2.3 From 163caabf5e4a2a63216c2192dc476d4184ab81b3 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Fri, 15 Aug 2014 15:33:25 +0200 Subject: Remove all occurences of ParamikoTest. Sorry paramiko, it's time to put on the big boy pants. You no longer support old as hell versions of python. --- tests/test_buffered_pipe.py | 5 ++--- tests/test_transport.py | 5 +++-- tests/test_util.py | 5 ++--- tests/util.py | 10 ---------- 4 files changed, 7 insertions(+), 18 deletions(-) (limited to 'tests/test_util.py') diff --git a/tests/test_buffered_pipe.py b/tests/test_buffered_pipe.py index 1045a925..3b8f97ad 100644 --- a/tests/test_buffered_pipe.py +++ b/tests/test_buffered_pipe.py @@ -22,11 +22,10 @@ Some unit tests for BufferedPipe. import threading import time +import unittest from paramiko.buffered_pipe import BufferedPipe, PipeTimeout from paramiko import pipe -from tests.util import ParamikoTest - def delay_thread(p): p.feed('a') @@ -40,7 +39,7 @@ def close_thread(p): p.close() -class BufferedPipeTest(ParamikoTest): +class BufferedPipeTest(unittest.TestCase): def test_1_buffered_pipe(self): p = BufferedPipe() self.assertTrue(not p.read_ready()) diff --git a/tests/test_transport.py b/tests/test_transport.py index ac744b26..5c77a321 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -26,6 +26,7 @@ import socket import time import threading import random +import unittest from paramiko import Transport, SecurityOptions, ServerInterface, RSAKey, DSSKey, \ SSHException, ChannelException @@ -35,7 +36,7 @@ from paramiko.common import MSG_KEXINIT, cMSG_CHANNEL_WINDOW_ADJUST from paramiko.py3compat import bytes from paramiko.message import Message from tests.loop import LoopSocket -from tests.util import ParamikoTest, test_path +from tests.util import test_path LONG_BANNER = """\ @@ -105,7 +106,7 @@ class NullServer (ServerInterface): return OPEN_SUCCEEDED -class TransportTest(ParamikoTest): +class TransportTest(unittest.TestCase): def setUp(self): self.socks = LoopSocket() self.sockc = LoopSocket() diff --git a/tests/test_util.py b/tests/test_util.py index 69c75518..643d9171 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -24,13 +24,12 @@ from binascii import hexlify import errno import os from hashlib import sha1 +import unittest import paramiko.util from paramiko.util import lookup_ssh_host_config as host_config from paramiko.py3compat import StringIO, byte_ord -from tests.util import ParamikoTest - test_config_file = """\ Host * User robey @@ -60,7 +59,7 @@ BGQ3GQ/Fc7SX6gkpXkwcZryoi4kNFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW\ from paramiko import * -class UtilTest(ParamikoTest): +class UtilTest(unittest.TestCase): def test_1_import(self): """ verify that all the classes can be imported from paramiko. diff --git a/tests/util.py b/tests/util.py index 66d2696c..b546a7e1 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,17 +1,7 @@ import os -import unittest root_path = os.path.dirname(os.path.realpath(__file__)) - -class ParamikoTest(unittest.TestCase): - # for Python 2.3 and below - if not hasattr(unittest.TestCase, 'assertTrue'): - assertTrue = unittest.TestCase.failUnless - if not hasattr(unittest.TestCase, 'assertFalse'): - assertFalse = unittest.TestCase.failIf - - def test_path(filename): return os.path.join(root_path, filename) -- cgit v1.2.3 From d05ef512bab1849e4fecafbc2a0c23465bf6b2c6 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Fri, 15 Aug 2014 15:41:49 +0200 Subject: Don't end a line with whitespace. This might be stripped by editors at will, which will make some tests brake. --- tests/test_util.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'tests/test_util.py') diff --git a/tests/test_util.py b/tests/test_util.py index 69c75518..28c162e5 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -41,7 +41,12 @@ Host *.example.com \tUser bjork Port=3333 Host * - \t \t Crazy something dumb +""" + +dont_strip_whitespace_please = "\t \t Crazy something dumb " + +test_config_file += dont_strip_whitespace_please +test_config_file += """ Host spoo.example.com Crazy something else """ -- cgit v1.2.3 From 8e2d4321509cf942c3d499b643b1978b1addaebf Mon Sep 17 00:00:00 2001 From: Søren Løvborg Date: Fri, 19 Sep 2014 15:00:18 +0200 Subject: SSHConfig.get_hostnames: List literal hostnames from SSH config --- paramiko/config.py | 10 ++++++++++ tests/test_util.py | 5 +++++ 2 files changed, 15 insertions(+) (limited to 'tests/test_util.py') diff --git a/paramiko/config.py b/paramiko/config.py index 20ca4aa7..4972c27b 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -126,6 +126,16 @@ class SSHConfig (object): ret = self._expand_variables(ret, hostname) return ret + def get_hostnames(self): + """ + Return the set of literal hostnames defined in the SSH config (both + explicit hostnames and wildcard entries). + """ + hosts = set() + for entry in self._config: + hosts.update(entry['host']) + return hosts + def _allowed(self, hosts, hostname): match = False for host in hosts: diff --git a/tests/test_util.py b/tests/test_util.py index 35e15765..f961fbbc 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -349,6 +349,11 @@ IdentityFile something_%l_using_fqdn config.parse(config_file) self.assertEqual(config.lookup("abcqwerty")["hostname"], "127.0.0.1") + def test_14_get_hostnames(self): + f = StringIO(test_config_file) + config = paramiko.util.parse_ssh_config(f) + self.assertEqual(config.get_hostnames(), set(['*', '*.example.com', 'spoo.example.com'])) + def test_quoted_host_names(self): test_config_file = """\ Host "param pam" param "pam" -- cgit v1.2.3 From d120ce4f06da5866c76e5e61196742a89f3c54c3 Mon Sep 17 00:00:00 2001 From: Sean Johnson Date: Tue, 11 Nov 2014 13:15:08 +1100 Subject: Added check for proxycommand none and associated test as per Paramiko Issue 415 Conflicts: tests/test_util.py --- paramiko/config.py | 3 +++ tests/test_util.py | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+) (limited to 'tests/test_util.py') diff --git a/paramiko/config.py b/paramiko/config.py index 91943ffb..233a87d9 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -73,6 +73,9 @@ class SSHConfig (object): 'host': self._get_hosts(value), 'config': {} } + elif key == 'proxycommand' and value.lower() == 'none': + # Proxycommands of none should not be added as an actual value. (Issue #415) + continue else: if value.startswith('"') and value.endswith('"'): value = value[1:-1] diff --git a/tests/test_util.py b/tests/test_util.py index 7f68de21..bfdc525e 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -464,3 +464,23 @@ Host param3 parara assert safe_vanilla == vanilla, err.format(safe_vanilla, vanilla) assert safe_has_bytes == expected_bytes, \ err.format(safe_has_bytes, expected_bytes) + + def test_proxycommand_none_issue_418(self): + test_config_file = """ +Host proxycommand-standard-none + ProxyCommand None + +Host proxycommand-with-equals-none + ProxyCommand=None + """ + for host, values in { + 'proxycommand-standard-none': {'hostname': 'proxycommand-standard-none'}, + 'proxycommand-with-equals-none': {'hostname': 'proxycommand-with-equals-none'} + }.items(): + + f = StringIO(test_config_file) + config = paramiko.util.parse_ssh_config(f) + self.assertEqual( + paramiko.util.lookup_ssh_host_config(host, config), + values + ) -- cgit v1.2.3 From 0c93fa94fbbb8d6284ee89e82298cc0e580203b4 Mon Sep 17 00:00:00 2001 From: Nick Pillitteri Date: Fri, 8 Jan 2016 16:47:54 -0500 Subject: Update SSHConfig.parse to strip leading and trailing whitespace Fixes #499 --- paramiko/config.py | 4 +++- tests/test_util.py | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) (limited to 'tests/test_util.py') diff --git a/paramiko/config.py b/paramiko/config.py index 5c2efdcc..6553691b 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -55,7 +55,9 @@ class SSHConfig (object): """ host = {"host": ['*'], "config": {}} for line in file_obj: - line = line.rstrip('\r\n').lstrip() + # Strip any leading or trailing whitespace from the line. + # See https://github.com/paramiko/paramiko/issues/499 for more info. + line = line.strip() if not line or line.startswith('#'): continue if '=' in line: diff --git a/tests/test_util.py b/tests/test_util.py index 0e7d0b2b..bca6f5d5 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -30,6 +30,7 @@ from paramiko.py3compat import StringIO, byte_ord, b from tests.util import ParamikoTest +# Note some lines in this configuration have trailing spaces on purpose test_config_file = """\ Host * User robey @@ -105,7 +106,7 @@ class UtilTest(ParamikoTest): self.assertEqual(config._config, [{'host': ['*'], 'config': {}}, {'host': ['*'], 'config': {'identityfile': ['~/.ssh/id_rsa'], 'user': 'robey'}}, {'host': ['*.example.com'], 'config': {'user': 'bjork', 'port': '3333'}}, - {'host': ['*'], 'config': {'crazy': 'something dumb '}}, + {'host': ['*'], 'config': {'crazy': 'something dumb'}}, {'host': ['spoo.example.com'], 'config': {'crazy': 'something else'}}]) def test_3_host_config(self): @@ -114,14 +115,14 @@ class UtilTest(ParamikoTest): config = paramiko.util.parse_ssh_config(f) for host, values in { - 'irc.danger.com': {'crazy': 'something dumb ', + 'irc.danger.com': {'crazy': 'something dumb', 'hostname': 'irc.danger.com', 'user': 'robey'}, - 'irc.example.com': {'crazy': 'something dumb ', + 'irc.example.com': {'crazy': 'something dumb', 'hostname': 'irc.example.com', 'user': 'robey', 'port': '3333'}, - 'spoo.example.com': {'crazy': 'something dumb ', + 'spoo.example.com': {'crazy': 'something dumb', 'hostname': 'spoo.example.com', 'user': 'robey', 'port': '3333'} -- cgit v1.2.3