summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Gaynor <alex.gaynor@gmail.com>2017-05-26 20:42:22 -0400
committerAlex Gaynor <alex.gaynor@gmail.com>2017-06-03 02:02:46 -0400
commitb03ebb2d64ec87da589d6fcfa4f1c00ead40c1a7 (patch)
tree11b2dea580a680cd0f9f40e1c29a375b898f64e1
parent7326702b0fc7bcdf2a811acb46d042deed6f2947 (diff)
downloadparamiko-b03ebb2d64ec87da589d6fcfa4f1c00ead40c1a7.tar.gz
Fixes #325 -- add support for Ed25519 keys
-rw-r--r--paramiko/client.py35
-rw-r--r--paramiko/ed25519key.py136
-rw-r--r--paramiko/hostkeys.py2
-rw-r--r--paramiko/transport.py3
-rw-r--r--setup.py1
5 files changed, 158 insertions, 19 deletions
diff --git a/paramiko/client.py b/paramiko/client.py
index 8325d90f..42b52712 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -32,6 +32,7 @@ from paramiko.common import DEBUG
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
@@ -586,25 +587,21 @@ class SSHClient (ClosingContextManager):
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 = []
diff --git a/paramiko/ed25519key.py b/paramiko/ed25519key.py
new file mode 100644
index 00000000..bf4b9d6e
--- /dev/null
+++ b/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
diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py
index 18a0d333..7586b903 100644
--- a/paramiko/hostkeys.py
+++ b/paramiko/hostkeys.py
@@ -360,6 +360,8 @@ class HostKeyEntry:
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
diff --git a/paramiko/transport.py b/paramiko/transport.py
index b61e82c4..acba0b81 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -54,6 +54,7 @@ from paramiko.common import (
)
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
@@ -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())
@@ -211,6 +213,7 @@ class Transport (threading.Thread, ClosingContextManager):
'ssh-rsa': RSAKey,
'ssh-dss': DSSKey,
'ecdsa-sha2-nistp256': ECDSAKey,
+ 'ssh-ed25519': Ed25519Key,
}
_kex_info = {
diff --git a/setup.py b/setup.py
index 80d5ea7f..2756a76d 100644
--- a/setup.py
+++ b/setup.py
@@ -75,6 +75,7 @@ setup(
],
install_requires=[
'cryptography>=1.1',
+ 'pynacl',
'pyasn1>=0.1.7',
],
)