Skip to content

Commit

Permalink
Fixes #325 -- add support for Ed25519 keys
Browse files Browse the repository at this point in the history
  • Loading branch information
alex committed Jun 3, 2017
1 parent 7326702 commit b03ebb2
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 19 deletions.
35 changes: 16 additions & 19 deletions paramiko/client.py
Expand Up @@ -32,6 +32,7 @@
from paramiko.config import SSH_PORT
from paramiko.dsskey import DSSKey
from paramiko.ecdsakey import ECDSAKey
from paramiko.ed25519key import Ed25519Key
from paramiko.hostkeys import HostKeys
from paramiko.py3compat import string_types
from paramiko.resource import ResourceManager
Expand Down Expand Up @@ -586,25 +587,21 @@ def _auth(self, username, password, pkey, key_filenames, allow_agent,

if not two_factor:
keyfiles = []
rsa_key = os.path.expanduser('~/.ssh/id_rsa')
dsa_key = os.path.expanduser('~/.ssh/id_dsa')
ecdsa_key = os.path.expanduser('~/.ssh/id_ecdsa')
if os.path.isfile(rsa_key):
keyfiles.append((RSAKey, rsa_key))
if os.path.isfile(dsa_key):
keyfiles.append((DSSKey, dsa_key))
if os.path.isfile(ecdsa_key):
keyfiles.append((ECDSAKey, ecdsa_key))
# look in ~/ssh/ for windows users:
rsa_key = os.path.expanduser('~/ssh/id_rsa')
dsa_key = os.path.expanduser('~/ssh/id_dsa')
ecdsa_key = os.path.expanduser('~/ssh/id_ecdsa')
if os.path.isfile(rsa_key):
keyfiles.append((RSAKey, rsa_key))
if os.path.isfile(dsa_key):
keyfiles.append((DSSKey, dsa_key))
if os.path.isfile(ecdsa_key):
keyfiles.append((ECDSAKey, ecdsa_key))

for keytype, path in [
(RSAKey, "rsa"),
(DSSKey, "dsa"),
(ECDSAKey, "ecdsa"),
(Ed25519Key, "ed25519"),
]:
full_path = os.path.expanduser("~/.ssh/id_%s" % path)
if os.path.isfile(full_path):
keyfiles.append((keytype, full_path))

# look in ~/ssh/ for windows users:
full_path = os.path.expanduser("~/ssh/id_%s" % path)
if os.path.isfile(full_path):
keyfiles.append((keytype, full_path))

if not look_for_keys:
keyfiles = []
Expand Down
136 changes: 136 additions & 0 deletions paramiko/ed25519key.py
@@ -0,0 +1,136 @@
# 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 distrubuted 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.

import nacl.signing

import six

from paramiko.message import Message
from paramiko.pkey import PKey


OPENSSH_AUTH_MAGIC = "openssh-key-v1\x00"

def unpad(data):
padding_length = six.indexbytes(data, -1)
if padding_length > 16:
raise SSHException('Invalid key')
for i in range(1, padding_length + 1):
if six.indexbytes(data, -i) != (padding_length - i + 1):
raise SSHException('Invalid key')
return data[:-padding_length]


class Ed25519Key(PKey):
def __init__(self, msg=None, data=None, filename=None, password=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')
verifying_key = nacl.signing.VerifyKey(msg.get_bytes(32))
elif filename is not None:
with open(filename, "rb") as f:
data = self._read_private_key("OPENSSH", f)
signing_key = self._parse_signing_key_data(data)

if signing_key is None and verifying_key is None:
import pdb; pdb.set_trace()

self._signing_key = signing_key
self._verifying_key = verifying_key


def _parse_signing_key_data(self, data):
# We may eventually want this to be usable for other key types, as
# OpenSSH moves to it, but for now this is just for Ed25519 keys.
message = Message(data)
if message.get_bytes(len(OPENSSH_AUTH_MAGIC)) != OPENSSH_AUTH_MAGIC:
raise SSHException('Invalid key')

ciphername = message.get_string()
kdfname = message.get_string()
kdfoptions = message.get_string()
num_keys = message.get_int()

if ciphername != "none" or kdfname != "none" or kdfoptions:
raise NotImplementedError("Encrypted keys are not implemented")

public_keys = []
for _ in range(num_keys):
# We don't need the public keys, fast-forward through them.
pubkey = Message(message.get_binary())
if pubkey.get_string() != 'ssh-ed25519':
raise SSHException('Invalid key')
public_keys.append(pubkey.get_binary())

message = Message(unpad(message.get_binary()))
if message.get_int() != message.get_int():
raise SSHException('Invalid key')

signing_keys = []
for i in range(num_keys):
if message.get_string() != 'ssh-ed25519':
raise SSHException('Invalid key')
# A copy of the public key, again, ignore.
public = message.get_binary()
key_data = message.get_binary()
# The second half of the key data is yet another copy of the public
# key...
signing_key = nacl.signing.SigningKey(key_data[:32])
assert (
signing_key.verify_key.encode() == public == public_keys[i] == key_data[32:]
)
signing_keys.append(signing_key)
# Comment, ignore.
message.get_string()

if len(signing_keys) != 1:
raise SSHException('Invalid key')
return signing_keys[0]

def asbytes(self):
m = Message()
m.add_string('ssh-ed25519')
m.add_bytes(self._signing_key.verify_key.encode())
return m.asbytes()

def get_name(self):
return "ssh-ed25519"

def get_bits(self):
return 256

def can_sign(self):
return self._signing_key is not None

def sign_ssh_data(self, data):
m = Message()
m.add_string('ssh-ed25519')
m.add_string(self._signing_key.sign(data).signature)
return m

def verify_ssh_sig(self, data, msg):
if msg.get_text() != 'ssh-ed25519':
return False

try:
self._verifying_key.verify(data, msg.get_binary())
except nacl.exceptions.BadSignatureError:
return False
else:
return True
2 changes: 2 additions & 0 deletions paramiko/hostkeys.py
Expand Up @@ -360,6 +360,8 @@ def from_line(cls, line, lineno=None):
key = DSSKey(data=decodebytes(key))
elif keytype in ECDSAKey.supported_key_format_identifiers():
key = ECDSAKey(data=decodebytes(key), validate_point=False)
elif keytype == 'ssh-ed25519':
key = Ed25519Key(data=decodebytes(key))
else:
log.info("Unable to handle key of type %s" % (keytype,))
return None
Expand Down
3 changes: 3 additions & 0 deletions paramiko/transport.py
Expand Up @@ -54,6 +54,7 @@
)
from paramiko.compress import ZlibCompressor, ZlibDecompressor
from paramiko.dsskey import DSSKey
from paramiko.ed25519key import Ed25519Key
from paramiko.kex_gex import KexGex, KexGexSHA256
from paramiko.kex_group1 import KexGroup1
from paramiko.kex_group14 import KexGroup14
Expand Down Expand Up @@ -123,6 +124,7 @@ class Transport (threading.Thread, ClosingContextManager):
'hmac-sha1',
)
_preferred_keys = (
'ssh-ed25519',
'ssh-rsa',
'ssh-dss',
) + tuple(ECDSAKey.supported_key_format_identifiers())
Expand Down Expand Up @@ -211,6 +213,7 @@ class Transport (threading.Thread, ClosingContextManager):
'ssh-rsa': RSAKey,
'ssh-dss': DSSKey,
'ecdsa-sha2-nistp256': ECDSAKey,
'ssh-ed25519': Ed25519Key,
}

_kex_info = {
Expand Down
1 change: 1 addition & 0 deletions setup.py
Expand Up @@ -75,6 +75,7 @@
],
install_requires=[
'cryptography>=1.1',
'pynacl',
'pyasn1>=0.1.7',
],
)

0 comments on commit b03ebb2

Please sign in to comment.