diff options
author | Sebastian Deiss <sdeiss@haw-landshut.de> | 2014-04-23 14:42:23 +0200 |
---|---|---|
committer | Sebastian Deiss <sdeiss@haw-landshut.de> | 2014-04-23 14:42:23 +0200 |
commit | ce87fc8d7a8a025671183fc78091e5d1f6760f5f (patch) | |
tree | bce8764d8c708567e378d773e9352ee13a62daf6 | |
parent | 550f0b99eadf8a366253046acdd944709214123f (diff) | |
parent | 417102dbea0fbe2f7df6c690ee610b2cc2c18b19 (diff) |
Merge branch 'paramiko:master' into gssapi-py3-support
Conflicts:
sites/www/changelog.rst
test.py
-rw-r--r-- | paramiko/agent.py | 2 | ||||
-rw-r--r-- | paramiko/auth_handler.py | 2 | ||||
-rw-r--r-- | paramiko/channel.py | 5 | ||||
-rw-r--r-- | paramiko/common.py | 5 | ||||
-rw-r--r-- | paramiko/dsskey.py | 26 | ||||
-rw-r--r-- | paramiko/ecdsakey.py | 14 | ||||
-rw-r--r-- | paramiko/hostkeys.py | 13 | ||||
-rw-r--r-- | paramiko/kex_gex.py | 13 | ||||
-rw-r--r-- | paramiko/kex_group1.py | 11 | ||||
-rw-r--r-- | paramiko/packet.py | 14 | ||||
-rw-r--r-- | paramiko/pkey.py | 17 | ||||
-rw-r--r-- | paramiko/primes.py | 29 | ||||
-rw-r--r-- | paramiko/rsakey.py | 20 | ||||
-rw-r--r-- | paramiko/sftp_client.py | 4 | ||||
-rw-r--r-- | paramiko/sftp_server.py | 18 | ||||
-rw-r--r-- | paramiko/ssh_exception.py | 12 | ||||
-rw-r--r-- | paramiko/transport.py | 40 | ||||
-rw-r--r-- | paramiko/util.py | 9 | ||||
-rw-r--r-- | sites/docs/conf.py | 7 | ||||
-rw-r--r-- | sites/shared_conf.py | 8 | ||||
-rw-r--r-- | sites/www/changelog.rst | 14 | ||||
-rw-r--r-- | sites/www/conf.py | 4 | ||||
-rw-r--r-- | sites/www/contributing.rst | 3 | ||||
-rwxr-xr-x | test.py | 6 | ||||
-rw-r--r-- | tests/test_kex.py | 17 | ||||
-rw-r--r-- | tests/test_packetizer.py | 9 | ||||
-rw-r--r-- | tests/test_pkey.py | 19 | ||||
-rwxr-xr-x | tests/test_sftp.py | 17 | ||||
-rw-r--r-- | tests/test_util.py | 11 | ||||
-rw-r--r-- | tox.ini | 2 |
30 files changed, 195 insertions, 176 deletions
diff --git a/paramiko/agent.py b/paramiko/agent.py index 68e9be9f..4f463449 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -365,7 +365,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 8532d1f9..cb06da2d 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -238,7 +238,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 3da43cf2..22ee8810 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -143,11 +143,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..1901596d 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -20,11 +20,13 @@ DSS keys. """ +import os +from hashlib import sha1 + 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): - digest = SHA.new(data).digest() + def sign_ssh_data(self, data): + digest = sha1(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) @@ -111,9 +113,9 @@ class DSSKey (PKey): rstr = util.deflate_long(r, 0) sstr = util.deflate_long(s, 0) if len(rstr) < 20: - rstr += zero_byte * (20 - len(rstr)) + rstr = zero_byte * (20 - len(rstr)) + rstr if len(sstr) < 20: - sstr += zero_byte * (20 - len(sstr)) + sstr = zero_byte * (20 - len(sstr)) + sstr m.add_string(rstr + sstr) return m @@ -130,7 +132,7 @@ class DSSKey (PKey): # pull out (r, s) which are NOT encoded as mpints sigR = util.inflate_long(sig[:20], 1) sigS = util.inflate_long(sig[20:], 1) - sigM = util.inflate_long(SHA.new(data).digest(), 1) + sigM = util.inflate_long(sha1(data).digest(), 1) dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q))) return dss.verify(sigM, (sigR, sigS)) @@ -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..6736315f 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -21,11 +21,12 @@ L{ECDSAKey} """ import binascii +from hashlib import sha256 + 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 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,10 +98,9 @@ class ECDSAKey (PKey): def can_sign(self): return self.signing_key is not None - def sign_ssh_data(self, rpool, data): - digest = SHA256.new(data).digest() - sig = self.signing_key.sign_digest(digest, entropy=rpool.read, - sigencode=self._sigencode) + def sign_ssh_data(self, data): + sig = self.signing_key.sign_deterministic( + data, sigencode=self._sigencode, hashfunc=sha256) m = Message() m.add_string('ecdsa-sha2-nistp256') m.add_string(sig) @@ -113,7 +113,7 @@ class ECDSAKey (PKey): # verify the signature by SHA'ing the data and encrypting it # using the public key. - hash_obj = SHA256.new(data).digest() + hash_obj = sha256(data).digest() return self.verifying_key.verify_digest(sig, hash_obj, sigdecode=self._sigdecode) diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index f32fbeb6..c0caeda9 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -18,8 +18,11 @@ import binascii -from Crypto.Hash import SHA, HMAC -from paramiko.common import rng +import os + +from hashlib import sha1 +from hmac import HMAC + from paramiko.py3compat import b, u, encodebytes, decodebytes try: @@ -262,13 +265,13 @@ class HostKeys (MutableMapping): :return: the hashed hostname as a `str` """ if salt is None: - salt = rng.read(SHA.digest_size) + salt = os.urandom(sha1().digest_size) else: if salt.startswith('|1|'): salt = salt.split('|')[2] salt = decodebytes(b(salt)) - assert len(salt) == SHA.digest_size - hmac = HMAC.HMAC(salt, b(hostname), SHA).digest() + assert len(salt) == sha1().digest_size + hmac = HMAC(salt, b(hostname), sha1).digest() hostkey = '|1|%s|%s' % (u(encodebytes(salt)), u(encodebytes(hmac))) return hostkey.replace('\n', '') hash_host = staticmethod(hash_host) diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py index 02e507b7..5ff8a287 100644 --- a/paramiko/kex_gex.py +++ b/paramiko/kex_gex.py @@ -22,7 +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. """ -from Crypto.Hash import SHA +import os +from hashlib import sha1 from paramiko import util from paramiko.common import DEBUG @@ -101,7 +102,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): @@ -203,10 +204,10 @@ class KexGex (object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - H = SHA.new(hm.asbytes()).digest() + H = sha1(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 +216,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() @@ -238,6 +239,6 @@ class KexGex (object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - self.transport._set_K_H(K, SHA.new(hm.asbytes()).digest()) + self.transport._set_K_H(K, sha1(hm.asbytes()).digest()) self.transport._verify_key(host_key, sig) self.transport._activate_outbound() diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py index f588332e..a88f00d2 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -21,7 +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. """ -from Crypto.Hash import SHA +import os +from hashlib import sha1 from paramiko import util from paramiko.common import max_byte, zero_byte @@ -82,7 +83,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): @@ -105,7 +106,7 @@ class KexGroup1(object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - self.transport._set_K_H(K, SHA.new(hm.asbytes()).digest()) + self.transport._set_K_H(K, sha1(hm.asbytes()).digest()) self.transport._verify_key(host_key, sig) self.transport._activate_outbound() @@ -124,10 +125,10 @@ class KexGroup1(object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - H = SHA.new(hm.asbytes()).digest() + H = sha1(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..e97d92f0 100644 --- a/paramiko/packet.py +++ b/paramiko/packet.py @@ -21,25 +21,21 @@ Packet handling """ import errno +import os import socket import struct import threading import time +from hmac import HMAC 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 -try: - from r_hmac import HMAC -except ImportError: - from Crypto.Hash.HMAC import HMAC - - def compute_hmac(key, message, digest_class): return HMAC(key, message, digest_class).digest() @@ -359,7 +355,7 @@ class Packetizer (object): raise SSHException('Mismatched MAC') padding = byte_ord(packet[0]) payload = packet[1:packet_size - padding] - + if self.__dump_packets: self._log(DEBUG, 'Got payload (%d bytes, %d padding)' % (packet_size, padding)) @@ -455,7 +451,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..373563f6 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -23,12 +23,12 @@ Common API for all public keys. import base64 from binascii import hexlify, unhexlify import os +from hashlib import md5 -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 @@ -126,7 +126,7 @@ class PKey (object): a 16-byte `string <str>` (binary) of the MD5 fingerprint, in SSH format. """ - return MD5.new(self.asbytes()).digest() + return md5(self.asbytes()).digest() def get_base64(self): """ @@ -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>`. """ @@ -300,7 +299,7 @@ class PKey (object): keysize = self._CIPHER_TABLE[encryption_type]['keysize'] mode = self._CIPHER_TABLE[encryption_type]['mode'] salt = unhexlify(b(saltstr)) - key = util.generate_key_bytes(MD5, salt, password, keysize) + key = util.generate_key_bytes(md5, salt, password, keysize) return cipher.new(key, mode, salt).decrypt(data) def _write_private_key_file(self, tag, filename, data, password=None): @@ -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) - key = util.generate_key_bytes(MD5, salt, password, keysize) + 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 7d3b6a3e..7415c182 100644 --- a/paramiko/primes.py +++ b/paramiko/primes.py @@ -20,7 +20,7 @@ Utility functions for dealing with primes. """ -from Crypto.Util import number +import os from paramiko import util from paramiko.py3compat import byte_mask, long @@ -28,25 +28,7 @@ from paramiko.ssh_exception import SSHException from paramiko.common import * -def _generate_prime(bits, rng): - """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) - if hbyte_mask > 0: - x = byte_mask(x[0], hbyte_mask) + x[1:] - n = util.inflate_long(x, 1) - n |= 1 - n |= (1 << (bits - 1)) - while not number.isPrime(n): - n += 2 - if util.bit_length(n) == bits: - break - 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 @@ -59,7 +41,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) @@ -74,11 +56,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() @@ -148,5 +129,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..d1f3ecfe 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -20,11 +20,13 @@ RSA keys. """ +import os +from hashlib import sha1 + 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,8 +92,8 @@ class RSAKey (PKey): def can_sign(self): return self.d is not None - def sign_ssh_data(self, rpool, data): - digest = SHA.new(data).digest() + def sign_ssh_data(self, data): + digest = sha1(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) m = Message() @@ -106,7 +108,7 @@ class RSAKey (PKey): # verify the signature by SHA'ing the data and encrypting it using the # public key. some wackiness ensues where we "pkcs1imify" the 20-byte # hash into a string as long as the RSA key. - hash_obj = util.inflate_long(self._pkcs1imify(SHA.new(data).digest()), True) + hash_obj = util.inflate_long(self._pkcs1imify(sha1(data).digest()), True) rsa = RSA.construct((long(self.n), long(self.e))) return rsa.verify(hash_obj, (sig,)) @@ -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/sftp_client.py b/paramiko/sftp_client.py index ce6fbec6..1caaf165 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -117,8 +117,10 @@ class SFTPClient(BaseSFTP): def _log(self, level, msg, *args): if isinstance(msg, list): for m in msg: - super(SFTPClient, self)._log(level, "[chan %s] " + m, *([self.sock.get_name()] + list(args))) + self._log(level, m, *args) else: + # escape '%' in msg (they could come from file or directory names) before logging + msg = msg.replace('%','%%') super(SFTPClient, self)._log(level, "[chan %s] " + msg, *([self.sock.get_name()] + list(args))) def close(self): diff --git a/paramiko/sftp_server.py b/paramiko/sftp_server.py index dadfd026..2d8d1909 100644 --- a/paramiko/sftp_server.py +++ b/paramiko/sftp_server.py @@ -22,9 +22,9 @@ Server-mode SFTP support. import os import errno - -from Crypto.Hash import MD5, SHA import sys +from hashlib import md5, sha1 + from paramiko import util from paramiko.sftp import BaseSFTP, Message, SFTP_FAILURE, \ SFTP_PERMISSION_DENIED, SFTP_NO_SUCH_FILE @@ -45,8 +45,8 @@ from paramiko.sftp import CMD_HANDLE, SFTP_DESC, CMD_STATUS, SFTP_EOF, CMD_NAME, CMD_READLINK, CMD_SYMLINK, CMD_REALPATH, CMD_EXTENDED, SFTP_OP_UNSUPPORTED _hash_class = { - 'sha1': SHA, - 'md5': MD5, + 'sha1': sha1, + 'md5': md5, } @@ -82,14 +82,14 @@ class SFTPServer (BaseSFTP, SubsystemHandler): self.file_table = {} self.folder_table = {} self.server = sftp_si(server, *largs, **kwargs) - + def _log(self, level, msg): if issubclass(type(msg), list): for m in msg: super(SFTPServer, self)._log(level, "[chan " + self.sock.get_name() + "] " + m) else: super(SFTPServer, self)._log(level, "[chan " + self.sock.get_name() + "] " + msg) - + def start_subsystem(self, name, transport, channel): self.sock = channel self._log(DEBUG, 'Started sftp server on channel %s' % repr(channel)) @@ -157,7 +157,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler): This is meant to be a handy helper function for translating SFTP file requests into local file operations. - + :param str filename: name of the file to alter (should usually be an absolute path). :param .SFTPAttributes attr: attributes to change. @@ -281,7 +281,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler): # don't try to read more than about 64KB at a time chunklen = min(blocklen, 65536) count = 0 - hash_obj = alg.new() + hash_obj = alg() while count < blocklen: data = f.read(offset, chunklen) if not isinstance(data, bytes_types): @@ -298,7 +298,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler): msg.add_string(algname) msg.add_bytes(sum_out) self._send_packet(CMD_EXTENDED_REPLY, msg) - + def _convert_pflags(self, pflags): """convert SFTP-style open() flags to Python's os.open() flags""" if (pflags & SFTP_FLAG_READ) and (pflags & SFTP_FLAG_WRITE): diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index 63ca6409..b99e42b3 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -59,7 +59,9 @@ class BadAuthenticationType (AuthenticationException): def __init__(self, explanation, types): AuthenticationException.__init__(self, explanation) self.allowed_types = types - + # for unpickling + self.args = (explanation, types, ) + def __str__(self): return SSHException.__str__(self) + ' (allowed_types=%r)' % self.allowed_types @@ -73,6 +75,8 @@ class PartialAuthentication (AuthenticationException): def __init__(self, types): AuthenticationException.__init__(self, 'partial authentication') self.allowed_types = types + # for unpickling + self.args = (types, ) class ChannelException (SSHException): @@ -86,6 +90,8 @@ class ChannelException (SSHException): def __init__(self, code, text): SSHException.__init__(self, text) self.code = code + # for unpickling + self.args = (code, text, ) class BadHostKeyException (SSHException): @@ -103,6 +109,8 @@ class BadHostKeyException (SSHException): self.hostname = hostname self.key = got_key self.expected_key = expected_key + # for unpickling + self.args = (hostname, got_key, expected_key, ) class ProxyCommandFailure (SSHException): @@ -119,3 +127,5 @@ class ProxyCommandFailure (SSHException): ) ) self.error = error + # for unpickling + self.args = (command, error, ) diff --git a/paramiko/transport.py b/paramiko/transport.py index 86b5e7e4..86c9130c 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -20,18 +20,20 @@ Core protocol implementation """ +import os import socket import sys import threading import time import weakref +from hashlib import md5, sha1 import paramiko from paramiko import util from paramiko.auth_handler import AuthHandler from paramiko.ssh_gss import GSSAuth 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, \ @@ -60,9 +62,7 @@ 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: from Crypto.Util import Counter except ImportError: @@ -110,10 +110,10 @@ class Transport (threading.Thread): } _mac_info = { - 'hmac-sha1': {'class': SHA, 'size': 20}, - 'hmac-sha1-96': {'class': SHA, 'size': 12}, - 'hmac-md5': {'class': MD5, 'size': 16}, - 'hmac-md5-96': {'class': MD5, 'size': 12}, + 'hmac-sha1': {'class': sha1, 'size': 20}, + 'hmac-sha1-96': {'class': sha1, 'size': 12}, + 'hmac-md5': {'class': md5, 'size': 16}, + 'hmac-md5-96': {'class': md5, 'size': 12}, } _key_info = { @@ -199,7 +199,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: @@ -373,7 +372,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: @@ -509,7 +507,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: @@ -766,8 +764,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): @@ -1434,13 +1432,13 @@ class Transport (threading.Thread): m.add_bytes(self.H) m.add_byte(b(id)) m.add_bytes(self.session_id) - out = sofar = SHA.new(m.asbytes()).digest() + out = sofar = sha1(m.asbytes()).digest() while len(out) < nbytes: m = Message() m.add_mpint(self.K) m.add_bytes(self.H) m.add_bytes(sofar) - digest = SHA.new(m.asbytes()).digest() + digest = sha1(m.asbytes()).digest() out += digest sofar += digest return out[:nbytes] @@ -1498,10 +1496,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: @@ -1686,7 +1680,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) @@ -1815,9 +1809,9 @@ class Transport (threading.Thread): # initial mac keys are done in the hash's natural size (not the potentially truncated # transmission size) if self.server_mode: - mac_key = self._compute_key('E', mac_engine.digest_size) + mac_key = self._compute_key('E', mac_engine().digest_size) else: - mac_key = self._compute_key('F', mac_engine.digest_size) + mac_key = self._compute_key('F', mac_engine().digest_size) self.packetizer.set_inbound_cipher(engine, block_size, mac_engine, mac_size, mac_key) compress_in = self._compression_info[self.remote_compression][1] if (compress_in is not None) and ((self.remote_compression != 'zlib@openssh.com') or self.authenticated): @@ -1842,9 +1836,9 @@ class Transport (threading.Thread): # initial mac keys are done in the hash's natural size (not the potentially truncated # transmission size) if self.server_mode: - mac_key = self._compute_key('F', mac_engine.digest_size) + mac_key = self._compute_key('F', mac_engine().digest_size) else: - mac_key = self._compute_key('E', mac_engine.digest_size) + mac_key = self._compute_key('E', mac_engine().digest_size) sdctr = self.local_cipher.endswith('-ctr') self.packetizer.set_outbound_cipher(engine, block_size, mac_engine, mac_size, mac_key, sdctr) compress_out = self._compression_info[self.local_compression][0] diff --git a/paramiko/util.py b/paramiko/util.py index dbcbbae4..f4ee3adc 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -143,15 +143,14 @@ def tb_strings(): return ''.join(traceback.format_exception(*sys.exc_info())).split('\n') -def generate_key_bytes(hashclass, salt, key, nbytes): +def generate_key_bytes(hash_alg, salt, key, nbytes): """ Given a password, passphrase, or other human-source key, scramble it through a secure hash into some keyworthy bytes. This specific algorithm is used for encrypting/decrypting private key files. - :param class hashclass: - class from `Crypto.Hash` that can be used as a secure hashing function - (like ``MD5`` or ``SHA``). + :param function hash_alg: A function which creates a new hash object, such + as ``hashlib.sha256``. :param salt: data to salt the hash with. :type salt: byte string :param str key: human-entered password or passphrase. @@ -163,7 +162,7 @@ def generate_key_bytes(hashclass, salt, key, nbytes): if len(salt) > 8: salt = salt[:8] while nbytes > 0: - hash_obj = hashclass.new() + hash_obj = hash_alg() if len(digest) > 0: hash_obj.update(digest) hash_obj.update(b(key)) diff --git a/sites/docs/conf.py b/sites/docs/conf.py index f9355715..5674fed1 100644 --- a/sites/docs/conf.py +++ b/sites/docs/conf.py @@ -5,16 +5,11 @@ sys.path.append(os.path.abspath('../..')) from shared_conf import * # Enable autodoc, intersphinx -extensions.extend(['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']) +extensions.extend(['sphinx.ext.autodoc']) # Autodoc settings autodoc_default_flags = ['members', 'special-members'] -# Intersphinx connection to stdlib -intersphinx_mapping = { - 'python': ('http://docs.python.org/2.6', None), -} - # Sister-site links to WWW html_theme_options['extra_nav_links'] = { "Main website": 'http://www.paramiko.org', diff --git a/sites/shared_conf.py b/sites/shared_conf.py index c265fc49..69908388 100644 --- a/sites/shared_conf.py +++ b/sites/shared_conf.py @@ -5,7 +5,7 @@ import alabaster # Alabaster theme + mini-extension html_theme_path = [alabaster.get_path()] -extensions = ['alabaster'] +extensions = ['alabaster', 'sphinx.ext.intersphinx'] # Paths relative to invoking conf.py - not this shared file html_theme = 'alabaster' html_theme_options = { @@ -14,6 +14,7 @@ html_theme_options = { 'github_repo': 'paramiko', 'gittip_user': 'bitprophet', 'analytics_id': 'UA-18486793-2', + 'travis_button': True, } html_sidebars = { '**': [ @@ -24,6 +25,11 @@ html_sidebars = { ] } +# Everything intersphinx's to Python +intersphinx_mapping = { + 'python': ('http://docs.python.org/2.6', None), +} + # Regular settings project = 'Paramiko' year = datetime.now().year diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 7ac09abd..0680eb38 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -3,6 +3,20 @@ Changelog ========= * :feature:`250` GSS-API / SSPI authenticated Diffie-Hellman Key Exchange and user authentication. +* :bug:`-` Added self.args for exception classes. Used for unpickling. Related + to (`Fabric #986 <https://github.com/fabric/fabric/issues/986>`_, `Fabric + #714 <https://github.com/fabric/fabric/issues/714>`_). Thanks to Alex + Plugaru. +* :bug:`-` Fix logging error in sftp_client for filenames containing the '%' + character. Thanks to Antoine Brenner. +* :bug:`308` Fix regression in dsskey.py that caused sporadic signature + verification failures. Thanks to Chris Rose. +* :support:`299` Use deterministic signatures for ECDSA keys for improved + security. Thanks to Alex Gaynor. +* :support:`297` Replace PyCrypto's ``Random`` with `os.urandom` for improved + speed and security. Thanks again to Alex. +* :support:`295` Swap out a bunch of PyCrypto hash functions with use of + `hashlib`. Thanks to Alex Gaynor. * :support:`290` (also :issue:`292`) Add support for building universal (Python 2+3 compatible) wheel files during the release process. Courtesy of Alex Gaynor. diff --git a/sites/www/conf.py b/sites/www/conf.py index 5047fa67..bdb5929a 100644 --- a/sites/www/conf.py +++ b/sites/www/conf.py @@ -20,9 +20,7 @@ target = join(dirname(__file__), '..', 'docs', '_build') if os.environ.get('READTHEDOCS') == 'True': # TODO: switch to docs.paramiko.org post go-live of sphinx API docs target = 'http://docs.paramiko.org/en/latest/' -intersphinx_mapping = { - 'docs': (target, None), -} +intersphinx_mapping['docs'] = (target, None) # Sister-site links to API docs html_theme_options['extra_nav_links'] = { diff --git a/sites/www/contributing.rst b/sites/www/contributing.rst index 2b752cc5..634c2b26 100644 --- a/sites/www/contributing.rst +++ b/sites/www/contributing.rst @@ -17,3 +17,6 @@ How to submit bug reports or new code Please see `this project-agnostic contribution guide <http://contribution-guide.org>`_ - we follow it explicitly. + +Our current changelog is located in ``sites/www/changelog.rst`` - the top +level files like ``ChangeLog.*`` and ``NEWS`` are historical only. @@ -126,12 +126,12 @@ def main(): parser.add_option('--server_mode', action='store_true', dest='server_mode', default=False, help='Usage with --gssapi-test. Test the available GSS-API / SSPI server mode to.\ Note: you need to have access to the kerberos keytab file.') - + 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_packetizer.py b/tests/test_packetizer.py index d4d5544e..a8c0f973 100644 --- a/tests/test_packetizer.py +++ b/tests/test_packetizer.py @@ -21,9 +21,12 @@ Some unit tests for the ssh2 protocol in Transport. """ import unittest +from hashlib import sha1 + from tests.loop import LoopSocket + from Crypto.Cipher import AES -from Crypto.Hash import SHA + from paramiko import Message, Packetizer, util from paramiko.common import byte_chr, zero_byte @@ -41,7 +44,7 @@ class PacketizerTest (unittest.TestCase): p.set_log(util.get_logger('paramiko.transport')) p.set_hexdump(True) cipher = AES.new(zero_byte * 16, AES.MODE_CBC, x55 * 16) - p.set_outbound_cipher(cipher, 16, SHA, 12, x1f * 20) + p.set_outbound_cipher(cipher, 16, sha1, 12, x1f * 20) # message has to be at least 16 bytes long, so we'll have at least one # block of data encrypted that contains zero random padding bytes @@ -64,7 +67,7 @@ class PacketizerTest (unittest.TestCase): p.set_log(util.get_logger('paramiko.transport')) p.set_hexdump(True) cipher = AES.new(zero_byte * 16, AES.MODE_CBC, x55 * 16) - p.set_inbound_cipher(cipher, 16, SHA, 12, x1f * 20) + p.set_inbound_cipher(cipher, 16, sha1, 12, x1f * 20) wsock.send(b'\x43\x91\x97\xbd\x5b\x50\xac\x25\x87\xc2\xc4\x6b\xc7\xe9\x38\xc0\x90\xd2\x16\x56\x0d\x71\x73\x61\x38\x7c\x4c\x3d\xfb\x97\x7d\xe2\x6e\x03\xb1\xa0\xc2\x1c\xd6\x41\x41\x4c\xb4\x59') cmd, m = p.read_message() self.assertEqual(100, cmd) diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 6ff68fc2..1468ee27 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -20,11 +20,13 @@ Some unit tests for public/private key objects. """ -from binascii import hexlify import unittest +from binascii import hexlify +from hashlib import md5 + 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 @@ -90,8 +92,7 @@ class KeyTest (unittest.TestCase): pass def test_1_generate_key_bytes(self): - from Crypto.Hash import MD5 - key = util.generate_key_bytes(MD5, x1234, 'happy birthday', 30) + 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' self.assertEqual(exp, key) @@ -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_sftp.py b/tests/test_sftp.py index 6417ac90..e0534eb0 100755 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -37,6 +37,7 @@ from paramiko.common import o777, o600, o666, o644 from tests.stub_sftp import StubServer, StubSFTPServer from tests.loop import LoopSocket from tests.util import test_path +import paramiko.util from paramiko.sftp_attr import SFTPAttributes ARTICLE = ''' @@ -732,7 +733,23 @@ class SFTPTest (unittest.TestCase): sftp.remove(target) + def test_N_file_with_percent(self): + """ + verify that we can create a file with a '%' in the filename. + ( it needs to be properly escaped by _log() ) + """ + self.assertTrue( paramiko.util.get_logger("paramiko").handlers, "This unit test requires logging to be enabled" ) + f = sftp.open(FOLDER + '/test%file', 'w') + try: + self.assertEqual(f.stat().st_size, 0) + finally: + f.close() + sftp.remove(FOLDER + '/test%file') + + if __name__ == '__main__': SFTPTest.init_loopback() + # logging is required by test_N_file_with_percent + paramiko.util.log_to_file('test_sftp.log') from unittest import main main() diff --git a/tests/test_util.py b/tests/test_util.py index 6bde4045..69c75518 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') @@ -153,12 +154,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.* @@ -1,5 +1,5 @@ [tox] -envlist = py25,py26,py27,py32,py33 +envlist = py26,py27,py32,py33 [testenv] commands = pip install --use-mirrors -q -r tox-requirements.txt |