Skip to content

Commit

Permalink
Merge branch 'master' into 979-int
Browse files Browse the repository at this point in the history
  • Loading branch information
bitprophet committed Sep 6, 2017
2 parents cf14b9f + 6ab07ec commit 57af309
Show file tree
Hide file tree
Showing 25 changed files with 504 additions and 57 deletions.
4 changes: 2 additions & 2 deletions paramiko/__init__.py
Expand Up @@ -30,7 +30,7 @@

from paramiko.transport import SecurityOptions, Transport
from paramiko.client import (
SSHClient, MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy,
SSHClient, MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy,
WarningPolicy,
)
from paramiko.auth_handler import AuthHandler
Expand All @@ -57,7 +57,7 @@
from paramiko.packet import Packetizer
from paramiko.file import BufferedFile
from paramiko.agent import Agent, AgentKey
from paramiko.pkey import PKey
from paramiko.pkey import PKey, PublicBlob
from paramiko.hostkeys import HostKeys
from paramiko.config import SSHConfig
from paramiko.proxy import ProxyCommand
Expand Down
1 change: 1 addition & 0 deletions paramiko/agent.py
Expand Up @@ -387,6 +387,7 @@ class AgentKey(PKey):
def __init__(self, agent, blob):
self.agent = agent
self.blob = blob
self.public_blob = None
self.name = Message(blob).get_text()

def asbytes(self):
Expand Down
34 changes: 26 additions & 8 deletions paramiko/auth_handler.py
Expand Up @@ -36,6 +36,7 @@
cMSG_USERAUTH_GSSAPI_MIC, MSG_USERAUTH_GSSAPI_RESPONSE,
MSG_USERAUTH_GSSAPI_TOKEN, MSG_USERAUTH_GSSAPI_ERROR,
MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC, MSG_NAMES,
cMSG_USERAUTH_BANNER
)
from paramiko.message import Message
from paramiko.py3compat import bytestring
Expand Down Expand Up @@ -186,8 +187,13 @@ def _get_session_blob(self, key, service, username):
m.add_string(service)
m.add_string('publickey')
m.add_boolean(True)
m.add_string(key.get_name())
m.add_string(key)
# Use certificate contents, if available, plain pubkey otherwise
if key.public_blob:
m.add_string(key.public_blob.key_type)
m.add_string(key.public_blob.key_blob)
else:
m.add_string(key.get_name())
m.add_string(key)
return m.asbytes()

