summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeff Forcier <jeff@bitprophet.org>2017-09-11 10:54:18 -0700
committerJeff Forcier <jeff@bitprophet.org>2017-09-11 10:54:18 -0700
commitfb299ad1617cbe3c76310eab19370878d113366e (patch)
tree5004e5140b74a8b6f365fd2004c98f0f131b26e1
parent538bb118870a8b0dbaf35dd104b1178481c213b3 (diff)
parent2098abc1f26c572cc4257eb7270d4e8ebc627e9e (diff)
downloadparamiko-fb299ad1617cbe3c76310eab19370878d113366e.tar.gz
Merge branch '827-int' into final-827-int
-rw-r--r--dev-requirements.txt1
-rw-r--r--paramiko/__init__.py2
-rw-r--r--paramiko/agent.py1
-rw-r--r--paramiko/auth_handler.py98
-rw-r--r--paramiko/client.py95
-rw-r--r--paramiko/compress.py3
-rw-r--r--paramiko/dsskey.py19
-rw-r--r--paramiko/ecdsakey.py40
-rw-r--r--paramiko/ed25519key.py26
-rw-r--r--paramiko/pkcs11.py93
-rw-r--r--paramiko/pkey.py170
-rw-r--r--paramiko/rsakey.py25
-rw-r--r--paramiko/server.py13
-rw-r--r--paramiko/transport.py17
-rw-r--r--setup.py2
-rw-r--r--sites/docs/api/keys.rst5
-rw-r--r--sites/www/changelog.rst62
-rw-r--r--tasks.py3
-rwxr-xr-xtest.py2
-rw-r--r--tests/test_client.py59
-rw-r--r--tests/test_dss.key-cert.pub1
-rw-r--r--tests/test_ecdsa_256.key-cert.pub1
-rw-r--r--tests/test_ed25519.key-cert.pub1
-rw-r--r--tests/test_pkcs11.py166
-rw-r--r--tests/test_pkey.py33
-rw-r--r--tests/test_rsa.key-cert.pub1
-rw-r--r--tests/test_rsa.key.pub1
-rw-r--r--tests/util.py1
28 files changed, 773 insertions, 168 deletions
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 716f432d..eb7408b1 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -10,3 +10,4 @@ semantic_version<3.0
wheel==0.24
twine==1.5
flake8==2.6.2
+mock>=2.0.0
diff --git a/paramiko/__init__.py b/paramiko/__init__.py
index da8a093f..3b3b1f9d 100644
--- a/paramiko/__init__.py
+++ b/paramiko/__init__.py
@@ -58,7 +58,7 @@ from paramiko.message import Message
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
diff --git a/paramiko/agent.py b/paramiko/agent.py
index bc857efa..7a4dde21 100644
--- a/paramiko/agent.py
+++ b/paramiko/agent.py
@@ -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):
diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py
index 86935ecc..1090883b 100644
--- a/paramiko/auth_handler.py
+++ b/paramiko/auth_handler.py
@@ -39,6 +39,7 @@ from paramiko.common import (
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
@@ -51,6 +52,7 @@ from paramiko.ssh_gss import GSSAuth
from paramiko import pkcs11_open_session, pkcs11_close_session
from paramiko.pkcs11 import pkcs11_get_public_key
from .authentication import hostkey_from_text
+from paramiko.pkcs11 import PKCS11Exception
class AuthHandler (object):
@@ -77,10 +79,7 @@ class AuthHandler (object):
self.gss_host = None
self.gss_deleg_creds = True
# for PKCS11 / Smartcard
- self.pkcs11pin = None
- self.pkcs11provider = None
self.pkcs11session = None
- self.pkcs11publickey = None
if AuthHandler._pkcs11_lock is None:
AuthHandler._pkcs11_lock = threading.Lock()
self.pkcs11_lock = AuthHandler._pkcs11_lock
@@ -115,15 +114,12 @@ class AuthHandler (object):
finally:
self.transport.lock.release()
- def auth_pkcs11(self, username, pkcs11pin, pkcs11provider, pkcs11session,
- event):
+ def auth_pkcs11(self, username, pkcs11session, event):
self.transport.lock.acquire()
try:
self.auth_event = event
self.auth_method = 'publickey'
self.username = username
- self.pkcs11pin = pkcs11pin
- self.pkcs11provider = pkcs11provider
self.pkcs11session = pkcs11session
self._request_auth()
finally:
@@ -215,8 +211,13 @@ class AuthHandler (object):
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):
@@ -254,35 +255,39 @@ class AuthHandler (object):
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()
def _pkcs11_get_public_key(self):
- if self.pkcs11publickey is None:
- if self.pkcs11session is None:
- self.pkcs11publickey = pkcs11_get_public_key()
- else:
- self.pkcs11publickey = self.pkcs11session[1]
-
- if self.pkcs11publickey is None or len(self.pkcs11publickey) < 1:
- raise SSHException("Invalid ssh public key returned by \
- pkcs15-tool")
- return str(self.pkcs11publickey)
+ if "public_key" not in self.pkcs11session:
+ raise PKCS11Exception("pkcs11 session does not have a public_key")
+ if len(self.pkcs11session["public_key"]) < 1:
+ raise PKCS11Exception("pkcs11 session contains invalid public \
+ key %s"
+ % self.pkcs11session["public_key"])
+ return self.pkcs11session["public_key"]
def _pkcs11_sign_ssh_data(self, blob, key_name):
- if not os.path.isfile(self.pkcs11provider):
- raise SSHException("pkcs11provider path is not valid: %s"
- % self.pkcs11provider)
- lib = cdll.LoadLibrary(self.pkcs11provider)
- if self.pkcs11session is None:
- (session,
- public_key,
- keyret) = pkcs11_open_session(self.pkcs11provider,
- self.pkcs11pin,
- self.pkcs11publickey)
- else:
- (session, public_key, keyret) = self.pkcs11session
+ if "provider" not in self.pkcs11session:
+ raise PKCS11Exception("pkcs11 session does not have a provider")
+ if "session" not in self.pkcs11session:
+ raise PKCS11Exception("pkcs11 session does not have a session")
+ if "keyret" not in self.pkcs11session:
+ raise PKCS11Exception("pkcs11 session does not have a keyret")
+ if not os.path.isfile(self.pkcs11session["provider"]):
+ raise PKCS11Exception("pkcs11provider does not exist: %s"
+ % self.pkcs11session["provider"])
+ lib = cdll.LoadLibrary(self.pkcs11session["provider"])
+ session = self.pkcs11session["session"]
+ keyret = self.pkcs11session["keyret"]
# Init Signing Data
class ck_mechanism(Structure):
@@ -294,7 +299,7 @@ class AuthHandler (object):
self.pkcs11_lock.acquire()
res = lib.C_SignInit(session, byref(mech), keyret)
if res != 0:
- raise SSHException("PKCS11 Failed to Sign Init")
+ raise PKCS11Exception("PKCS11 Failed to Sign Init")
in_buffer = (c_char * 1025)()
sig_buffer = (c_char * 512)()
@@ -305,11 +310,8 @@ class AuthHandler (object):
r = c_int(len(blob))
res = lib.C_Sign(session, in_buffer, r, sig_buffer, byref(sig_len))
if res != 0:
- raise SSHException("PKCS11 Failed to Sign")
+ raise PKCS11Exception("PKCS11 Failed to Sign")
- if self.pkcs11session is None:
- # Let User Manage The Session, Do Not Close
- pkcs11_close_session(self.pkcs11provider)
self.pkcs11_lock.release()
# Convert ctype char array to python string
@@ -336,11 +338,17 @@ class AuthHandler (object):
password = bytestring(self.password)
m.add_string(password)
elif self.auth_method == 'publickey':
- if self.pkcs11pin is None:
- # Private Key
+ if self.pkcs11session is None:
m.add_boolean(True)
- m.add_string(self.private_key.get_name())
- m.add_string(self.private_key)
+ # 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)
@@ -351,7 +359,8 @@ class AuthHandler (object):
fields = pubkey_source.split(' ')
if len(fields) < 2:
- SSHException("Not enough fields found in pkcs11 key")
+ raise PKCS11Exception("Not enough fields \
+ found in pkcs11 key")
keytype = fields[0]
pub_key = fields[1]
@@ -566,10 +575,9 @@ class AuthHandler (object):
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()
diff --git a/paramiko/client.py b/paramiko/client.py
index cdfde2ec..f4e6791f 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -227,8 +227,6 @@ class SSHClient (ClosingContextManager):
gss_deleg_creds=True,
gss_host=None,
banner_timeout=None,
- pkcs11pin=None,
- pkcs11provider=None,
pkcs11session=None,
auth_timeout=None,
):
@@ -243,9 +241,23 @@ class SSHClient (ClosingContextManager):
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
@@ -260,8 +272,8 @@ class SSHClient (ClosingContextManager):
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:
@@ -282,12 +294,11 @@ class SSHClient (ClosingContextManager):
The targets name in the kerberos database. default: hostname
:param float banner_timeout: an optional timeout (in seconds) to wait
for the SSH banner to be presented.
- :param str pkcs11pin: If using PKCS11, this will be the pin of your
- token or smartcard.
- :param str pkcs11provider: If using PKCS11, this will be the provider
- for the PKCS11 interface. Example: /usr/local/lib/opensc-pkcs11.so.
- :param str pkcs11session: If using PKCS11 in a multithreaded
- application you can share the session between threads.
+ :param str pkcs11session: The pkcs11 session obtained by calling
+ pkcs11_open_session. If using PKCS11 in a multithreaded application
+ you can share the session between threads. You can make multiple
+ calls to connect using the same pkcs11 session. You must call
+ pkcs11_close_session to cleanly close the session.
:param float auth_timeout: an optional timeout (in seconds) to wait for
an authentication response.
@@ -401,7 +412,7 @@ class SSHClient (ClosingContextManager):
gss_host = hostname
self._auth(username, password, pkey, key_filenames, allow_agent,
look_for_keys, gss_auth, gss_kex, gss_deleg_creds, gss_host,
- pkcs11pin, pkcs11provider, pkcs11session)
+ pkcs11session)
def close(self):
"""
@@ -509,13 +520,45 @@ class SSHClient (ClosingContextManager):
"""
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,
- pkcs11pin, pkcs11provider, pkcs11session):
+ pkcs11session):
"""
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).
@@ -530,11 +573,10 @@ class SSHClient (ClosingContextManager):
two_factor_types = set(['keyboard-interactive', 'password'])
# PKCS11 / Smartcard authentication
- if username is not None and pkcs11pin is not None and \
- pkcs11provider is not None:
+ if username is not None and pkcs11session is not None:
try:
allowed_types = set(self._transport.auth_pkcs11(username,
- pkcs11pin, pkcs11provider, pkcs11session))
+ pkcs11session))
two_factor = (allowed_types & two_factor_types)
if not two_factor:
return
@@ -579,12 +621,9 @@ class SSHClient (ClosingContextManager):
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)
@@ -630,19 +669,19 @@ class SSHClient (ClosingContextManager):
"~/%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(
diff --git a/paramiko/compress.py b/paramiko/compress.py
index b55f0b1d..5073109c 100644
--- a/paramiko/compress.py
+++ b/paramiko/compress.py
@@ -25,7 +25,8 @@ import zlib
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)
diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py
index 9af5d0c1..ac1d4c2e 100644
--- a/paramiko/dsskey.py
+++ b/paramiko/dsskey.py
@@ -49,6 +49,7 @@ class DSSKey(PKey):
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
@@ -60,10 +61,11 @@ class DSSKey(PKey):
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()
@@ -106,9 +108,8 @@ class DSSKey(PKey):
)
)
).private_key(backend=default_backend())
- signer = key.signer(hashes.SHA1())
- signer.update(data)
- r, s = decode_dss_signature(signer.finalize())
+ sig = key.sign(data, hashes.SHA1())
+ r, s = decode_dss_signature(sig)
m = Message()
m.add_string('ssh-dss')
@@ -146,10 +147,8 @@ class DSSKey(PKey):
g=self.g
)
).public_key(backend=default_backend())
- verifier = key.verifier(signature, hashes.SHA1())
- verifier.update(data)
try:
- verifier.verify()
+ key.verify(signature, data, hashes.SHA1())
except InvalidSignature:
return False
else:
diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py
index fa850c2e..1bb5676f 100644
--- a/paramiko/ecdsakey.py
+++ b/paramiko/ecdsakey.py
@@ -105,6 +105,7 @@ class ECDSAKey(PKey):
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
@@ -118,12 +119,29 @@ class ECDSAKey(PKey):
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)
@@ -179,9 +197,7 @@ class ECDSAKey(PKey):
def sign_ssh_data(self, data):
ecdsa = ec.ECDSA(self.ecdsa_curve.hash_object())
- signer = self.signing_key.signer(ecdsa)
- signer.update(data)
- sig = signer.finalize()
+ sig = self.signing_key.sign(data, ecdsa)
r, s = decode_dss_signature(sig)
m = Message()
@@ -196,12 +212,10 @@ class ECDSAKey(PKey):
sigR, sigS = self._sigdecode(sig)
signature = encode_dss_signature(sigR, sigS)
- verifier = self.verifying_key.verifier(
- signature, ec.ECDSA(self.ecdsa_curve.hash_object())
- )
- verifier.update(data)
try:
- verifier.verify()
+ self.verifying_key.verify(
+ signature, data, ec.ECDSA(self.ecdsa_curve.hash_object())
+ )
except InvalidSignature:
return False
else:
diff --git a/paramiko/ed25519key.py b/paramiko/ed25519key.py
index a50d68bc..434b3f45 100644
--- a/paramiko/ed25519key.py
+++ b/paramiko/ed25519key.py
@@ -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")
diff --git a/paramiko/pkcs11.py b/paramiko/pkcs11.py
index f7b7f6bf..e8f85b55 100644
--- a/paramiko/pkcs11.py
+++ b/paramiko/pkcs11.py
@@ -3,13 +3,30 @@ from ctypes import (c_void_p, c_ulong, c_int, c_char_p, cast, addressof,
import subprocess
import os
import errno
-from paramiko.ssh_exception import SSHException
+from paramiko.ssh_exception import AuthenticationException, SSHException
-def pkcs11_get_public_key():
+class PKCS11Exception (SSHException):
+ """
+ Exception raised by failures in the PKCS11 api or logic errors.
+ """
+ pass
+
+
+class PKCS11AuthenticationException (AuthenticationException):
+ """
+ Exception raised when pkcs11 authentication failed for some reason.
+ """
+ pass
+
+
+def pkcs11_get_public_key(keyid="01"):
+ """
+ :param str pkcs11keyid: The keyid to use for the pkcs11 session.
+ """
public_key = None
try:
- p = subprocess.Popen(["pkcs15-tool", "--read-ssh-key", "01"],
+ p = subprocess.Popen(["pkcs15-tool", "--read-ssh-key", keyid],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=subprocess.PIPE)
@@ -18,23 +35,34 @@ def pkcs11_get_public_key():
public_key = out
except OSError as error:
if error.errno == errno.ENOENT:
- raise SSHException("Cannot find pkcs15-tool in PATH.")
+ raise PKCS11Exception("Cannot find pkcs15-tool in PATH.")
else:
raise
if public_key is None or len(public_key) < 1:
- raise SSHException("Invalid ssh public key returned by pkcs15-tool")
-
- return str(public_key)
-
-
-def pkcs11_open_session(pkcs11provider, pkcs11pin, pkcs11publickey=None):
- public_key = ""
+ raise PKCS11Exception("Invalid ssh public key returned by pkcs15-tool")
+
+ return public_key.decode('utf-8')
+
+
+def pkcs11_open_session(pkcs11provider, pkcs11pin, pkcs11keyid="01",
+ pkcs11slot=0, pkcs11publickey=None):
+ """
+ :param str pkcs11provider: If using PKCS11, this will be the provider
+ for the PKCS11 interface. Example: /usr/local/lib/opensc-pkcs11.so.
+ :param str pkcs11pin: If using PKCS11, this will be the pin of your
+ token or smartcard.
+ :param str pkcs11keyid: The keyid to use for the pkcs11 session.
+ :param int pkcs11slot: The slot id used for establishing the session.
+ :param str pkcs11publickey: If left the default (None), the public key
+ will be detected using OpenSC pkcs15-tool. Alternatively you can
+ provide it manually using this argument.
+ """
session = None
# Get Public SSH Key
if pkcs11publickey is None:
- public_key = pkcs11_get_public_key()
+ pkcs11publickey = pkcs11_get_public_key(pkcs11keyid)
class ck_c_initialize_args(Structure):
_fields_ = [('CreateMutex', c_void_p), ('DestroyMutex', c_void_p),
@@ -52,28 +80,28 @@ def pkcs11_open_session(pkcs11provider, pkcs11pin, pkcs11publickey=None):
# Init
if not os.path.isfile(pkcs11provider):
- raise Exception("pkcs11provider path is not valid: %s"
- % pkcs11provider)
+ raise PKCS11Exception("pkcs11provider path is not valid: %s"
+ % pkcs11provider)
lib = cdll.LoadLibrary(pkcs11provider)
res = lib.C_Initialize(byref(init_args))
if res != 0:
- raise Exception("PKCS11 Failed to Initialize")
+ raise PKCS11Exception("PKCS11 Failed to Initialize")
# Session
- slot = c_ulong(0) # 0 is the slot number
+ slot = c_ulong(pkcs11slot) # slot number
session = c_ulong()
flags = c_int(6) # CKF_SERIAL_SESSION (100b), CKF_RW_SESSION(10b)
res = lib.C_OpenSession(slot, flags, 0, 0, byref(session))
if res != 0:
- raise Exception("PKCS11 Failed to Open Session")
+ raise PKCS11Exception("PKCS11 Failed to Open Session")
# Login
login_type = c_int(1) # 1=USER PIN
- str_pin = str(pkcs11pin)
+ str_pin = pkcs11pin.encode('utf-8')
pin = c_char_p(str_pin)
res = lib.C_Login(session, login_type, pin, len(str_pin))
if res != 0:
- raise Exception("PKCS11 Login Failed")
+ raise PKCS11AuthenticationException("PKCS11 Login Failed")
# Get object for key
class ck_attribute(Structure):
@@ -89,7 +117,7 @@ def pkcs11_open_session(pkcs11provider, pkcs11pin, pkcs11publickey=None):
keyret = c_ulong()
cls = c_ulong(3) # CKO_PRIVATE_KEY
- objid_str = "1"
+ objid_str = pkcs11keyid.encode('utf-8')
objid = c_char_p(objid_str)
objid_len = c_ulong(len(objid_str))
attrs[0].type = c_ulong(0) # CKA_CLASS
@@ -100,23 +128,32 @@ def pkcs11_open_session(pkcs11provider, pkcs11pin, pkcs11publickey=None):
attrs[1].value_len = objid_len
res = lib.C_FindObjectsInit(session, attrs, nattrs)
if res != 0:
- raise Exception("PKCS11 Failed to Find Init")
+ raise PKCS11Exception("PKCS11 Failed to Find Init")
res = lib.C_FindObjects(session, byref(keyret), 1, byref(count))
if res != 0:
- raise Exception("PKCS11 Failed to Find Objects")
+ raise PKCS11Exception("PKCS11 Failed to Find Objects")
res = lib.C_FindObjectsFinal(session)
if res != 0:
- raise Exception("PKCS11 Failed to Find Objects Final")
+ raise PKCS11Exception("PKCS11 Failed to Find Objects Final")
- return (session, public_key, keyret)
+ return {"session": session, "public_key": pkcs11publickey,
+ "keyret": keyret, "provider": pkcs11provider}
-def pkcs11_close_session(pkcs11provider):
+def pkcs11_close_session(pkcs11session):
+ """
+ :param str pkcs11session: pkcs11 session obtained
+ by calling pkcs11_open_session
+ """
+ if "provider" not in pkcs11session:
+ raise PKCS11Exception("pkcs11 session is missing the provider,\
+ the session is not valid")
+ pkcs11provider = pkcs11session["provider"]
if not os.path.isfile(pkcs11provider):
- raise Exception("pkcs11provider path is not valid: %s"
- % pkcs11provider)
+ raise PKCS11Exception("pkcs11provider path is not valid: %s"
+ % pkcs11provider)
lib = cdll.LoadLibrary(pkcs11provider)
# Wrap things up
res = lib.C_Finalize(c_int(0))
if res != 0:
- raise Exception("PKCS11 Failed to Finalize")
+ raise PKCS11Exception("PKCS11 Failed to Finalize")
diff --git a/paramiko/pkey.py b/paramiko/pkey.py
index 35a26fc7..67723be2 100644
--- a/paramiko/pkey.py
+++ b/paramiko/pkey.py
@@ -31,8 +31,9 @@ from cryptography.hazmat.primitives.ciphers import algorithms, modes, Cipher
from paramiko import util
from paramiko.common import o600
-from paramiko.py3compat import u, encodebytes, decodebytes, b
+from paramiko.py3compat import u, encodebytes, decodebytes, b, string_types
from paramiko.ssh_exception import SSHException, PasswordRequiredException
+from paramiko.message import Message
class PKey(object):
@@ -363,3 +364,170 @@ class PKey(object):
format,
encryption
).decode())
+
+ def _check_type_and_load_cert(self, msg, key_type, cert_type):
+ """
+ Perform message type-checking & optional certificate loading.
+
+ This includes fast-forwarding cert ``msg`` objects past the nonce, so
+ that the subsequent fields are the key numbers; thus the caller may
+ expect to treat the message as key material afterwards either way.
+
+ The obtained key type is returned for classes which need to know what
+ it was (e.g. ECDSA.)
+ """
+ # Normalization; most classes have a single key type and give a string,
+ # but eg ECDSA is a 1:N mapping.
+ key_types = key_type
+ cert_types = cert_type
+ if isinstance(key_type, string_types):
+ key_types = [key_types]
+ if isinstance(cert_types, string_types):
+ cert_types = [cert_types]
+ # Can't do much with no message, that should've been handled elsewhere
+ if msg is None:
+ raise SSHException('Key object may not be empty')
+ # First field is always key type, in either kind of object. (make sure
+ # we rewind before grabbing it - sometimes caller had to do their own
+ # introspection first!)
+ msg.rewind()
+ type_ = msg.get_text()
+ # Regular public key - nothing special to do besides the implicit
+ # type check.
+ if type_ in key_types:
+ pass
+ # OpenSSH-compatible certificate - store full copy as .public_blob
+ # (so signing works correctly) and then fast-forward past the
+ # nonce.
+ elif type_ in cert_types:
+ # This seems the cleanest way to 'clone' an already-being-read
+ # message; they're *IO objects at heart and their .getvalue()
+ # always returns the full value regardless of pointer position.
+ self.load_certificate(Message(msg.asbytes()))
+ # Read out nonce as it comes before the public numbers.
+ # TODO: usefully interpret it & other non-public-number fields
+ # (requires going back into per-type subclasses.)
+ msg.get_string()
+ else:
+ err = 'Invalid key (class: {0}, data type: {1}'
+ raise SSHException(err.format(self.__class__.__name__, type_))
+
+ def load_certificate(self, value):
+ """
+ Supplement the private key contents with data loaded from an OpenSSH
+ public key (``.pub``) or certificate (``-cert.pub``) file, a string
+ containing such a file, or a `.Message` object.
+
+ The .pub contents adds no real value, since the private key
+ file includes sufficient information to derive the public
+ key info. For certificates, however, this can be used on
+ the client side to offer authentication requests to the server
+ based on certificate instead of raw public key.
+
+ See:
+ https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys
+
+ Note: very little effort is made to validate the certificate contents,
+ that is for the server to decide if it is good enough to authenticate
+ successfully.
+ """
+ if isinstance(value, Message):
+ constructor = 'from_message'
+ elif os.path.isfile(value):
+ constructor = 'from_file'
+ else:
+ constructor = 'from_string'
+ blob = getattr(PublicBlob, constructor)(value)
+ if not blob.key_type.startswith(self.get_name()):
+ err = "PublicBlob type {0} incompatible with key type {1}"
+ raise ValueError(err.format(blob.key_type, self.get_name()))
+ self.public_blob = blob
+
+
+# General construct for an OpenSSH style Public Key blob
+# readable from a one-line file of the format:
+# <key-name> <base64-blob> [<comment>]
+# Of little value in the case of standard public keys
+# {ssh-rsa, ssh-dss, ssh-ecdsa, ssh-ed25519}, but should
+# provide rudimentary support for {*-cert.v01}
+class PublicBlob(object):
+ """
+ OpenSSH plain public key or OpenSSH signed public key (certificate).
+
+ Tries to be as dumb as possible and barely cares about specific
+ per-key-type data.
+
+ ..note::
+ Most of the time you'll want to call `from_file`, `from_string` or
+ `from_message` for useful instantiation, the main constructor is
+ basically "I should be using ``attrs`` for this."
+ """
+ def __init__(self, type_, blob, comment=None):
+ """
+ Create a new public blob of given type and contents.
+
+ :param str type_: Type indicator, eg ``ssh-rsa``.
+ :param blob: The blob bytes themselves.
+ :param str comment: A comment, if one was given (e.g. file-based.)
+ """
+ self.key_type = type_
+ self.key_blob = blob
+ self.comment = comment
+
+ @classmethod
+ def from_file(cls, filename):
+ """
+ Create a public blob from a ``-cert.pub``-style file on disk.
+ """
+ with open(filename) as f:
+ string = f.read()
+ return cls.from_string(string)
+
+ @classmethod
+ def from_string(cls, string):
+ """
+ Create a public blob from a ``-cert.pub``-style string.
+ """
+ fields = string.split(None, 2)
+ if len(fields) < 2:
+ msg = "Not enough fields for public blob: {0}"
+ raise ValueError(msg.format(fields))
+ key_type = fields[0]
+ key_blob = decodebytes(b(fields[1]))
+ try:
+ comment = fields[2].strip()
+ except IndexError:
+ comment = None
+ # Verify that the blob message first (string) field matches the
+ # key_type
+ m = Message(key_blob)
+ blob_type = m.get_text()
+ if blob_type != key_type:
+ msg = "Invalid PublicBlob contents: key type={0!r}, but blob type={1!r}" # noqa
+ raise ValueError(msg.format(key_type, blob_type))
+ # All good? All good.
+ return cls(type_=key_type, blob=key_blob, comment=comment)
+
+ @classmethod
+ def from_message(cls, message):
+ """
+ Create a public blob from a network `.Message`.
+
+ Specifically, a cert-bearing pubkey auth packet, because by definition
+ OpenSSH-style certificates 'are' their own network representation."
+ """
+ type_ = message.get_text()
+ return cls(type_=type_, blob=message.asbytes())
+
+ def __str__(self):
+ ret = '{0} public key/certificate'.format(self.key_type)
+ if self.comment:
+ ret += "- {0}".format(self.comment)
+ return ret
+
+ def __eq__(self, other):
+ # Just piggyback on Message/BytesIO, since both of these should be one.
+ return self and other and self.key_blob == other.key_blob
+
+ def __ne__(self, other):
+ return not self == other
diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py
index b5107515..8dfcfb01 100644
--- a/paramiko/rsakey.py
+++ b/paramiko/rsakey.py
@@ -40,6 +40,7 @@ class RSAKey(PKey):
def __init__(self, msg=None, data=None, filename=None, password=None,
key=None, file_obj=None):
self.key = None
+ self.public_blob = None
if file_obj is not None:
self._from_private_key(file_obj, password)
return
@@ -51,10 +52,11 @@ class RSAKey(PKey):
if key is not None:
self.key = key
else:
- if msg is None:
- raise SSHException('Key object may not be empty')
- if msg.get_text() != 'ssh-rsa':
- raise SSHException('Invalid key')
+ self._check_type_and_load_cert(
+ msg=msg,
+ key_type='ssh-rsa',
+ cert_type='ssh-rsa-cert-v01@openssh.com',
+ )
self.key = rsa.RSAPublicNumbers(
e=msg.get_mpint(), n=msg.get_mpint()
).public_key(default_backend())
@@ -103,12 +105,11 @@ class RSAKey(PKey):
return isinstance(self.key, rsa.RSAPrivateKey)
def sign_ssh_data(self, data):
- signer = self.key.signer(
+ sig = self.key.sign(
+ data,
padding=padding.PKCS1v15(),
algorithm=hashes.SHA1(),
)
- signer.update(data)
- sig = signer.finalize()
m = Message()
m.add_string('ssh-rsa')
@@ -122,14 +123,10 @@ class RSAKey(PKey):
if isinstance(key, rsa.RSAPrivateKey):
key = key.public_key()
- verifier = key.verifier(
- signature=msg.get_binary(),
- padding=padding.PKCS1v15(),
- algorithm=hashes.SHA1(),
- )
- verifier.update(data)
try:
- verifier.verify()
+ key.verify(
+ msg.get_binary(), data, padding.PKCS1v15(), hashes.SHA1()
+ )
except InvalidSignature:
return False
else:
diff --git a/paramiko/server.py b/paramiko/server.py
index adc606bf..c96126e9 100644
--- a/paramiko/server.py
+++ b/paramiko/server.py
@@ -570,6 +570,19 @@ class ServerInterface (object):
"""
return False
+ def get_banner(self):
+ """
+ A pre-login banner to display to the user. The message may span
+ multiple lines separated by crlf pairs. The language should be in
+ rfc3066 style, for example: en-US
+
+ The default implementation always returns ``(None, None)``.
+
+ :returns: A tuple containing the banner and language code.
+
+ .. versionadded:: 2.3
+ """
+ return (None, None)
class InteractiveQuery (object):
"""
diff --git a/paramiko/transport.py b/paramiko/transport.py
index 989f9ed0..d782415b 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -1387,15 +1387,10 @@ class Transport(threading.Thread, ClosingContextManager):
return []
return self.auth_handler.wait_for_response(my_event)
- def auth_pkcs11(self, username, pkcs11pin, pkcs11provider, pkcs11session,
- event=None):
+ def auth_pkcs11(self, username, pkcs11session, event=None):
"""
:param str username: the username to authenticate as
- :param str pkcs11pin: pin to authenticate to smartcard
- :param str pkcs11provider: pkcs11 provider such as opensc.
- Example: /usr/local/lib/opensc-pkcs11.so.
- :param str pkcs11session: pkcs11 session used for multithreaded
- applications.
+ :param str pkcs11session: session obtained from pkcs11_open_session
:param .threading.Event event:
an event to trigger when the authentication attempt is complete
(whether it was successful or not)
@@ -1416,8 +1411,7 @@ class Transport(threading.Thread, ClosingContextManager):
else:
my_event = event
self.auth_handler = AuthHandler(self)
- self.auth_handler.auth_pkcs11(username, pkcs11pin, pkcs11provider,
- pkcs11session, my_event)
+ self.auth_handler.auth_pkcs11(username, pkcs11session, my_event)
if event is not None:
# caller wants to wait for event themselves
return []
@@ -1851,8 +1845,6 @@ class Transport(threading.Thread, ClosingContextManager):
continue
elif ptype == MSG_DISCONNECT:
self._parse_disconnect(m)
- self.active = False
- self.packetizer.close()
break
elif ptype == MSG_DEBUG:
self._parse_debug(m)
@@ -1876,8 +1868,7 @@ class Transport(threading.Thread, ClosingContextManager):
self._log(DEBUG, 'Ignoring message for dead channel %d' % chanid) # noqa
else:
self._log(ERROR, 'Channel request for unknown channel %d' % chanid) # noqa
- self.active = False
- self.packetizer.close()
+ break
elif (
self.auth_handler is not None and
ptype in self.auth_handler._handler_table
diff --git a/setup.py b/setup.py
index 1038fb68..1234bfa5 100644
--- a/setup.py
+++ b/setup.py
@@ -76,7 +76,7 @@ setup(
],
install_requires=[
'bcrypt>=3.1.3',
- 'cryptography>=1.1',
+ 'cryptography>=1.5',
'pynacl>=1.0.1',
'pyasn1>=0.1.7',
],
diff --git a/sites/docs/api/keys.rst b/sites/docs/api/keys.rst
index c6412f77..a456f502 100644
--- a/sites/docs/api/keys.rst
+++ b/sites/docs/api/keys.rst
@@ -21,3 +21,8 @@ ECDSA
=====
.. automodule:: paramiko.ecdsakey
+
+Ed25519
+=======
+
+.. automodule:: paramiko.ed25519key
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index 97b2edd8..4f7303b4 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -12,6 +12,68 @@ Changelog
still importable from its previous home, ``hostkeys.py``.)
* :feature:`827` Add support for PKCS #11 which enables the use of smartcards
and other cryptographic tokens.
+* :support:`979` Update how we use `Cryptography <https://cryptography.io>`_'s
+ signature/verification methods so we aren't relying on a deprecated API.
+ Thanks to Paul Kehrer for the patch.
+
+ .. warning::
+ This bumps the minimum Cryptography version from 1.1 to 1.5. Such an
+ upgrade should be backwards compatible and easy to do. See `their changelog
+ <https://cryptography.io/en/latest/changelog/>`_ for additional details.
+* :support:`-` Ed25519 keys never got proper API documentation support; this
+ has been fixed.
+* :feature:`1026` Update `~paramiko.ed25519key.Ed25519Key` so its constructor
+ offers the same ``file_obj`` parameter as its sibling key classes. Credit:
+ Michal Kuffa.
+* :feature:`1013` Added pre-authentication banner support for the server
+ interface (`ServerInterface.get_banner
+ <paramiko.server.ServerInterface.get_banner>` plus related support in
+ ``Transport/AuthHandler``.) Patch courtesy of Dennis Kaarsemaker.
+* :bug:`60 major` (via :issue:`1037`) Paramiko originally defaulted to zlib
+ compression level 9 (when one connects with ``compression=True``; it defaults
+ to off.) This has been found to be quite wasteful and tends to cause much
+ longer transfers in most cases, than is necessary.
+
+ OpenSSH defaults to compression level 6, which is a much more reasonable
+ setting (nearly identical compression characteristics but noticeably,
+ sometimes significantly, faster transmission); Paramiko now uses this value
+ instead.
+
+ Thanks to Damien Dubé for the report and ``@DrNeutron`` for investigating &
+ submitting the patch.
+* :support:`-` Display exception type and message when logging auth-rejection
+ messages (ones reading ``Auth rejected: unsupported or mangled public key``);
+ previously this error case had a bare except and did not display exactly why
+ the key failed. It will now append info such as ``KeyError:
+ 'some-unknown-type-string'`` or similar.
+* :feature:`1042` (also partially :issue:`531`) Implement basic client-side
+ certificate authentication (as per the OpenSSH vendor extension.)
+
+ The core implementation is `PKey.load_certificate
+ <paramiko.pkey.PKey.load_certificate>` and its corresponding ``.public_blob``
+ attribute on key objects, which is honored in the auth and transport modules.
+ Additionally, `SSHClient.connect <paramiko.client.SSHClient.connect>` will
+ now automatically load certificate data alongside private key data when one
+ has appropriately-named cert files (e.g. ``id_rsa-cert.pub``) - see its
+ docstring for details.
+
+ Thanks to Jason Rigby for a first draft (:issue:`531`) and to Paul Kapp for
+ the second draft, upon which the current functionality has been based (with
+ modifications.)
+
+ .. note::
+ This support is client-focused; Paramiko-driven server code is capable of
+ handling cert-bearing pubkey auth packets, *but* it does not interpret any
+ cert-specific fields, so the end result is functionally identical to a
+ vanilla pubkey auth process (and thus requires e.g. prepopulated
+ authorized-keys data.) We expect full server-side cert support to follow
+ later.
+
+* :support:`1041` Modify logic around explicit disconnect
+ messages, and unknown-channel situations, so that they rely on centralized
+ shutdown code instead of running their own. This is at worst removing some
+ unnecessary code, and may help with some situations where Paramiko hangs at
+ the end of a session. Thanks to Paul Kapp for the patch.
* :support:`1012` (via :issue:`1016`) Enhance documentation around the new
`SFTP.posix_rename <paramiko.sftp_client.SFTPClient.posix_rename>` method so
it's referenced in the 'standard' ``rename`` method for increased visibility.
diff --git a/tasks.py b/tasks.py
index 42c18bd0..a34fd3ce 100644
--- a/tasks.py
+++ b/tasks.py
@@ -4,6 +4,7 @@ from shutil import rmtree, copytree
from invoke import Collection, task
from invocations.docs import docs, www, sites
from invocations.packaging.release import ns as release_coll, publish
+from invocations.testing import count_errors
# Until we move to spec-based testing
@@ -49,7 +50,7 @@ def release(ctx, sdist=True, wheel=True, sign=True, dry_run=False):
# aliasing, defaults etc.
release_coll.tasks['publish'] = release
-ns = Collection(test, coverage, release_coll, docs, www, sites)
+ns = Collection(test, coverage, release_coll, docs, www, sites, count_errors)
ns.configure({
'packaging': {
# NOTE: many of these are also set in kwarg defaults above; but having
diff --git a/test.py b/test.py
index 7849c149..4cb4427c 100755
--- a/test.py
+++ b/test.py
@@ -50,6 +50,7 @@ from test_client import SSHClientTest # XXX why shadow the above import?
from test_gssapi import GSSAPITest
from test_ssh_gss import GSSAuthTest
from test_kex_gss import GSSKexTest
+from tests.test_pkcs11 import Pkcs11Test
default_host = 'localhost'
default_user = os.environ.get('USER', 'nobody')
@@ -160,6 +161,7 @@ def main():
suite.addTest(unittest.makeSuite(TransportTest))
suite.addTest(unittest.makeSuite(NoValidConnectionsErrorTest))
suite.addTest(unittest.makeSuite(SSHClientTest))
+ suite.addTest(unittest.makeSuite(Pkcs11Test))
if options.use_sftp:
suite.addTest(unittest.makeSuite(SFTPTest))
if options.use_big_file:
diff --git a/tests/test_client.py b/tests/test_client.py
index e912d5b2..7ada13da 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -35,6 +35,7 @@ import time
from tests.util import test_path
import paramiko
+from paramiko.pkey import PublicBlob
from paramiko.common import PY2
from paramiko.ssh_exception import SSHException, AuthenticationException
@@ -47,10 +48,12 @@ FINGERPRINTS = {
}
-class NullServer (paramiko.ServerInterface):
+class NullServer(paramiko.ServerInterface):
def __init__(self, *args, **kwargs):
# Allow tests to enable/disable specific key types
self.__allowed_keys = kwargs.pop('allowed_keys', [])
+ # And allow them to set a (single...meh) expected public blob (cert)
+ self.__expected_public_blob = kwargs.pop('public_blob', None)
super(NullServer, self).__init__(*args, **kwargs)
def get_allowed_auths(self, username):
@@ -71,12 +74,18 @@ class NullServer (paramiko.ServerInterface):
expected = FINGERPRINTS[key.get_name()]
except KeyError:
return paramiko.AUTH_FAILED
- if (
+ # Base check: allowed auth type & fingerprint matches
+ happy = (
key.get_name() in self.__allowed_keys and
key.get_fingerprint() == expected
+ )
+ # Secondary check: if test wants assertions about cert data
+ if (
+ self.__expected_public_blob is not None and
+ key.public_blob != self.__expected_public_blob
):
- return paramiko.AUTH_SUCCESSFUL
- return paramiko.AUTH_FAILED
+ happy = False
+ return paramiko.AUTH_SUCCESSFUL if happy else paramiko.AUTH_FAILED
def check_channel_request(self, kind, chanid):
return paramiko.OPEN_SUCCEEDED
@@ -117,7 +126,7 @@ class SSHClientTest (unittest.TestCase):
if hasattr(self, attr):
getattr(self, attr).close()
- def _run(self, allowed_keys=None, delay=0):
+ def _run(self, allowed_keys=None, delay=0, public_blob=None):
if allowed_keys is None:
allowed_keys = FINGERPRINTS.keys()
self.socks, addr = self.sockl.accept()
@@ -128,7 +137,7 @@ class SSHClientTest (unittest.TestCase):
keypath = test_path('test_ecdsa_256.key')
host_key = paramiko.ECDSAKey.from_private_key_file(keypath)
self.ts.add_server_key(host_key)
- server = NullServer(allowed_keys=allowed_keys)
+ server = NullServer(allowed_keys=allowed_keys, public_blob=public_blob)
if delay:
time.sleep(delay)
self.ts.start_server(self.event, server)
@@ -140,7 +149,9 @@ class SSHClientTest (unittest.TestCase):
The exception is ``allowed_keys`` which is stripped and handed to the
``NullServer`` used for testing.
"""
- run_kwargs = {'allowed_keys': kwargs.pop('allowed_keys', None)}
+ run_kwargs = {}
+ for key in ('allowed_keys', 'public_blob'):
+ run_kwargs[key] = kwargs.pop(key, None)
# Server setup
threading.Thread(target=self._run, kwargs=run_kwargs).start()
host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key'))
@@ -248,6 +259,40 @@ class SSHClientTest (unittest.TestCase):
allowed_keys=['ecdsa-sha2-nistp256'],
)
+ def test_certs_allowed_as_key_filename_values(self):
+ # NOTE: giving cert path here, not key path. (Key path test is below.
+ # They're similar except for which path is given; the expected auth and
+ # server-side behavior is 100% identical.)
+ # NOTE: only bothered whipping up one cert per overall class/family.
+ for type_ in ('rsa', 'dss', 'ecdsa_256', 'ed25519'):
+ cert_path = test_path('test_{0}.key-cert.pub'.format(type_))
+ self._test_connection(
+ key_filename=cert_path,
+ public_blob=PublicBlob.from_file(cert_path),
+ )
+
+ def test_certs_implicitly_loaded_alongside_key_filename_keys(self):
+ # NOTE: a regular test_connection() w/ test_rsa.key would incidentally
+ # test this (because test_xxx.key-cert.pub exists) but incidental tests
+ # stink, so NullServer and friends were updated to allow assertions
+ # about the server-side key object's public blob. Thus, we can prove
+ # that a specific cert was found, along with regular authorization
+ # succeeding proving that the overall flow works.
+ for type_ in ('rsa', 'dss', 'ecdsa_256', 'ed25519'):
+ key_path = test_path('test_{0}.key'.format(type_))
+ self._test_connection(
+ key_filename=key_path,
+ public_blob=PublicBlob.from_file(
+ '{0}-cert.pub'.format(key_path)
+ ),
+ )
+
+ def test_default_key_locations_trigger_cert_loads_if_found(self):
+ # TODO: what it says on the tin: ~/.ssh/id_rsa tries to load
+ # ~/.ssh/id_rsa-cert.pub. Right now no other tests actually test that
+ # code path (!) so we're punting too, sob.
+ pass
+
def test_4_auto_add_policy(self):
"""
verify that SSHClient's AutoAddPolicy works.
diff --git a/tests/test_dss.key-cert.pub b/tests/test_dss.key-cert.pub
new file mode 100644
index 00000000..07fd5578
--- /dev/null
+++ b/tests/test_dss.key-cert.pub
@@ -0,0 +1 @@
+ssh-dss-cert-v01@openssh.com AAAAHHNzaC1kc3MtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgJA3GjLmg6JbIWxokW/c827lmPOSvSfPDIY586yICFqIAAACBAOeBpgNnfRzr/twmAQRu2XwWAp3CFtrVnug6s6fgwj/oLjYbVtjAy6pl/h0EKCWx2rf1IetyNsTxWrniA9I6HeDj65X1FyDkg6g8tvCnaNB8Xp/UUhuzHuGsMIipRxBxw9LF608EqZcj1E3ytktoW5B5OcjrkEoz3xG7C+rpIjYvAAAAFQDwz4UnmsGiSNu5iqjn3uTzwUpshwAAAIEAkxfFeY8P2wZpDjX0MimZl5wkoFQDL25cPzGBuB4OnB8NoUk/yjAHIIpEShw8V+LzouMK5CTJQo5+Ngw3qIch/WgRmMHy4kBq1SsXMjQCte1So6HBMvBPIW5SiMTmjCfZZiw4AYHK+B/JaOwaG9yRg2Ejg4Ok10+XFDxlqZo8Y+wAAACARmR7CCPjodxASvRbIyzaVpZoJ/Z6x7dAumV+ysrV1BVYd0lYukmnjO1kKBWApqpH1ve9XDQYN8zgxM4b16L21kpoWQnZtXrY3GZ4/it9kUgyB7+NwacIBlXa8cMDL7Q/69o0d54U0X/NeX5QxuYR6OMJlrkQB7oiW/P/1mwjQgEAAAAAAAAAAAAAAAEAAAAJdXNlcl90ZXN0AAAACAAAAAR0ZXN0AAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQDskr46Umjxh3wo7PoPQsSVS3xt6+5PhwmXrnVtBBnkOo+zHRwQo8G8sY+Lc6oOOzA5GCSawKOwqE305GIDfB8/L9EKOkAjdN18imDjw/YuJFA4bl9yFhsXrCb1GZPJw0pJ0H0Eid9EldyMQAhGE49MWvnFMQl1TgO6YWq/g71xAFimge0LvVWijlbMy7O+nsGxSpinIprV5S9Viv8XC/ku89tadZfca1uxq751aGfAWGeYrVytpUl8UO0ggqH6BaUvkDU7rWh2n5RHUTvgzceKWnz5wqd8BngK37WmJjAgCtHCJS5ZRf6oJGj2QVcqc6cjvEFWsCuOKB4KAjktauWxAAABDwAAAAdzc2gtcnNhAAABAK6jweL231fRhFoybEGTOXJfj0lx55KpDsw9Q1rBvZhrSgwUr2dFr9HVcKe44mTC7CMtdW5VcyB67l1fnMil/D/e4zYxI0PvbW6RxLFNqvvtxBu5sGt5B7uzV4aAV31TpWR0l5RwwpZqc0NUlTx7oMutN1BDrPqW70QZ/iTEwalkn5fo1JWej0cf4BdC9VgYDLnprx0KN3IToukbszRQySnuR6MQUfj0m7lUloJfF3rq8G0kNxWqDGoJilMhO5Lqu9wAhlZWdouypI6bViO6+ToCVixLNUYs3EfS1zCxvXpiyMvh6rZofJ6WqzUuSd4Mzb2Ka4ocTKi7kynF+OG0Ivo= tests/test_dss.key.pub
diff --git a/tests/test_ecdsa_256.key-cert.pub b/tests/test_ecdsa_256.key-cert.pub
new file mode 100644
index 00000000..f2c93ccf
--- /dev/null
+++ b/tests/test_ecdsa_256.key-cert.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgJ+ZkRXedIWPl9y6fvel60p47ys5WgwMSjiwzJ2Ho+4MAAAAIbmlzdHAyNTYAAABBBJSPZm3ZWkvk/Zx8WP+fZRZ5/NBBHnGQwR6uIC6XHGPDIHuWUzIjAwA0bzqkOUffEsbLe+uQgKl5kbc/L8KA/eoAAAAAAAAAAAAAAAEAAAAJdXNlcl90ZXN0AAAACAAAAAR0ZXN0AAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQDskr46Umjxh3wo7PoPQsSVS3xt6+5PhwmXrnVtBBnkOo+zHRwQo8G8sY+Lc6oOOzA5GCSawKOwqE305GIDfB8/L9EKOkAjdN18imDjw/YuJFA4bl9yFhsXrCb1GZPJw0pJ0H0Eid9EldyMQAhGE49MWvnFMQl1TgO6YWq/g71xAFimge0LvVWijlbMy7O+nsGxSpinIprV5S9Viv8XC/ku89tadZfca1uxq751aGfAWGeYrVytpUl8UO0ggqH6BaUvkDU7rWh2n5RHUTvgzceKWnz5wqd8BngK37WmJjAgCtHCJS5ZRf6oJGj2QVcqc6cjvEFWsCuOKB4KAjktauWxAAABDwAAAAdzc2gtcnNhAAABALdnEil8XIFkcgLZgYwS2cIQPHetUzMNxYCqzk7mSfVpCaIYNTr27RG+f+sD0cerdAIUUvhCT7iA82/Y7wzwkO2RUBi61ATfw9DDPPRQTDfix1SSRwbmPB/nVI1HlPMCEs6y48PFaBZqXwJPS3qycgSxoTBhaLCLzT+r6HRaibY7kiRLDeL3/WHyasK2PRdcYJ6KrLd0ctQcUHZCLK3fJfMfuQRg8MZLVrmK3fHStCXHpRFueRxUhZjaiS9evA/NtzEQhf46JDClQ2rLYpSqSg7QUR/rKwqWWyMuQkOHmlJw797VVa+ZzpUFXP7ekWel3FaBj8IHiimIA7Jm6dOCLm4= tests/test_ecdsa_256.key.pub
diff --git a/tests/test_ed25519.key-cert.pub b/tests/test_ed25519.key-cert.pub
new file mode 100644
index 00000000..4e01415a
--- /dev/null
+++ b/tests/test_ed25519.key-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIIjBkc8l1X887CLBHraU+d6/74Hxr9oa+3HC0iioecZ6AAAAIHr1K9komH/1WBIvQbbtvnFVhryd62EfcgRFuLRiokNfAAAAAAAAAAAAAAABAAAACXVzZXJfdGVzdAAAAAgAAAAEdGVzdAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAABFwAAAAdzc2gtcnNhAAAAAwEAAQAAAQEA7JK+OlJo8Yd8KOz6D0LElUt8bevuT4cJl651bQQZ5DqPsx0cEKPBvLGPi3OqDjswORgkmsCjsKhN9ORiA3wfPy/RCjpAI3TdfIpg48P2LiRQOG5fchYbF6wm9RmTycNKSdB9BInfRJXcjEAIRhOPTFr5xTEJdU4DumFqv4O9cQBYpoHtC71Voo5WzMuzvp7BsUqYpyKa1eUvVYr/Fwv5LvPbWnWX3Gtbsau+dWhnwFhnmK1craVJfFDtIIKh+gWlL5A1O61odp+UR1E74M3Hilp8+cKnfAZ4Ct+1piYwIArRwiUuWUX+qCRo9kFXKnOnI7xBVrArjigeCgI5LWrlsQAAAQ8AAAAHc3NoLXJzYQAAAQCNfYITv/GCW42fLI89x0pKpXIET/xHIBVan5S3fy5SZq9gLG1Db9g/FITDfOVA7OX8mU/91rucHGtuEi3isILdNFrCcoLEml289tyyluUbbFD5fjvBchMWBkYPwrOPfEzSs299Yk8ZgfV1pjWlndfV54s4c9pinkGu8c0Vzc6stEbWkdmoOHE8su3ogUPg/hOygDzJ+ZOgP5HIUJ6YgkgVpWgZm7zofwdZfa2HEb+WhZaKfMK1UCw1UiSBVk9dx6qzF9m243tHwSHraXvb9oJ1wT1S/MypTbP4RT4fHN8koYNrv2szEBN+lkRgk1D7xaxS/Md2TJsau9ho/UCXSR8L tests/test_ed25519.key.pub
diff --git a/tests/test_pkcs11.py b/tests/test_pkcs11.py
new file mode 100644
index 00000000..6fb60025
--- /dev/null
+++ b/tests/test_pkcs11.py
@@ -0,0 +1,166 @@
+# This file is part of paramiko.
+#
+# Paramiko is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+
+"""
+Test the used APIs for pkcs11
+"""
+
+import unittest
+import mock
+from paramiko.pkcs11 import (
+ PKCS11Exception, pkcs11_get_public_key, pkcs11_close_session,
+ pkcs11_open_session
+)
+from paramiko.auth_handler import AuthHandler
+from paramiko.transport import Transport
+from tests.loop import LoopSocket
+
+
+test_rsa_public_key = b"ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA049W6geFpm\
+sljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN\
+79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4u\
+oZpww3nmE0kb3y21tH4c="
+
+
+class MockPKCS11Lib(object):
+ def __init__(self):
+ pass
+
+ def C_Finalize(val):
+ return 0
+
+ def C_Initialize(val1, val2):
+ return 0
+
+ def C_OpenSession(val1, val2, val3, val4, val5, val6):
+ return 0
+
+ def C_Login(val1, val2, val3, val4, val5):
+ return 0
+
+ def C_FindObjectsInit(val1, val2, val3, val4):
+ return 0
+
+ def C_FindObjects(val1, val2, val3, val4, val5):
+ return 0
+
+ def C_FindObjectsFinal(val1, val2):
+ return 0
+
+ def C_Sign(val1, val2, val3, val4, val5, val6):
+ return 0
+
+ def C_SignInit(val1, val2, val3, val4):
+ return 0
+
+
+class MockPopen_pkcs15tool_rsakey(object):
+ def __init__(self, stdout, stderr):
+ self.stdout = stdout
+ self.stderr = stderr
+ self.stdout = test_rsa_public_key
+
+ def communicate(self):
+ return (self.stdout, self.stderr)
+
+ def wait(self):
+ pass
+
+ def kill(self):
+ pass
+
+
+class Pkcs11Test(unittest.TestCase):
+ def setUp(self):
+ self.sockc = LoopSocket()
+
+ def tearDown(self):
+ pass
+
+ @mock.patch('subprocess.Popen',
+ return_value=MockPopen_pkcs15tool_rsakey([], []))
+ def test_1_pkcs11_get_public_key(self, mock_popen):
+ """
+ Test Getting Public Key
+ """
+ public_key = pkcs11_get_public_key()
+ self.assertEqual(public_key, test_rsa_public_key.decode("utf-8"))
+
+ @mock.patch('os.path.isfile', return_value=True)
+ @mock.patch('paramiko.pkcs11.cdll.LoadLibrary',
+ return_value=MockPKCS11Lib())
+ def test_2_pkcs11_close_session_success(self,
+ mock_isfile,
+ mock_loadlibrary):
+ pkcs11session = {"pkcs11provider": "/test/path/example"}
+ threw_exception = True
+ try:
+ pkcs11_close_session(pkcs11session)
+ except Exception:
+ threw_exception = False
+ self.assertTrue(not threw_exception)
+
+ @mock.patch('os.path.isfile', return_value=False)
+ @mock.patch('paramiko.pkcs11.cdll.LoadLibrary',
+ return_value=MockPKCS11Lib())
+ def test_3_pkcs11_close_session_fail_nofile(self, mock_isfile,
+ mock_loadlibrary):
+ pkcs11session = {"pkcs11provider": "/test/path/example"}
+ threw_exception = False
+ try:
+ pkcs11_close_session(pkcs11session)
+ except PKCS11Exception:
+ threw_exception = True
+ self.assertTrue(threw_exception)
+
+ @mock.patch('subprocess.Popen',
+ return_value=MockPopen_pkcs15tool_rsakey([], []))
+ @mock.patch('os.path.isfile', return_value=True)
+ @mock.patch('paramiko.pkcs11.cdll.LoadLibrary',
+ return_value=MockPKCS11Lib())
+ def test_4_pkcs11_open_session(self,
+ mock_popen,
+ mock_isfile,
+ mock_loadlibrary):
+ session = pkcs11_open_session("/test/provider/example", "1234")
+ self.assertEqual(0, session["session"].value)
+ self.assertEqual(test_rsa_public_key.decode("utf-8"), session["public_key"])
+ self.assertEqual(0, session["keyret"].value)
+ self.assertEqual("/test/provider/example", session["provider"])
+
+ @mock.patch('paramiko.auth_handler.AuthHandler._request_auth',
+ return_value=True)
+ def test_5_pkcs11_authhandler_auth_pkcs11_basic(self, mock_requestauth):
+ # Mock _request_auth just to test the setup
+ # Just test the setup
+ tc = Transport(self.sockc)
+ session = {"session": None}
+ testauth = AuthHandler(tc)
+ testauth.auth_pkcs11("testuser", session, None)
+ self.assertEqual(testauth.auth_event, None)
+ self.assertEqual(testauth.auth_method, 'publickey')
+ self.assertEqual(testauth.username, "testuser")
+ self.assertEqual(testauth.pkcs11session, session)
+
+ @mock.patch('paramiko.auth_handler.AuthHandler._request_auth',
+ return_value=True)
+ def test_6_pkcs11_authhandler_pkcs11_get_public_key(self, mock_requestauth):
+ tc = Transport(self.sockc)
+ session = {"public_key": test_rsa_public_key}
+ testauth = AuthHandler(tc)
+ testauth.auth_pkcs11("testuser", session, None)
+ public_key = testauth._pkcs11_get_public_key()
+ self.assertEqual(public_key, test_rsa_public_key)
diff --git a/tests/test_pkey.py b/tests/test_pkey.py
index 35b425da..a0e03407 100644
--- a/tests/test_pkey.py
+++ b/tests/test_pkey.py
@@ -467,6 +467,12 @@ class KeyTest(unittest.TestCase):
self.assertTrue(not pub.can_sign())
self.assertEqual(key, pub)
+ def test_ed25519_load_from_file_obj(self):
+ with open(test_path('test_ed25519.key')) as pkey_fileobj:
+ key = Ed25519Key.from_private_key(pkey_fileobj)
+ self.assertEqual(key, key)
+ self.assertTrue(key.can_sign())
+
def test_keyfile_is_actually_encrypted(self):
# Read an existing encrypted private key
file_ = test_path('test_rsa_password.key')
@@ -481,3 +487,30 @@ class KeyTest(unittest.TestCase):
self.assert_keyfile_is_encrypted(newfile)
finally:
os.remove(newfile)
+
+ def test_certificates(self):
+ # PKey.load_certificate
+ key = RSAKey.from_private_key_file(test_path('test_rsa.key'))
+ self.assertTrue(key.public_blob is None)
+ key.load_certificate(test_path('test_rsa.key-cert.pub'))
+ self.assertTrue(key.public_blob is not None)
+ self.assertEqual(key.public_blob.key_type, 'ssh-rsa-cert-v01@openssh.com')
+ self.assertEqual(key.public_blob.comment, 'test_rsa.key.pub')
+ # Delve into blob contents, for test purposes
+ msg = Message(key.public_blob.key_blob)
+ self.assertEqual(msg.get_text(), 'ssh-rsa-cert-v01@openssh.com')
+ nonce = msg.get_string()
+ e = msg.get_mpint()
+ n = msg.get_mpint()
+ self.assertEqual(e, key.public_numbers.e)
+ self.assertEqual(n, key.public_numbers.n)
+ # Serial number
+ self.assertEqual(msg.get_int64(), 1234)
+
+ # Prevented from loading certificate that doesn't match
+ key1 = Ed25519Key.from_private_key_file(test_path('test_ed25519.key'))
+ self.assertRaises(
+ ValueError,
+ key1.load_certificate,
+ test_path('test_rsa.key-cert.pub'),
+ )
diff --git a/tests/test_rsa.key-cert.pub b/tests/test_rsa.key-cert.pub
new file mode 100644
index 00000000..7487ab66
--- /dev/null
+++ b/tests/test_rsa.key-cert.pub
@@ -0,0 +1 @@
+ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgsZlXTd5NE4uzGAn6TyAqQj+IPbsTEFGap2x5pTRwQR8AAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4cAAAAAAAAE0gAAAAEAAAAmU2FtcGxlIHNlbGYtc2lnbmVkIE9wZW5TU0ggY2VydGlmaWNhdGUAAAASAAAABXVzZXIxAAAABXVzZXIyAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAACVAAAAB3NzaC1yc2EAAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4cAAACPAAAAB3NzaC1yc2EAAACATFHFsARDgQevc6YLxNnDNjsFtZ08KPMyYVx0w5xm95IVZHVWSOc5w+ccjqN9HRwxV3kP7IvL91qx0Uc3MJdB9g/O6HkAP+rpxTVoTb2EAMekwp5+i8nQJW4CN2BSsbQY1M6r7OBZ5nmF4hOW/5Pu4l22lXe2ydy8kEXOEuRpUeQ= test_rsa.key.pub
diff --git a/tests/test_rsa.key.pub b/tests/test_rsa.key.pub
new file mode 100644
index 00000000..bfa1e150
--- /dev/null
+++ b/tests/test_rsa.key.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4c=
diff --git a/tests/util.py b/tests/util.py
index b546a7e1..c1b43da8 100644
--- a/tests/util.py
+++ b/tests/util.py
@@ -4,4 +4,3 @@ root_path = os.path.dirname(os.path.realpath(__file__))
def test_path(filename):
return os.path.join(root_path, filename)
-