Skip to content

Commit d973500

Browse files
authored
Cert (#236)
* Added certificate authentication for ssh client * Updated parallel and single clients for cert auth. Added embedded openssh certificate authentication support. * Added parallel client cert auth test. * Updated docs. * Updated requirements, setup.py * Updated changelog
1 parent ccdbc59 commit d973500

20 files changed

+250
-12
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,6 @@ pypy
4444

4545
# Documentation builds
4646
doc/_build
47+
48+
tests/unit_test_cert_key-cert.pub
49+
tests/embedded_server/principals

Changelog.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
Change Log
22
============
33

4+
2.1.0
5+
+++++
6+
7+
Changes
8+
-------
9+
10+
* Added certificate authentication support for the ``pssh.clients.ssh`` clients.
11+
412
2.0.0
513
+++++
614

doc/advanced.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,25 @@ GSS authentication allows logins using Windows LDAP configured user accounts via
9999
``ssh-python`` :py:class:`ParallelSSHClient <pssh.clients.ssh.parallel.ParallelSSHClient>` only.
100100

101101

102+
Certificate authentication
103+
--------------------------
104+
105+
In the ``pssh.clients.ssh`` clients, certificate authentication is supported.
106+
107+
.. code-block:: python
108+
109+
from pssh.clients.ssh import ParallelSSHClient
110+
111+
client = ParallelSSHClient(hosts, pkey='id_rsa', cert_file='id_rsa-cert.pub')
112+
113+
114+
Where ``id_rsa-cert.pub`` is an RSA signed certificate file for the ``id_rsa`` private key.
115+
116+
Both private key and corresponding signed public certificate file must be provided.
117+
118+
``ssh-python`` :py:mod:`ParallelSSHClient <pssh.clients.ssh>` clients only.
119+
120+
102121
Tunnelling
103122
**********
104123

doc/constants.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Constants
2+
==========
3+
4+
.. automodule:: pssh.constants
5+
:members:
6+
:undoc-members:
7+
:member-order: groupwise

pssh/clients/ssh/parallel.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class ParallelSSHClient(BaseParallelSSHClient):
3030
"""ssh-python based parallel client."""
3131

3232
def __init__(self, hosts, user=None, password=None, port=22, pkey=None,
33+
cert_file=None,
3334
num_retries=DEFAULT_RETRIES, timeout=None, pool_size=100,
3435
allow_agent=True, host_config=None, retry_delay=RETRY_DELAY,
3536
forward_ssh_agent=False,
@@ -52,6 +53,13 @@ def __init__(self, hosts, user=None, password=None, port=22, pkey=None,
5253
:param pkey: Private key file path to use. Path must be either absolute
5354
path or relative to user home directory like ``~/<path>``.
5455
:type pkey: str
56+
:param cert_file: Public key signed certificate file to use for
57+
authentication. The corresponding private key must also be provided
58+
via ``pkey`` parameter.
59+
For example ``pkey='id_rsa',cert_file='id_rsa-cert.pub'`` for RSA
60+
signed certificate.
61+
Path must be absolute or relative to user home directory.
62+
:type cert_file: str
5563
:param num_retries: (Optional) Number of connection and authentication
5664
attempts before the client gives up. Defaults to 3.
5765
:type num_retries: int
@@ -126,6 +134,7 @@ def __init__(self, hosts, user=None, password=None, port=22, pkey=None,
126134
host_config=host_config, retry_delay=retry_delay,
127135
identity_auth=identity_auth)
128136
self.pkey = _validate_pkey_path(pkey)
137+
self.cert_file = _validate_pkey_path(cert_file)
129138
self.forward_ssh_agent = forward_ssh_agent
130139
self.gssapi_auth = gssapi_auth
131140
self.gssapi_server_identity = gssapi_server_identity
@@ -243,7 +252,9 @@ def _make_ssh_client(self, host_i, host):
243252
host_i, host)
244253
_client = SSHClient(
245254
host, user=_user, password=_password, port=_port,
246-
pkey=_pkey, num_retries=self.num_retries,
255+
pkey=_pkey,
256+
cert_file=self.cert_file,
257+
num_retries=self.num_retries,
247258
timeout=self.timeout,
248259
allow_agent=self.allow_agent, retry_delay=self.retry_delay,
249260
gssapi_auth=self.gssapi_auth,

pssh/clients/ssh/single.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@
2525
from gevent.select import POLLIN, POLLOUT
2626
from ssh import options
2727
from ssh.session import Session, SSH_READ_PENDING, SSH_WRITE_PENDING
28-
from ssh.key import import_privkey_file
28+
from ssh.key import import_privkey_file, import_cert_file, copy_cert_to_privkey
2929
from ssh.exceptions import EOF
3030
from ssh.error_codes import SSH_AGAIN
3131

3232
from ..base.single import BaseSSHClient
3333
from ...exceptions import AuthenticationError, SessionError, Timeout
3434
from ...constants import DEFAULT_RETRIES, RETRY_DELAY
35+
from ..common import _validate_pkey_path
3536

3637

3738
logger = logging.getLogger(__name__)
@@ -43,6 +44,7 @@ class SSHClient(BaseSSHClient):
4344
def __init__(self, host,
4445
user=None, password=None, port=None,
4546
pkey=None,
47+
cert_file=None,
4648
num_retries=DEFAULT_RETRIES,
4749
retry_delay=RETRY_DELAY,
4850
allow_agent=True, timeout=None,
@@ -64,6 +66,13 @@ def __init__(self, host,
6466
be either absolute path or relative to user home directory
6567
like ``~/<path>``.
6668
:type pkey: str
69+
:param cert_file: Public key signed certificate file to use for
70+
authentication. The corresponding private key must also be provided
71+
via ``pkey`` parameter.
72+
For example ``pkey='id_rsa',cert_file='id_rsa-cert.pub'`` for RSA
73+
signed certificate.
74+
Path must be absolute or relative to user home directory.
75+
:type cert_file: str
6776
:param num_retries: (Optional) Number of connection and authentication
6877
attempts before the client gives up. Defaults to 3.
6978
:type num_retries: int
@@ -96,6 +105,7 @@ def __init__(self, host,
96105
:raises: :py:class:`pssh.exceptions.PKeyFileError` on errors finding
97106
provided private key.
98107
"""
108+
self.cert_file = _validate_pkey_path(cert_file, host)
99109
self.gssapi_auth = gssapi_auth
100110
self.gssapi_server_identity = gssapi_server_identity
101111
self.gssapi_client_identity = gssapi_client_identity
@@ -194,10 +204,18 @@ def _password_auth(self):
194204
raise AuthenticationError("Password authentication failed - %s", ex)
195205

196206
def _pkey_auth(self, pkey, password=None):
197-
password = b'' if not password else password
198207
pkey = import_privkey_file(pkey, passphrase=password)
208+
if self.cert_file is not None:
209+
logger.debug("Certificate file set - trying certificate authentication")
210+
self._import_cert_file(pkey)
199211
self.session.userauth_publickey(pkey)
200212

213+
def _import_cert_file(self, pkey):
214+
cert_key = import_cert_file(self.cert_file)
215+
self.session.userauth_try_publickey(cert_key)
216+
copy_cert_to_privkey(cert_key, pkey)
217+
logger.debug("Imported certificate file %s for pkey %s", self.cert_file, self.pkey)
218+
201219
def open_session(self):
202220
"""Open new channel from session."""
203221
logger.debug("Opening new channel on %s", self.host)

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
gevent>=1.1
22
ssh2-python>=0.22.0
3-
ssh-python>=0.7.0
3+
ssh-python>=0.8.0

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
'*.tests', '*.tests.*')
3838
),
3939
install_requires=[
40-
'gevent>=1.1', 'ssh2-python>=0.22.0', 'ssh-python>=0.7.0'],
40+
'gevent>=1.1', 'ssh2-python>=0.22.0', 'ssh-python>=0.8.0'],
4141
classifiers=[
4242
'Development Status :: 5 - Production/Stable',
4343
'License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)',

tests/embedded_server/ca_host_key

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
-----BEGIN PRIVATE KEY-----
2+
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
39+
niIHADTER1prOuXylm1pWZdk
40+
-----END PRIVATE KEY-----
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[email protected] AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgu84sipb+xoLb4xY/PnjXE6HGJlJKz+VdqW+Z9oMFMv4AAAADAQABAAABgQCphJViWy6xwUQngUeHU2AmHaHj/kkDSI4dHQLoWhYwEQ85CowwDaJ7NhZBr6bpA0OTYBgXcfkQw8ncH2k6HybfeT9KXLogTS/hKwZIz1NY5bvVBHSXI8uzSX5OlBujPUxk99kc1Vm3OA9fKB0iCdufgmdXo+3c3JBG5EqO4H21u8zxwfHmVpigkERGxMYkOMWEjAF7i7kptG9d0Gtijw8GWYUmO7R2xkBWsLaZY3llOdCCH6EDYerfHdUOnkwvm0yhy4UqSt8Cu6XoErhdH4l+uF4Pbz+rWgmQ7GQlpSCgZqrIAyyQyTQOn+heFkGr407lFl+PkClXHK5NgZ90EgXSA+dRNl9Qjw3BO5tUN9jWALEURfT4CQemtSe+1JNMbxsA7+byW1YeLbVZknBMieRhMoerXRvG9ENglkYeFzKeKKAj0pS5FzzegXP4QxK+XAkX1Mw3GTRGkrpwrd8b6FVWf/yFzCFkm//rCQiQ933om57Q3QQriJtABfBH0L9bg2sAAAAAAAAAAAAAAAIAAAAMdGVzdF9jYV9ob3N0AAAAAAAAAAAAAAAA//////////8AAAAAAAAAAAAAAAAAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQCphJViWy6xwUQngUeHU2AmHaHj/kkDSI4dHQLoWhYwEQ85CowwDaJ7NhZBr6bpA0OTYBgXcfkQw8ncH2k6HybfeT9KXLogTS/hKwZIz1NY5bvVBHSXI8uzSX5OlBujPUxk99kc1Vm3OA9fKB0iCdufgmdXo+3c3JBG5EqO4H21u8zxwfHmVpigkERGxMYkOMWEjAF7i7kptG9d0Gtijw8GWYUmO7R2xkBWsLaZY3llOdCCH6EDYerfHdUOnkwvm0yhy4UqSt8Cu6XoErhdH4l+uF4Pbz+rWgmQ7GQlpSCgZqrIAyyQyTQOn+heFkGr407lFl+PkClXHK5NgZ90EgXSA+dRNl9Qjw3BO5tUN9jWALEURfT4CQemtSe+1JNMbxsA7+byW1YeLbVZknBMieRhMoerXRvG9ENglkYeFzKeKKAj0pS5FzzegXP4QxK+XAkX1Mw3GTRGkrpwrd8b6FVWf/yFzCFkm//rCQiQ933om57Q3QQriJtABfBH0L9bg2sAAAGUAAAADHJzYS1zaGEyLTI1NgAAAYBv+fXgU7e+0OW/mBZbG6jlMtEALbF2IxjRe2KbmRMb5V7XVqVHVt1qAsl+aNtGntARVRMLnOn8Nasoquy3N+7voaMrQAADpAx+Ig3o8cNR1Ym86TUX7cuUTaSkWm7gOrfUILzIwJnyTJAV74NUac1LGXHL1w3EiLtkga5I+dsAm7Ba+8XohkK350D2lwjq8msvPsrPdT5IHoTG6XWO4QCJBYiu4FTV8Xye2XjFP60pH0F3RkrIFk9l3ftyJPm/2ptWgWJOM9TJimTcSy3bnt6DjLfUZJmePIXyyETEhBGQkRXyZ6VrJHd+biFdEz1X87BEVQcYoBBL5glnWgPAqsKzI70Z9pmS9SX7qA795Xp6zMJq0Ov/H7mUvZSvhnIip6NF9z4KOAy7mmF9dZR8UABE4upZpT2k0xIO+x+aJChkg8EwYX3z4v4VC+dzCIfUGEIfbrvYnQOh/VvG6A04lnALaJ/iRleXjmrbW2EJCDlOtZMTHqmvc2KLzponpJHyyeM= zefrer@kirin

0 commit comments

Comments
 (0)