summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMikael Magnusson <mikma@users.sourceforge.net>2023-07-19 10:58:18 +0200
committerMikael Magnusson <mikma@users.sourceforge.net>2023-08-02 22:07:45 +0200
commit6c85a310c33b953877092d938940ca7e799f843e (patch)
tree93c3ab642bcc629071556c6897262bf09ce6ee09
parent4753b3619dd0f08185c16e3d3d60b98afdbaa040 (diff)
WIP: working authentication!
-rwxr-xr-xdemos/demo.py11
-rw-r--r--demos/demo_server.py12
-rw-r--r--paramiko/__init__.py4
-rw-r--r--paramiko/auth_handler.py9
-rw-r--r--paramiko/ecdsakey.py4
-rw-r--r--paramiko/ecdsaskkey.py298
-rw-r--r--paramiko/pkey.py2
-rw-r--r--paramiko/transport.py8
8 files changed, 342 insertions, 6 deletions
diff --git a/demos/demo.py b/demos/demo.py
index b9bf7c3f..64539423 100755
--- a/demos/demo.py
+++ b/demos/demo.py
@@ -86,6 +86,17 @@ def manual_auth(username, hostname):
password = getpass.getpass("DSS key password: ")
key = paramiko.DSSKey.from_private_key_file(path, password)
t.auth_publickey(username, key)
+ elif auth == "ecdsa-sk":
+ default_path = os.path.join(os.environ["HOME"], ".ssh", "id_ecdsa_sk")
+ path = input("ECDA-SK key [%s]: " % default_path)
+ if len(path) == 0:
+ path = default_path
+ try:
+ key = paramiko.ECDSASkKey.from_private_key_file(path)
+ except paramiko.PasswordRequiredException:
+ password = getpass.getpass("ECDSA-SK key password: ")
+ key = paramiko.ECDSASkKey.from_private_key_file(path, password)
+ t.auth_publickey(username, key)
else:
pw = getpass.getpass("Password for %s@%s: " % (username, hostname))
t.auth_password(username, pw)
diff --git a/demos/demo_server.py b/demos/demo_server.py
index 7a175ac8..8a6b14f3 100644
--- a/demos/demo_server.py
+++ b/demos/demo_server.py
@@ -41,13 +41,17 @@ print("Read key: " + hexlify(host_key.get_fingerprint()).decode())
class Server(paramiko.ServerInterface):
# 'data' is the output of base64.b64encode(key)
# (using the "user_rsa_key" files)
- data = (
+ data_robey = (
b"AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hp"
b"fAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMC"
b"KDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iT"
b"UWT10hcuO4Ks8="
)
- good_pub_key = paramiko.RSAKey(data=base64.decodebytes(data))
+ data = (
+ b"AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBDPB7gJZxOHIjoT7IEj4BQ3g7j69uRrmUTXLpnXCPqvEBcG4r/hio9OLyghMa7uTfk7j1yKdZ2bRs1SReJpdqccAAAAEc3NoOg=="
+ )
+ #good_pub_key = paramiko.RSAKey(data=base64.decodebytes(data))
+ good_pub_key = paramiko.ECDSASkKey(data=base64.decodebytes(data))
def __init__(self):
self.event = threading.Event()
@@ -64,7 +68,7 @@ class Server(paramiko.ServerInterface):
def check_auth_publickey(self, username, key):
print("Auth attempt with key: " + hexlify(key.get_fingerprint()).decode())
- if (username == "robey") and (key == self.good_pub_key):
+ if (username == "mikael") and (key == self.good_pub_key):
return paramiko.AUTH_SUCCESSFUL
return paramiko.AUTH_FAILED
@@ -112,7 +116,7 @@ class Server(paramiko.ServerInterface):
return True
-DoGSSAPIKeyExchange = True
+DoGSSAPIKeyExchange = False
# now connect
try:
diff --git a/paramiko/__init__.py b/paramiko/__init__.py
index 476062ef..16d46215 100644
--- a/paramiko/__init__.py
+++ b/paramiko/__init__.py
@@ -67,6 +67,7 @@ from paramiko.server import ServerInterface, SubsystemHandler, InteractiveQuery
from paramiko.rsakey import RSAKey
from paramiko.dsskey import DSSKey
from paramiko.ecdsakey import ECDSAKey
+from paramiko.ecdsaskkey import ECDSASkKey
from paramiko.ed25519key import Ed25519Key
from paramiko.sftp import SFTPError, BaseSFTP
from paramiko.sftp_client import SFTP, SFTPClient
@@ -111,7 +112,7 @@ from paramiko.common import io_sleep
# TODO: I guess a real plugin system might be nice for future expansion...
-key_classes = [DSSKey, RSAKey, Ed25519Key, ECDSAKey]
+key_classes = [DSSKey, RSAKey, Ed25519Key, ECDSAKey, ECDSASkKey]
__author__ = "Jeff Forcier <jeff@bitprophet.org>"
@@ -132,6 +133,7 @@ __all__ = [
"CouldNotCanonicalize",
"DSSKey",
"ECDSAKey",
+ "ECDSASkKey",
"Ed25519Key",
"HostKeys",
"Message",
diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py
index bc7f298f..6ad86204 100644
--- a/paramiko/auth_handler.py
+++ b/paramiko/auth_handler.py
@@ -25,6 +25,8 @@ import threading
import time
import re
+from hashlib import sha256
+
from paramiko.common import (
cMSG_SERVICE_REQUEST,
cMSG_DISCONNECT,
@@ -231,6 +233,7 @@ class AuthHandler:
_, bits = self._get_key_type_and_bits(key)
m.add_string(algorithm)
m.add_string(bits)
+
return m.asbytes()
def wait_for_response(self, event):
@@ -385,6 +388,7 @@ class AuthHandler:
m.add_string(self.username)
m.add_string("ssh-connection")
m.add_string(self.auth_method)
+ self._log(DEBUG, "auth method {}".format(self.auth_method))
if self.auth_method == "password":
m.add_boolean(False)
password = b(self.password)
@@ -615,6 +619,7 @@ Error Message: {}
# telling us directly. No need for _finalize_pubkey_algorithm
# anywhere in this flow.
algorithm = m.get_text()
+ self._log(DEBUG, "publickey {}".format(algorithm))
keyblob = m.get_binary()
try:
key = self._generate_key_from_request(algorithm, keyblob)
@@ -632,9 +637,12 @@ Error Message: {}
result = self.transport.server_object.check_auth_publickey(
username, key
)
+ self._log(DEBUG, "publickey res {} {}".format(type(key), result))
if result != AUTH_FAILED:
+ self._log(DEBUG, "publickey not auth_failed")
# key is okay, verify it
if not sig_attached:
+ self._log(DEBUG, "publickey not sig_attached")
# client wants to know if this key is acceptable, before it
# signs anything... send special "ok" message
m = Message()
@@ -647,6 +655,7 @@ Error Message: {}
blob = self._get_session_blob(
key, service, username, algorithm
)
+ print("blob len {}".format(len(blob)))
if not key.verify_ssh_sig(blob, sig):
self._log(INFO, "Auth rejected: invalid signature")
result = AUTH_FAILED
diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py
index 6fd95fab..0765a30b 100644
--- a/paramiko/ecdsakey.py
+++ b/paramiko/ecdsakey.py
@@ -45,12 +45,14 @@ class _ECDSACurve:
package.
"""
+ IDENTIFIER_PREFIX = "ecdsa-sha2-"
+
def __init__(self, curve_class, nist_name):
self.nist_name = nist_name
self.key_length = curve_class.key_size
# Defined in RFC 5656 6.2
- self.key_format_identifier = "ecdsa-sha2-" + self.nist_name
+ self.key_format_identifier = self.IDENTIFIER_PREFIX + self.nist_name
# Defined in RFC 5656 6.2.1
if self.key_length <= 256:
diff --git a/paramiko/ecdsaskkey.py b/paramiko/ecdsaskkey.py
new file mode 100644
index 00000000..4459fd6a
--- /dev/null
+++ b/paramiko/ecdsaskkey.py
@@ -0,0 +1,298 @@
+# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
+#
+# 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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+ECDSA keys
+"""
+
+from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives.asymmetric.utils import (
+ decode_dss_signature,
+ encode_dss_signature,
+)
+
+from hashlib import sha256
+
+from paramiko.common import four_byte
+from paramiko.message import Message
+from paramiko.pkey import PKey
+from paramiko.ecdsakey import (
+ _ECDSACurveSet,
+ _ECDSACurve
+)
+
+from paramiko.ssh_exception import SSHException
+from paramiko.util import deflate_long
+from fido2.server import ( U2FFido2Server )
+from fido2.webauthn import ( PublicKeyCredentialRpEntity )
+from pyu2f import model
+from pyu2f.convenience import authenticator
+import base64
+
+class _ECDSASkCurve(_ECDSACurve):
+ IDENTIFIER_PREFIX = "sk-ecdsa-sha2-"
+
+class ECDSASkKey(PKey):
+ """
+ Representation of an ECDSA security key which can be used to sign and verify SSH2
+ data.
+ """
+
+ _ECDSA_CURVES = _ECDSACurveSet(
+ [
+ _ECDSASkCurve(ec.SECP256R1, "nistp256"),
+ ]
+ )
+
+ def __init__(
+ self,
+ msg=None,
+ data=None,
+ filename=None,
+ password=None,
+ vals=None,
+ file_obj=None,
+ # TODO 4.0: remove; it does nothing since porting to cryptography.io
+ validate_point=True,
+ ):
+# self.public_blob = None
+# return
+
+ self.verifying_key = None
+ self.signing_key = None
+ self.public_blob = None
+ self.sk_app_id = None
+ if file_obj is not None:
+ self._from_private_key(file_obj, password)
+ return
+ if filename is not None:
+ self._from_private_key_file(filename, password)
+ return
+ if (msg is None) and (data is not None):
+ msg = Message(data)
+ if vals is not None:
+ self.signing_key, self.verifying_key = vals
+ c_class = self.signing_key.curve.__class__
+ self.ecdsa_curve = self._ECDSA_CURVES.get_by_curve_class(c_class)
+ else:
+ # 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)]
+ suffix2 = "@openssh.com"
+ if key_type.endswith(suffix2):
+ key_type = key_type[: -len(suffix2)]
+ print("key type {}".format(key_type))
+ self.ecdsa_curve = self._ECDSA_CURVES.get_by_key_format_identifier(
+ key_type
+ )
+ key_types = [
+ "{}@openssh.com".format(x) for x in self._ECDSA_CURVES.get_key_format_identifier_list()
+ ]
+ cert_types = [
+ "{}-cert-v01@openssh.com".format(x) for x in self._ECDSA_CURVES.get_key_format_identifier_list()
+ ]
+ print("key type {} {} {}".format(key_type, key_types, cert_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 {}".format(curvename)
+ )
+
+ pointinfo = msg.get_binary()
+ self.sk_app_id = msg.get_text()
+ print("msg {} {} {} {} {}".format(key_type, self.ecdsa_curve.nist_name, msg, pointinfo, self.sk_app_id))
+ try:
+ key = ec.EllipticCurvePublicKey.from_encoded_point(
+ self.ecdsa_curve.curve_class(), pointinfo
+ )
+ self.verifying_key = key
+ except ValueError:
+ raise SSHException("Invalid public key")
+
+ @classmethod
+ def X_identifiers(cls):
+ return cls._ECDSA_CURVES.get_key_format_identifier_list()
+
+ # TODO 4.0: deprecate/remove
+ @classmethod
+ def X_supported_key_format_identifiers(cls):
+ return cls.identifiers()
+
+ def asbytes(self):
+ key = self.verifying_key
+ m = Message()
+ m.add_string(self._get_name())
+ m.add_string(self.ecdsa_curve.nist_name)
+
+ numbers = key.public_numbers()
+
+ key_size_bytes = (key.curve.key_size + 7) // 8
+
+ x_bytes = deflate_long(numbers.x, add_sign_padding=False)
+ x_bytes = b"\x00" * (key_size_bytes - len(x_bytes)) + x_bytes
+
+ y_bytes = deflate_long(numbers.y, add_sign_padding=False)
+ y_bytes = b"\x00" * (key_size_bytes - len(y_bytes)) + y_bytes
+
+ point_str = four_byte + x_bytes + y_bytes
+ m.add_string(point_str)
+ m.add_string(self.sk_app_id)
+ return m.asbytes()
+
+ def __str__(self):
+ return self.asbytes()
+
+ @property
+ def _fields(self):
+ return (
+ self.get_name(),
+ self.verifying_key.public_numbers().x,
+ self.verifying_key.public_numbers().y,
+ )
+
+ def _get_name(self):
+ return self.ecdsa_curve.key_format_identifier + "@openssh.com"
+
+ def X_get_bits(self):
+ return self.ecdsa_curve.key_length
+
+ def X_can_sign(self):
+ return self.signing_key is not None
+
+ def X_sign_ssh_data(self, data, algorithm=None):
+ ecdsa = ec.ECDSA(self.ecdsa_curve.hash_object())
+ sig = self.signing_key.sign(data, ecdsa)
+ r, s = decode_dss_signature(sig)
+
+ m = Message()
+ m.add_string(self.ecdsa_curve.key_format_identifier)
+ m.add_string(self._sigencode(r, s))
+ return m
+
+ def verify_ssh_sig(self, data, msg):
+ print("verify_ssh_sig {} {}".format(len(data), data))
+ if msg.get_text() != self._get_name():
+ print("bad key format identifier")
+ return False
+ sig = msg.get_binary()
+ sigR, sigS = self._sigdecode(sig)
+ signature = encode_dss_signature(sigR, sigS)
+ flags = msg.get_byte()
+ counter = msg.get_int()
+ if(len(msg.get_remainder()) != 0):
+ print("bad remainder != 0")
+ return False
+
+ try:
+ m = Message()
+ m.add_bytes(sha256(self.sk_app_id.encode('ascii')).digest())
+ m.add_byte(flags)
+ m.add_int(counter)
+# m.add_bytes(b'') # extensions
+ m.add_bytes(sha256(data).digest())
+ m_data = m.asbytes()
+# sk_data = sha256(m_data).digest()
+ sk_data = m_data
+
+ print("u2f sign {} {} {} {} {} {} <{}> <{}>".format(self.sk_app_id, len(sk_data), flags, counter, len(data), len(m_data), data, m_data))
+ print("sk_data {} {}".format(sk_data, self.ecdsa_curve.hash_object()))
+
+ self.verifying_key.verify(
+ signature, sk_data, ec.ECDSA(self.ecdsa_curve.hash_object())
+ )
+ except InvalidSignature:
+ print("sk sig fail")
+ return False
+ else:
+ print("sk sig ok")
+ return True
+
+ # ...internals...
+
+ def _from_private_key_file(self, filename, password):
+ data = self._read_private_key_file("EC", filename, password)
+ self._decode_key(data)
+
+ def _from_private_key(self, file_obj, password):
+ data = self._read_private_key("EC", file_obj, password)
+ self._decode_key(data)
+
+ def _decode_key(self, data):
+ return
+ pkformat, data = data
+ if pkformat == self._PRIVATE_KEY_FORMAT_ORIGINAL:
+ try:
+ key = serialization.load_der_private_key(
+ data, password=None, backend=default_backend()
+ )
+ except (
+ ValueError,
+ AssertionError,
+ TypeError,
+ UnsupportedAlgorithm,
+ ) as e:
+ raise SSHException(str(e))
+ elif pkformat == self._PRIVATE_KEY_FORMAT_OPENSSH:
+ try:
+ msg = Message(data)
+ curve_name = msg.get_text()
+ verkey = msg.get_binary() # noqa: F841
+ sigkey = msg.get_mpint()
+ name = "ecdsa-sha2-" + curve_name
+ curve = self._ECDSA_CURVES.get_by_key_format_identifier(name)
+ if not curve:
+ raise SSHException("Invalid key curve identifier")
+ key = ec.derive_private_key(
+ sigkey, curve.curve_class(), default_backend()
+ )
+ except Exception as e:
+ # PKey._read_private_key_openssh() should check or return
+ # keytype - parsing could fail for any reason due to wrong type
+ raise SSHException(str(e))
+ else:
+ self._got_bad_key_format_id(pkformat)
+
+ self.signing_key = key
+ self.verifying_key = key.public_key()
+ curve_class = key.curve.__class__
+ self.ecdsa_curve = self._ECDSA_CURVES.get_by_curve_class(curve_class)
+
+ def _sigencode(self, r, s):
+ msg = Message()
+ msg.add_mpint(r)
+ msg.add_mpint(s)
+ return msg.asbytes()
+
+ def _sigdecode(self, sig):
+ msg = Message(sig)
+ r = msg.get_mpint()
+ s = msg.get_mpint()
+ return r, s
diff --git a/paramiko/pkey.py b/paramiko/pkey.py
index ef371002..78ba9424 100644
--- a/paramiko/pkey.py
+++ b/paramiko/pkey.py
@@ -793,6 +793,7 @@ class PKey:
type_ = msg.get_text()
# Regular public key - nothing special to do besides the implicit
# type check.
+ print("type {} {} {}".format(type_, key_types, cert_types))
if type_ in key_types:
pass
# OpenSSH-compatible certificate - store full copy as .public_blob
@@ -839,6 +840,7 @@ class PKey:
else:
constructor = "from_string"
blob = getattr(PublicBlob, constructor)(value)
+ print("load_certificate {}".format(blob))
if not blob.key_type.startswith(self.get_name()):
err = "PublicBlob type {} incompatible with key type {}"
raise ValueError(err.format(blob.key_type, self.get_name()))
diff --git a/paramiko/transport.py b/paramiko/transport.py
index 8785d6bb..a7e87259 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -104,6 +104,7 @@ from paramiko.packet import Packetizer, NeedRekeyException
from paramiko.primes import ModulusPack
from paramiko.rsakey import RSAKey
from paramiko.ecdsakey import ECDSAKey
+from paramiko.ecdsaskkey import ECDSASkKey
from paramiko.server import ServerInterface
from paramiko.sftp_client import SFTPClient
from paramiko.ssh_exception import (
@@ -181,6 +182,8 @@ class Transport(threading.Thread, ClosingContextManager):
"ecdsa-sha2-nistp256",
"ecdsa-sha2-nistp384",
"ecdsa-sha2-nistp521",
+ "sk-ecdsa-sha2-nistp256",
+ "sk-ecdsa-sha2-nistp256@openssh.com",
"rsa-sha2-512",
"rsa-sha2-256",
"ssh-rsa",
@@ -192,6 +195,8 @@ class Transport(threading.Thread, ClosingContextManager):
"ecdsa-sha2-nistp256",
"ecdsa-sha2-nistp384",
"ecdsa-sha2-nistp521",
+ "sk-ecdsa-sha2-nistp256",
+ "sk-ecdsa-sha2-nistp256@openssh.com",
"rsa-sha2-512",
"rsa-sha2-256",
"ssh-rsa",
@@ -292,6 +297,9 @@ class Transport(threading.Thread, ClosingContextManager):
"ecdsa-sha2-nistp384-cert-v01@openssh.com": ECDSAKey,
"ecdsa-sha2-nistp521": ECDSAKey,
"ecdsa-sha2-nistp521-cert-v01@openssh.com": ECDSAKey,
+ "sk-ecdsa-sha2-nistp256": ECDSASkKey,
+ "sk-ecdsa-sha2-nistp256@openssh.com": ECDSASkKey,
+ "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com": ECDSASkKey,
"ssh-ed25519": Ed25519Key,
"ssh-ed25519-cert-v01@openssh.com": Ed25519Key,
}