diff options
author | Mikael Magnusson <mikma@users.sourceforge.net> | 2023-07-19 10:58:18 +0200 |
---|---|---|
committer | Mikael Magnusson <mikma@users.sourceforge.net> | 2023-08-02 22:07:45 +0200 |
commit | 6c85a310c33b953877092d938940ca7e799f843e (patch) | |
tree | 93c3ab642bcc629071556c6897262bf09ce6ee09 | |
parent | 4753b3619dd0f08185c16e3d3d60b98afdbaa040 (diff) |
WIP: working authentication!
-rwxr-xr-x | demos/demo.py | 11 | ||||
-rw-r--r-- | demos/demo_server.py | 12 | ||||
-rw-r--r-- | paramiko/__init__.py | 4 | ||||
-rw-r--r-- | paramiko/auth_handler.py | 9 | ||||
-rw-r--r-- | paramiko/ecdsakey.py | 4 | ||||
-rw-r--r-- | paramiko/ecdsaskkey.py | 298 | ||||
-rw-r--r-- | paramiko/pkey.py | 2 | ||||
-rw-r--r-- | paramiko/transport.py | 8 |
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, } |