def wait_for_response(self, event):
Expand Down Expand Up @@ -225,6 +231,13 @@ def _parse_service_request(self, m):
m.add_byte(cMSG_SERVICE_ACCEPT)
m.add_string(service)
self.transport._send_message(m)
banner, language = self.transport.server_object.get_banner()
if banner:
m = Message()
m.add_byte(cMSG_USERAUTH_BANNER)
m.add_string(banner)
m.add_string(language)
self.transport._send_message(m)
return
# dunno this one
self._disconnect_service_not_available()
Expand All @@ -244,8 +257,14 @@ def _parse_service_accept(self, m):
m.add_string(password)
elif self.auth_method == 'publickey':
m.add_boolean(True)
m.add_string(self.private_key.get_name())
m.add_string(self.private_key)
# Use certificate contents, if available, plain pubkey
# otherwise
if self.private_key.public_blob:
m.add_string(self.private_key.public_blob.key_type)
m.add_string(self.private_key.public_blob.key_blob)
else:
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(blob)
Expand Down Expand Up @@ -448,10 +467,9 @@ def _parse_userauth_request(self, m):
INFO,
'Auth rejected: public key: %s' % str(e))
key = None
except:
self.transport._log(
INFO,
'Auth rejected: unsupported or mangled public key')
except Exception as e:
msg = 'Auth rejected: unsupported or mangled public key ({0}: {1})' # noqa
self.transport._log(INFO, msg.format(e.__class__.__name__, e))
key = None
if key is None:
self._disconnect_no_more_auth()
Expand Down
73 changes: 58 additions & 15 deletions paramiko/client.py
Expand Up @@ -240,9 +240,23 @@ def connect(
Authentication is attempted in the following order of priority:
- The ``pkey`` or ``key_filename`` passed in (if any)
- ``key_filename`` may contain OpenSSH public certificate paths
as well as regular private-key paths; when files ending in
``-cert.pub`` are found, they are assumed to match a private
key, and both components will be loaded. (The private key
itself does *not* need to be listed in ``key_filename`` for
this to occur - *just* the certificate.)
- Any key we can find through an SSH agent
- Any "id_rsa", "id_dsa" or "id_ecdsa" key discoverable in
``~/.ssh/``
- When OpenSSH-style public certificates exist that match an
existing such private key (so e.g. one has ``id_rsa`` and
``id_rsa-cert.pub``) the certificate will be loaded alongside
the private key and used for authentication.
- Plain username/password auth, if a password was given
If a private key requires a password to unlock it, and a password is
Expand All @@ -257,8 +271,8 @@ def connect(
a password to use for authentication or for unlocking a private key
:param .PKey pkey: an optional private key to use for authentication
:param str key_filename:
the filename, or list of filenames, of optional private key(s) to
try for authentication
the filename, or list of filenames, of optional private key(s)
and/or certs to try for authentication
:param float timeout:
an optional timeout (in seconds) for the TCP connect
:param bool allow_agent:
Expand Down Expand Up @@ -499,12 +513,44 @@ def get_transport(self):
"""
return self._transport

def _key_from_filepath(self, filename, klass, password):
"""
Attempt to derive a `.PKey` from given string path ``filename``:
- If ``filename`` appears to be a cert, the matching private key is
loaded.
- Otherwise, the filename is assumed to be a private key, and the
matching public cert will be loaded if it exists.
"""
cert_suffix = '-cert.pub'
# Assume privkey, not cert, by default
if filename.endswith(cert_suffix):
key_path = filename[:-len(cert_suffix)]
cert_path = filename
else:
key_path = filename
cert_path = filename + cert_suffix
# Blindly try the key path; if no private key, nothing will work.
key = klass.from_private_key_file(key_path, password)
# TODO: change this to 'Loading' instead of 'Trying' sometime; probably
# when #387 is released, since this is a critical log message users are
# likely testing/filtering for (bah.)
msg = "Trying discovered key {0} in {1}".format(
hexlify(key.get_fingerprint()), key_path,
)
self._log(DEBUG, msg)
# Attempt to load cert if it exists.
if os.path.isfile(cert_path):
key.load_certificate(cert_path)
self._log(DEBUG, "Adding public certificate {0}".format(cert_path))
return key

def _auth(self, username, password, pkey, key_filenames, allow_agent,
look_for_keys, gss_auth, gss_kex, gss_deleg_creds, gss_host):
"""
Try, in order:
- The key passed in, if one was passed in.
- The key(s) passed in, if one was passed in.
- Any key we can find through an SSH agent (if allowed).
- Any "id_rsa", "id_dsa" or "id_ecdsa" key discoverable in ~/.ssh/
(if allowed).
Expand Down Expand Up @@ -556,12 +602,9 @@ def _auth(self, username, password, pkey, key_filenames, allow_agent,
for key_filename in key_filenames:
for pkey_class in (RSAKey, DSSKey, ECDSAKey, Ed25519Key):
try:
key = pkey_class.from_private_key_file(
key_filename, password)
self._log(
DEBUG,
'Trying key %s from %s' % (
hexlify(key.get_fingerprint()), key_filename))
key = self._key_from_filepath(
key_filename, pkey_class, password,
)
allowed_types = set(
self._transport.auth_publickey(username, key))
two_factor = (allowed_types & two_factor_types)
Expand Down Expand Up @@ -607,19 +650,19 @@ def _auth(self, username, password, pkey, key_filenames, allow_agent,
"~/%s/id_%s" % (directory, name)
)
if os.path.isfile(full_path):
# TODO: only do this append if below did not run
keyfiles.append((keytype, full_path))
if os.path.isfile(full_path + '-cert.pub'):
keyfiles.append((keytype, full_path + '-cert.pub'))

if not look_for_keys:
keyfiles = []

for pkey_class, filename in keyfiles:
try:
key = pkey_class.from_private_key_file(filename, password)
self._log(
DEBUG,
'Trying discovered key %s in %s' % (
hexlify(key.get_fingerprint()), filename))

key = self._key_from_filepath(
filename, pkey_class, password,
)
# for 2-factor auth a successfully auth'd key will result
# in ['password']
allowed_types = set(
Expand Down
3 changes: 2 additions & 1 deletion paramiko/compress.py
Expand Up @@ -25,7 +25,8 @@

class ZlibCompressor (object):
def __init__(self):
self.z = zlib.compressobj(9)
# Use the default level of zlib compression
self.z = zlib.compressobj()

def __call__(self, data):
return self.z.compress(data) + self.z.flush(zlib.Z_FULL_FLUSH)
Expand Down
10 changes: 6 additions & 4 deletions paramiko/dsskey.py
Expand Up @@ -49,6 +49,7 @@ def __init__(self, msg=None, data=None, filename=None, password=None,
self.g = None
self.y = None
self.x = None
self.public_blob = None
if file_obj is not None:
self._from_private_key(file_obj, password)
return
Expand All @@ -60,10 +61,11 @@ def __init__(self, msg=None, data=None, filename=None, password=None,
if vals is not None:
self.p, self.q, self.g, self.y = vals
else:
if msg is None:
raise SSHException('Key object may not be empty')
if msg.get_text() != 'ssh-dss':
raise SSHException('Invalid key')
self._check_type_and_load_cert(
msg=msg,
key_type='ssh-dss',
cert_type='ssh-dss-cert-v01@openssh.com',
)
self.p = msg.get_mpint()
self.q = msg.get_mpint()
self.g = msg.get_mpint()
Expand Down
28 changes: 23 additions & 5 deletions paramiko/ecdsakey.py
Expand Up @@ -105,6 +105,7 @@ def __init__(self, msg=None, data=None, filename=None, password=None,
vals=None, file_obj=None, validate_point=True):
self.verifying_key = None
self.signing_key = None
self.public_blob = None
if file_obj is not None:
self._from_private_key(file_obj, password)
return
Expand All @@ -118,12 +119,29 @@ def __init__(self, msg=None, data=None, filename=None, password=None,
c_class = self.signing_key.curve.__class__
self.ecdsa_curve = self._ECDSA_CURVES.get_by_curve_class(c_class)
else:
if msg is None:
raise SSHException('Key object may not be empty')
# Must set ecdsa_curve first; subroutines called herein may need to
# spit out our get_name(), which relies on this.
key_type = msg.get_text()
# But this also means we need to hand it a real key/curve
# identifier, so strip out any cert business. (NOTE: could push
# that into _ECDSACurveSet.get_by_key_format_identifier(), but it
# feels more correct to do it here?)
suffix = '-cert-v01@openssh.com'
if key_type.endswith(suffix):
key_type = key_type[:-len(suffix)]
self.ecdsa_curve = self._ECDSA_CURVES.get_by_key_format_identifier(
msg.get_text())
if self.ecdsa_curve is None:
raise SSHException('Invalid key')
key_type
)
key_types = self._ECDSA_CURVES.get_key_format_identifier_list()
cert_types = [
'{0}-cert-v01@openssh.com'.format(x)
for x in key_types
]
self._check_type_and_load_cert(
msg=msg,
key_type=key_types,
cert_type=cert_types,
)
curvename = msg.get_text()
if curvename != self.ecdsa_curve.nist_name:
raise SSHException("Can't handle curve of type %s" % curvename)
Expand Down
26 changes: 22 additions & 4 deletions paramiko/ed25519key.py
Expand Up @@ -45,18 +45,36 @@ def unpad(data):


class Ed25519Key(PKey):
def __init__(self, msg=None, data=None, filename=None, password=None):
"""
Representation of an `Ed25519 <https://ed25519.cr.yp.to/>`_ key.
.. note::
Ed25519 key support was added to OpenSSH in version 6.5.
.. versionadded:: 2.2
.. versionchanged:: 2.3
Added a ``file_obj`` parameter to match other key classes.
"""
def __init__(self, msg=None, data=None, filename=None, password=None,
file_obj=None):
verifying_key = signing_key = None
if msg is None and data is not None:
msg = Message(data)
if msg is not None:
if msg.get_text() != "ssh-ed25519":
raise SSHException("Invalid key")
self._check_type_and_load_cert(
msg=msg,
key_type="ssh-ed25519",
cert_type="ssh-ed25519-cert-v01@openssh.com",
)
verifying_key = nacl.signing.VerifyKey(msg.get_binary())
elif filename is not None:
with open(filename, "r") as f:
data = self._read_private_key("OPENSSH", f)
signing_key = self._parse_signing_key_data(data, password)
elif file_obj is not None:
data = self._read_private_key("OPENSSH", file_obj)

if filename or file_obj:
signing_key = self._parse_signing_key_data(data, password)

if signing_key is None and verifying_key is None:
raise ValueError("need a key")
Expand Down

0 comments on commit 57af309

Please sign in to comment.