aboutsummaryrefslogtreecommitdiff
path: root/paramiko/transport.py
diff options
context:
space:
mode:
Diffstat (limited to 'paramiko/transport.py')
-rw-r--r--paramiko/transport.py220
1 files changed, 184 insertions, 36 deletions
diff --git a/paramiko/transport.py b/paramiko/transport.py
index 31c27a2..18fb103 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -20,13 +20,14 @@
Core protocol implementation
"""
+from __future__ import print_function
import os
import socket
import sys
import threading
import time
import weakref
-from hashlib import md5, sha1
+from hashlib import md5, sha1, sha256, sha512
import paramiko
from paramiko import util
@@ -47,7 +48,7 @@ from paramiko.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \
MAX_WINDOW_SIZE, DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE
from paramiko.compress import ZlibCompressor, ZlibDecompressor
from paramiko.dsskey import DSSKey
-from paramiko.kex_gex import KexGex
+from paramiko.kex_gex import KexGex, KexGexSHA256
from paramiko.kex_group1 import KexGroup1
from paramiko.kex_group14 import KexGroup14
from paramiko.kex_gss import KexGSSGex, KexGSSGroup1, KexGSSGroup14, NullHostKey
@@ -94,27 +95,109 @@ class Transport (threading.Thread, ClosingContextManager):
_PROTO_ID = '2.0'
_CLIENT_ID = 'paramiko_%s' % paramiko.__version__
- _preferred_ciphers = ('aes128-ctr', 'aes256-ctr', 'aes128-cbc', 'blowfish-cbc',
- 'aes256-cbc', '3des-cbc', 'arcfour128', 'arcfour256')
- _preferred_macs = ('hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96')
- _preferred_keys = ('ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256')
- _preferred_kex = ( 'diffie-hellman-group14-sha1', 'diffie-hellman-group-exchange-sha1' , 'diffie-hellman-group1-sha1')
+ # These tuples of algorithm identifiers are in preference order; do not
+ # reorder without reason!
+ _preferred_ciphers = (
+ 'aes128-ctr',
+ 'aes192-ctr',
+ 'aes256-ctr',
+ 'aes128-cbc',
+ 'blowfish-cbc',
+ 'aes192-cbc',
+ 'aes256-cbc',
+ '3des-cbc',
+ 'arcfour128',
+ 'arcfour256',
+ )
+ _preferred_macs = (
+ 'hmac-sha2-256',
+ 'hmac-sha2-512',
+ 'hmac-md5',
+ 'hmac-sha1-96',
+ 'hmac-md5-96',
+ 'hmac-sha1',
+ )
+ _preferred_keys = (
+ 'ssh-rsa',
+ 'ssh-dss',
+ 'ecdsa-sha2-nistp256',
+ )
+ _preferred_kex = (
+ 'diffie-hellman-group1-sha1',
+ 'diffie-hellman-group14-sha1',
+ 'diffie-hellman-group-exchange-sha1',
+ 'diffie-hellman-group-exchange-sha256',
+ )
_preferred_compression = ('none',)
_cipher_info = {
- 'aes128-ctr': {'class': AES, 'mode': AES.MODE_CTR, 'block-size': 16, 'key-size': 16},
- 'aes256-ctr': {'class': AES, 'mode': AES.MODE_CTR, 'block-size': 16, 'key-size': 32},
- 'blowfish-cbc': {'class': Blowfish, 'mode': Blowfish.MODE_CBC, 'block-size': 8, 'key-size': 16},
- 'aes128-cbc': {'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 16},
- 'aes256-cbc': {'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 32},
- '3des-cbc': {'class': DES3, 'mode': DES3.MODE_CBC, 'block-size': 8, 'key-size': 24},
- 'arcfour128': {'class': ARC4, 'mode': None, 'block-size': 8, 'key-size': 16},
- 'arcfour256': {'class': ARC4, 'mode': None, 'block-size': 8, 'key-size': 32},
+ 'aes128-ctr': {
+ 'class': AES,
+ 'mode': AES.MODE_CTR,
+ 'block-size': 16,
+ 'key-size': 16
+ },
+ 'aes192-ctr': {
+ 'class': AES,
+ 'mode': AES.MODE_CTR,
+ 'block-size': 16,
+ 'key-size': 24
+ },
+ 'aes256-ctr': {
+ 'class': AES,
+ 'mode': AES.MODE_CTR,
+ 'block-size': 16,
+ 'key-size': 32
+ },
+ 'blowfish-cbc': {
+ 'class': Blowfish,
+ 'mode': Blowfish.MODE_CBC,
+ 'block-size': 8,
+ 'key-size': 16
+ },
+ 'aes128-cbc': {
+ 'class': AES,
+ 'mode': AES.MODE_CBC,
+ 'block-size': 16,
+ 'key-size': 16
+ },
+ 'aes192-cbc': {
+ 'class': AES,
+ 'mode': AES.MODE_CBC,
+ 'block-size': 16,
+ 'key-size': 24
+ },
+ 'aes256-cbc': {
+ 'class': AES,
+ 'mode': AES.MODE_CBC,
+ 'block-size': 16,
+ 'key-size': 32
+ },
+ '3des-cbc': {
+ 'class': DES3,
+ 'mode': DES3.MODE_CBC,
+ 'block-size': 8,
+ 'key-size': 24
+ },
+ 'arcfour128': {
+ 'class': ARC4,
+ 'mode': None,
+ 'block-size': 8,
+ 'key-size': 16
+ },
+ 'arcfour256': {
+ 'class': ARC4,
+ 'mode': None,
+ 'block-size': 8,
+ 'key-size': 32
+ },
}
_mac_info = {
'hmac-sha1': {'class': sha1, 'size': 20},
'hmac-sha1-96': {'class': sha1, 'size': 12},
+ 'hmac-sha2-256': {'class': sha256, 'size': 32},
+ 'hmac-sha2-512': {'class': sha512, 'size': 64},
'hmac-md5': {'class': md5, 'size': 16},
'hmac-md5-96': {'class': md5, 'size': 12},
}
@@ -129,6 +212,7 @@ class Transport (threading.Thread, ClosingContextManager):
'diffie-hellman-group1-sha1': KexGroup1,
'diffie-hellman-group14-sha1': KexGroup14,
'diffie-hellman-group-exchange-sha1': KexGex,
+ 'diffie-hellman-group-exchange-sha256': KexGexSHA256,
'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGroup1,
'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGroup14,
'gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGex
@@ -1294,6 +1378,27 @@ class Transport (threading.Thread, ClosingContextManager):
self.auth_handler.auth_interactive(username, handler, my_event, submethods)
return self.auth_handler.wait_for_response(my_event)
+ def auth_interactive_dumb(self, username, handler=None, submethods=''):
+ """
+ Autenticate to the server interactively but dumber.
+ Just print the prompt and / or instructions to stdout and send back
+ the response. This is good for situations where partial auth is
+ achieved by key and then the user has to enter a 2fac token.
+ """
+
+ if not handler:
+ def handler(title, instructions, prompt_list):
+ answers = []
+ if title:
+ print(title.strip())
+ if instructions:
+ print(instructions.strip())
+ for prompt,show_input in prompt_list:
+ print(prompt.strip(),end=' ')
+ answers.append(raw_input())
+ return answers
+ return self.auth_interactive(username, handler, submethods)
+
def auth_gssapi_with_mic(self, username, gss_host, gss_deleg_creds):
"""
Authenticate to the Server using GSS-API / SSPI.
@@ -1507,13 +1612,23 @@ class Transport (threading.Thread, ClosingContextManager):
m.add_bytes(self.H)
m.add_byte(b(id))
m.add_bytes(self.session_id)
- out = sofar = sha1(m.asbytes()).digest()
+ # Fallback to SHA1 for kex engines that fail to specify a hex
+ # algorithm, or for e.g. transport tests that don't run kexinit.
+ hash_algo = getattr(self.kex_engine, 'hash_algo', None)
+ hash_select_msg = "kex engine %s specified hash_algo %r" % (self.kex_engine.__class__.__name__, hash_algo)
+ if hash_algo is None:
+ hash_algo = sha1
+ hash_select_msg += ", falling back to sha1"
+ if not hasattr(self, '_logged_hash_selection'):
+ self._log(DEBUG, hash_select_msg)
+ setattr(self, '_logged_hash_selection', True)
+ out = sofar = hash_algo(m.asbytes()).digest()
while len(out) < nbytes:
m = Message()
m.add_mpint(self.K)
m.add_bytes(self.H)
m.add_bytes(sofar)
- digest = sha1(m.asbytes()).digest()
+ digest = hash_algo(m.asbytes()).digest()
out += digest
sofar += digest
return out[:nbytes]
@@ -1591,10 +1706,13 @@ class Transport (threading.Thread, ClosingContextManager):
try:
try:
self.packetizer.write_all(b(self.local_version + '\r\n'))
+ self._log(DEBUG, 'Local version/idstring: %s' % self.local_version)
self._check_banner()
- # The above is actually very much part of the handshake, but sometimes the banner can be read
- # but the machine is not responding, for example when the remote ssh daemon is loaded in to memory
- # but we can not read from the disk/spawn a new shell.
+ # The above is actually very much part of the handshake, but
+ # sometimes the banner can be read but the machine is not
+ # responding, for example when the remote ssh daemon is loaded
+ # in to memory but we can not read from the disk/spawn a new
+ # shell.
# Make sure we can specify a timeout for the initial handshake.
# Re-use the banner timeout for now.
self.packetizer.start_handshake(self.handshake_timeout)
@@ -1696,6 +1814,18 @@ class Transport (threading.Thread, ClosingContextManager):
if self.sys.modules is not None:
raise
+
+ def _log_agreement(self, which, local, remote):
+ # Log useful, non-duplicative line re: an agreed-upon algorithm.
+ # Old code implied algorithms could be asymmetrical (different for
+ # inbound vs outbound) so we preserve that possibility.
+ msg = "{0} agreed: ".format(which)
+ if local == remote:
+ msg += local
+ else:
+ msg += "local={0}, remote={1}".format(local, remote)
+ self._log(DEBUG, msg)
+
### protocol stages
def _negotiate_keys(self, m):
@@ -1733,6 +1863,7 @@ class Transport (threading.Thread, ClosingContextManager):
raise SSHException('Indecipherable protocol version "' + buf + '"')
# save this server version string for later
self.remote_version = buf
+ self._log(DEBUG, 'Remote version/idstring: %s' % buf)
# pull off any attached comment
comment = ''
i = buf.find(' ')
@@ -1761,10 +1892,12 @@ class Transport (threading.Thread, ClosingContextManager):
self.clear_to_send_lock.release()
self.in_kex = True
if self.server_mode:
- if (self._modulus_pack is None) and ('diffie-hellman-group-exchange-sha1' in self._preferred_kex):
+ mp_required_prefix = 'diffie-hellman-group-exchange-sha'
+ kex_mp = [k for k in self._preferred_kex if k.startswith(mp_required_prefix)]
+ if (self._modulus_pack is None) and (len(kex_mp) > 0):
# can't do group-exchange if we don't have a pack of potential primes
- pkex = list(self.get_security_options().kex)
- pkex.remove('diffie-hellman-group-exchange-sha1')
+ pkex = [k for k in self.get_security_options().kex
+ if not k.startswith(mp_required_prefix)]
self.get_security_options().kex = pkex
available_server_keys = list(filter(list(self.server_key_dict.keys()).__contains__,
self._preferred_keys))
@@ -1816,15 +1949,24 @@ class Transport (threading.Thread, ClosingContextManager):
' server lang:' + str(server_lang_list) +
' kex follows?' + str(kex_follows))
- # as a server, we pick the first item in the client's list that we support.
- # as a client, we pick the first item in our list that the server supports.
+ # as a server, we pick the first item in the client's list that we
+ # support.
+ # as a client, we pick the first item in our list that the server
+ # supports.
if self.server_mode:
- agreed_kex = list(filter(self._preferred_kex.__contains__, kex_algo_list))
+ agreed_kex = list(filter(
+ self._preferred_kex.__contains__,
+ kex_algo_list
+ ))
else:
- agreed_kex = list(filter(kex_algo_list.__contains__, self._preferred_kex))
+ agreed_kex = list(filter(
+ kex_algo_list.__contains__,
+ self._preferred_kex
+ ))
if len(agreed_kex) == 0:
raise SSHException('Incompatible ssh peer (no acceptable kex algorithm)')
self.kex_engine = self._kex_info[agreed_kex[0]](self)
+ self._log(DEBUG, "Kex agreed: %s" % agreed_kex[0])
if self.server_mode:
available_server_keys = list(filter(list(self.server_key_dict.keys()).__contains__,
@@ -1852,7 +1994,9 @@ class Transport (threading.Thread, ClosingContextManager):
raise SSHException('Incompatible ssh server (no acceptable ciphers)')
self.local_cipher = agreed_local_ciphers[0]
self.remote_cipher = agreed_remote_ciphers[0]
- self._log(DEBUG, 'Ciphers agreed: local=%s, remote=%s' % (self.local_cipher, self.remote_cipher))
+ self._log_agreement(
+ 'Cipher', local=self.local_cipher, remote=self.remote_cipher
+ )
if self.server_mode:
agreed_remote_macs = list(filter(self._preferred_macs.__contains__, client_mac_algo_list))
@@ -1864,6 +2008,9 @@ class Transport (threading.Thread, ClosingContextManager):
raise SSHException('Incompatible ssh server (no acceptable macs)')
self.local_mac = agreed_local_macs[0]
self.remote_mac = agreed_remote_macs[0]
+ self._log_agreement(
+ 'MAC', local=self.local_mac, remote=self.remote_mac
+ )
if self.server_mode:
agreed_remote_compression = list(filter(self._preferred_compression.__contains__, client_compress_algo_list))
@@ -1875,10 +2022,11 @@ class Transport (threading.Thread, ClosingContextManager):
raise SSHException('Incompatible ssh server (no acceptable compression) %r %r %r' % (agreed_local_compression, agreed_remote_compression, self._preferred_compression))
self.local_compression = agreed_local_compression[0]
self.remote_compression = agreed_remote_compression[0]
-
- self._log(DEBUG, 'using kex %s; server key type %s; cipher: local %s, remote %s; mac: local %s, remote %s; compression: local %s, remote %s' %
- (agreed_kex[0], self.host_key_type, self.local_cipher, self.remote_cipher, self.local_mac,
- self.remote_mac, self.local_compression, self.remote_compression))
+ self._log_agreement(
+ 'Compression',
+ local=self.local_compression,
+ remote=self.remote_compression
+ )
# save for computing hash later...
# now wait! openssh has a bug (and others might too) where there are
@@ -1899,8 +2047,8 @@ class Transport (threading.Thread, ClosingContextManager):
engine = self._get_cipher(self.remote_cipher, key_in, IV_in)
mac_size = self._mac_info[self.remote_mac]['size']
mac_engine = self._mac_info[self.remote_mac]['class']
- # initial mac keys are done in the hash's natural size (not the potentially truncated
- # transmission size)
+ # 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)
else:
@@ -1926,8 +2074,8 @@ class Transport (threading.Thread, ClosingContextManager):
engine = self._get_cipher(self.local_cipher, key_out, IV_out)
mac_size = self._mac_info[self.local_mac]['size']
mac_engine = self._mac_info[self.local_mac]['class']
- # initial mac keys are done in the hash's natural size (not the potentially truncated
- # transmission size)
+ # 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)
else: