mirror of
				https://github.com/jtesta/ssh-audit.git
				synced 2025-11-03 18:52:15 +01:00 
			
		
		
		
	Added support for mixed host key/CA key types (i.e.: RSA host keys signed by ED25519 CAs) (#120).
This commit is contained in:
		@@ -27,7 +27,7 @@ import traceback
 | 
			
		||||
from typing import Dict, List, Set, Sequence, Tuple, Iterable  # noqa: F401
 | 
			
		||||
from typing import Callable, Optional, Union, Any  # noqa: F401
 | 
			
		||||
 | 
			
		||||
from ssh_audit.kexdh import KexGroupExchange_SHA1, KexGroupExchange_SHA256
 | 
			
		||||
from ssh_audit.kexdh import KexDHException, KexGroupExchange_SHA1, KexGroupExchange_SHA256
 | 
			
		||||
from ssh_audit.ssh2_kexdb import SSH2_KexDB
 | 
			
		||||
from ssh_audit.ssh2_kex import SSH2_Kex
 | 
			
		||||
from ssh_audit.ssh_socket import SSH_Socket
 | 
			
		||||
@@ -63,8 +63,8 @@ class GEXTest:
 | 
			
		||||
        try:
 | 
			
		||||
            # Parse the server's KEX.
 | 
			
		||||
            _, payload = s.read_packet(2)
 | 
			
		||||
            SSH2_Kex.parse(payload)
 | 
			
		||||
        except Exception:
 | 
			
		||||
            SSH2_Kex.parse(out, payload)
 | 
			
		||||
        except KexDHException:
 | 
			
		||||
            out.v("Failed to parse server's kex.  Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
@@ -98,7 +98,7 @@ class GEXTest:
 | 
			
		||||
            if gex_alg not in kex.kex_algorithms:
 | 
			
		||||
                out.d('Server does not support the algorithm "' + gex_alg + '".', write_now=True)
 | 
			
		||||
            else:
 | 
			
		||||
                kex_group = kex_group_class()
 | 
			
		||||
                kex_group = kex_group_class(out)
 | 
			
		||||
                out.d('Preparing to perform DH group exchange using ' + gex_alg + ' with min, pref and max modulus sizes of ' + str(bits_min) + ' bits, ' + str(bits_pref) + ' bits and ' + str(bits_max) + ' bits...', write_now=True)
 | 
			
		||||
 | 
			
		||||
                # It has been observed that reconnecting to some SSH servers
 | 
			
		||||
@@ -115,7 +115,7 @@ class GEXTest:
 | 
			
		||||
                    kex_group.recv_reply(s, False)
 | 
			
		||||
                    modulus_size_returned = kex_group.get_dh_modulus_size()
 | 
			
		||||
                    out.d('Modulus size returned by server: ' + str(modulus_size_returned) + ' bits', write_now=True)
 | 
			
		||||
                except Exception:
 | 
			
		||||
                except KexDHException:
 | 
			
		||||
                    out.d('[exception] ' + str(traceback.format_exc()), write_now=True)
 | 
			
		||||
                finally:
 | 
			
		||||
                    # The server is in a state that is not re-testable,
 | 
			
		||||
@@ -155,7 +155,7 @@ class GEXTest:
 | 
			
		||||
                if GEXTest.reconnect(out, s, kex, gex_alg) is False:
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
                kex_group = kex_group_class()
 | 
			
		||||
                kex_group = kex_group_class(out)
 | 
			
		||||
                smallest_modulus = -1
 | 
			
		||||
 | 
			
		||||
                # First try a range of weak sizes.
 | 
			
		||||
@@ -169,7 +169,7 @@ class GEXTest:
 | 
			
		||||
                    smallest_modulus = kex_group.get_dh_modulus_size()
 | 
			
		||||
                    out.d('Modulus size returned by server: ' + str(smallest_modulus) + ' bits', write_now=True)
 | 
			
		||||
 | 
			
		||||
                except Exception:
 | 
			
		||||
                except KexDHException:
 | 
			
		||||
                    out.d('[exception] ' + str(traceback.format_exc()), write_now=True)
 | 
			
		||||
                finally:
 | 
			
		||||
                    s.close()
 | 
			
		||||
@@ -194,8 +194,8 @@ class GEXTest:
 | 
			
		||||
                        kex_group.recv_reply(s, False)
 | 
			
		||||
                        smallest_modulus = kex_group.get_dh_modulus_size()
 | 
			
		||||
                        out.d('Modulus size returned by server: ' + str(smallest_modulus) + ' bits', write_now=True)
 | 
			
		||||
                    except Exception:
 | 
			
		||||
                        out.d('[exception] ' + str(traceback.format_exc()), write_now=True)
 | 
			
		||||
                    except KexDHException as e:
 | 
			
		||||
                        out.d('Exception when testing DH group exchange ' + gex_alg + ' with modulus size ' + str(bits) + '.  (Hint: this is probably normal since the server does not support this modulus size.): ' + str(e), write_now=True)
 | 
			
		||||
                    finally:
 | 
			
		||||
                        # The server is in a state that is not re-testable,
 | 
			
		||||
                        # so there's nothing else to do with this open
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ from typing import Callable, Optional, Union, Any  # noqa: F401
 | 
			
		||||
 | 
			
		||||
import traceback
 | 
			
		||||
 | 
			
		||||
from ssh_audit.kexdh import KexDH, KexGroup1, KexGroup14_SHA1, KexGroup14_SHA256, KexCurve25519_SHA256, KexGroup16_SHA512, KexGroup18_SHA512, KexGroupExchange_SHA1, KexGroupExchange_SHA256, KexNISTP256, KexNISTP384, KexNISTP521
 | 
			
		||||
from ssh_audit.kexdh import KexDH, KexDHException, KexGroup1, KexGroup14_SHA1, KexGroup14_SHA256, KexCurve25519_SHA256, KexGroup16_SHA512, KexGroup18_SHA512, KexGroupExchange_SHA1, KexGroupExchange_SHA256, KexNISTP256, KexNISTP384, KexNISTP521
 | 
			
		||||
from ssh_audit.ssh2_kex import SSH2_Kex
 | 
			
		||||
from ssh_audit.ssh2_kexdb import SSH2_KexDB
 | 
			
		||||
from ssh_audit.ssh_socket import SSH_Socket
 | 
			
		||||
@@ -55,6 +55,7 @@ class HostKeyTest:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    TWO2K_MODULUS_WARNING = '2048-bit modulus only provides 112-bits of symmetric strength'
 | 
			
		||||
    SMALL_ECC_MODULUS_WARNING = '224-bit ECC modulus only provides 112-bits of symmetric strength'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
@@ -82,7 +83,7 @@ class HostKeyTest:
 | 
			
		||||
        for server_kex_alg in server_kex.kex_algorithms:
 | 
			
		||||
            if server_kex_alg in KEX_TO_DHGROUP:
 | 
			
		||||
                kex_str = server_kex_alg
 | 
			
		||||
                kex_group = KEX_TO_DHGROUP[kex_str]()
 | 
			
		||||
                kex_group = KEX_TO_DHGROUP[kex_str](out)
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        if kex_str is not None and kex_group is not None:
 | 
			
		||||
@@ -110,7 +111,6 @@ class HostKeyTest:
 | 
			
		||||
                out.d('Preparing to obtain ' + host_key_type + ' host key...', write_now=True)
 | 
			
		||||
 | 
			
		||||
                cert = host_key_types[host_key_type]['cert']
 | 
			
		||||
                variable_key_len = host_key_types[host_key_type]['variable_key_len']
 | 
			
		||||
 | 
			
		||||
                # If the connection is closed, re-open it and get the kex again.
 | 
			
		||||
                if not s.is_connected():
 | 
			
		||||
@@ -131,7 +131,7 @@ class HostKeyTest:
 | 
			
		||||
                    try:
 | 
			
		||||
                        # Parse the server's KEX.
 | 
			
		||||
                        _, payload = s.read_packet()
 | 
			
		||||
                        SSH2_Kex.parse(payload)
 | 
			
		||||
                        SSH2_Kex.parse(out, payload)
 | 
			
		||||
                    except Exception:
 | 
			
		||||
                        out.v("Failed to parse server's kex.  Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
 | 
			
		||||
                        return
 | 
			
		||||
@@ -139,15 +139,29 @@ class HostKeyTest:
 | 
			
		||||
                # Do the initial DH exchange.  The server responds back
 | 
			
		||||
                # with the host key and its length.  Bingo.  We also get back the host key fingerprint.
 | 
			
		||||
                kex_group.send_init(s)
 | 
			
		||||
                raw_hostkey_bytes = b''
 | 
			
		||||
                try:
 | 
			
		||||
                    host_key = kex_group.recv_reply(s, variable_key_len)
 | 
			
		||||
                    if host_key is not None:
 | 
			
		||||
                        server_kex.set_host_key(host_key_type, host_key)
 | 
			
		||||
                except Exception:
 | 
			
		||||
                    pass
 | 
			
		||||
                    kex_reply = kex_group.recv_reply(s)
 | 
			
		||||
                    raw_hostkey_bytes = kex_reply if kex_reply is not None else b''
 | 
			
		||||
                except KexDHException:
 | 
			
		||||
                    out.v("Failed to parse server's host key.  Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
 | 
			
		||||
 | 
			
		||||
                    # Since parsing this host key failed, there's nothing more to do but close the socket and move on to the next host key type.
 | 
			
		||||
                    s.close()
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                hostkey_modulus_size = kex_group.get_hostkey_size()
 | 
			
		||||
                ca_key_type = kex_group.get_ca_type()
 | 
			
		||||
                ca_modulus_size = kex_group.get_ca_size()
 | 
			
		||||
                out.d("Hostkey type: [%s]; hostkey size: %u; CA type: [%s]; CA modulus size: %u" % (host_key_type, hostkey_modulus_size, ca_key_type, ca_modulus_size), write_now=True)
 | 
			
		||||
 | 
			
		||||
                # Record all the host key info.
 | 
			
		||||
                server_kex.set_host_key(host_key_type, raw_hostkey_bytes, hostkey_modulus_size, ca_key_type, ca_modulus_size)
 | 
			
		||||
 | 
			
		||||
                # Set the hostkey size for all RSA key types since 'ssh-rsa', 'rsa-sha2-256', etc. are all using the same host key.  Note, however, that this may change in the future.
 | 
			
		||||
                if cert is False and host_key_type in HostKeyTest.RSA_FAMILY:
 | 
			
		||||
                    for rsa_type in HostKeyTest.RSA_FAMILY:
 | 
			
		||||
                        server_kex.set_host_key(rsa_type, raw_hostkey_bytes, hostkey_modulus_size, ca_key_type, ca_modulus_size)
 | 
			
		||||
 | 
			
		||||
                # Close the socket, as the connection has
 | 
			
		||||
                # been put in a state that later tests can't use.
 | 
			
		||||
@@ -155,43 +169,53 @@ class HostKeyTest:
 | 
			
		||||
 | 
			
		||||
                # If the host key modulus or CA modulus was successfully parsed, check to see that its a safe size.
 | 
			
		||||
                if hostkey_modulus_size > 0 or ca_modulus_size > 0:
 | 
			
		||||
                    # Set the hostkey size for all RSA key types since 'ssh-rsa',
 | 
			
		||||
                    # 'rsa-sha2-256', etc. are all using the same host key.
 | 
			
		||||
                    # Note, however, that this may change in the future.
 | 
			
		||||
                    if cert is False and host_key_type in HostKeyTest.RSA_FAMILY:
 | 
			
		||||
                        for rsa_type in HostKeyTest.RSA_FAMILY:
 | 
			
		||||
                            server_kex.set_rsa_key_size(rsa_type, hostkey_modulus_size)
 | 
			
		||||
                    elif cert is True:
 | 
			
		||||
                        server_kex.set_rsa_key_size(host_key_type, hostkey_modulus_size, ca_modulus_size)
 | 
			
		||||
                    # The minimum good modulus size for RSA host keys is 3072.  However, since ECC cryptosystems are fundamentally different, the minimum good is 256.
 | 
			
		||||
                    hostkey_min_good = cakey_min_good = 3072
 | 
			
		||||
                    hostkey_min_warn = cakey_min_warn = 2048
 | 
			
		||||
                    hostkey_warn_str = cakey_warn_str = HostKeyTest.TWO2K_MODULUS_WARNING
 | 
			
		||||
                    if host_key_type.startswith('ssh-ed25519') or host_key_type.startswith('ecdsa-sha2-nistp'):
 | 
			
		||||
                        hostkey_min_good = 256
 | 
			
		||||
                        hostkey_min_warn = 224
 | 
			
		||||
                        hostkey_warn_str = HostKeyTest.SMALL_ECC_MODULUS_WARNING
 | 
			
		||||
                    if ca_key_type.startswith('ssh-ed25519') or host_key_type.startswith('ecdsa-sha2-nistp'):
 | 
			
		||||
                        cakey_min_good = 256
 | 
			
		||||
                        cakey_min_warn = 224
 | 
			
		||||
                        cakey_warn_str = HostKeyTest.SMALL_ECC_MODULUS_WARNING
 | 
			
		||||
 | 
			
		||||
                    # Keys smaller than 2048 result in a failure.  Keys smaller 3072 result in a warning.  Update the database accordingly.
 | 
			
		||||
                    if (cert is False) and (hostkey_modulus_size < 3072):
 | 
			
		||||
                        for rsa_type in HostKeyTest.RSA_FAMILY:
 | 
			
		||||
                            alg_list = SSH2_KexDB.ALGORITHMS['key'][rsa_type]
 | 
			
		||||
 | 
			
		||||
                            # Ensure that failure & warning lists exist.
 | 
			
		||||
                            while len(alg_list) < 3:
 | 
			
		||||
                                alg_list.append([])
 | 
			
		||||
 | 
			
		||||
                            # If the key is under 2048, add to the failure list.
 | 
			
		||||
                            if hostkey_modulus_size < 2048:
 | 
			
		||||
                                alg_list[1].append('using small %d-bit modulus' % hostkey_modulus_size)
 | 
			
		||||
                            elif HostKeyTest.TWO2K_MODULUS_WARNING not in alg_list[2]:  # Issue a warning about 2048-bit moduli.
 | 
			
		||||
                                alg_list[2].append(HostKeyTest.TWO2K_MODULUS_WARNING)
 | 
			
		||||
 | 
			
		||||
                    elif (cert is True) and ((hostkey_modulus_size < 3072) or (ca_modulus_size > 0 and ca_modulus_size < 3072)):  # pylint: disable=chained-comparison
 | 
			
		||||
                    if (cert is False) and (hostkey_modulus_size < hostkey_min_good):
 | 
			
		||||
                        alg_list = SSH2_KexDB.ALGORITHMS['key'][host_key_type]
 | 
			
		||||
                        min_modulus = min(hostkey_modulus_size, ca_modulus_size)
 | 
			
		||||
                        min_modulus = min_modulus if min_modulus > 0 else max(hostkey_modulus_size, ca_modulus_size)
 | 
			
		||||
 | 
			
		||||
                        # Ensure that failure & warning lists exist.
 | 
			
		||||
                        while len(alg_list) < 3:
 | 
			
		||||
                            alg_list.append([])
 | 
			
		||||
 | 
			
		||||
                        if (hostkey_modulus_size < 2048) or (ca_modulus_size > 0 and ca_modulus_size < 2048):  # pylint: disable=chained-comparison
 | 
			
		||||
                            alg_list[1].append('using small %d-bit modulus' % min_modulus)
 | 
			
		||||
                        elif HostKeyTest.TWO2K_MODULUS_WARNING not in alg_list[2]:
 | 
			
		||||
                            alg_list[2].append(HostKeyTest.TWO2K_MODULUS_WARNING)
 | 
			
		||||
                        # If the key is under 2048, add to the failure list.
 | 
			
		||||
                        if hostkey_modulus_size < hostkey_min_warn:
 | 
			
		||||
                            alg_list[1].append('using small %d-bit modulus' % hostkey_modulus_size)
 | 
			
		||||
                        elif hostkey_warn_str not in alg_list[2]:  # Issue a warning about 2048-bit moduli.
 | 
			
		||||
                            alg_list[2].append(hostkey_warn_str)
 | 
			
		||||
 | 
			
		||||
                    elif (cert is True) and ((hostkey_modulus_size < hostkey_min_good) or (0 < ca_modulus_size < cakey_min_good)):
 | 
			
		||||
                        alg_list = SSH2_KexDB.ALGORITHMS['key'][host_key_type]
 | 
			
		||||
 | 
			
		||||
                        # Ensure that failure & warning lists exist.
 | 
			
		||||
                        while len(alg_list) < 3:
 | 
			
		||||
                            alg_list.append([])
 | 
			
		||||
 | 
			
		||||
                        # If the host key is smaller than 2048-bit/224-bit, flag this as a failure.
 | 
			
		||||
                        if hostkey_modulus_size < hostkey_min_warn:
 | 
			
		||||
                            alg_list[1].append('using small %d-bit hostkey modulus' % hostkey_modulus_size)
 | 
			
		||||
                        # Otherwise, this is just a warning.
 | 
			
		||||
                        elif (hostkey_modulus_size < hostkey_min_good) and (hostkey_warn_str not in alg_list[2]):
 | 
			
		||||
                            alg_list[2].append(hostkey_warn_str)
 | 
			
		||||
 | 
			
		||||
                        # If the CA key is smaller than 2048-bit/224-bit, flag this as a failure.
 | 
			
		||||
                        if 0 < ca_modulus_size < cakey_min_warn:
 | 
			
		||||
                            alg_list[1].append('using small %d-bit CA key modulus' % ca_modulus_size)
 | 
			
		||||
                        # Otherwise, this is just a warning.
 | 
			
		||||
                        elif (0 < ca_modulus_size < cakey_min_good) and (cakey_warn_str not in alg_list[2]):
 | 
			
		||||
                            alg_list[2].append(cakey_warn_str)
 | 
			
		||||
 | 
			
		||||
                # If this host key type is in the RSA family, then mark them all as parsed (since results in one are valid for them all).
 | 
			
		||||
                if host_key_type in HostKeyTest.RSA_FAMILY:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
"""
 | 
			
		||||
   The MIT License (MIT)
 | 
			
		||||
 | 
			
		||||
   Copyright (C) 2017-2021 Joe Testa (jtesta@positronsecurity.com)
 | 
			
		||||
   Copyright (C) 2017-2023 Joe Testa (jtesta@positronsecurity.com)
 | 
			
		||||
   Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
 | 
			
		||||
 | 
			
		||||
   Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
@@ -31,6 +31,7 @@ import struct
 | 
			
		||||
from typing import Dict, List, Set, Sequence, Tuple, Iterable  # noqa: F401
 | 
			
		||||
from typing import Callable, Optional, Union, Any  # noqa: F401
 | 
			
		||||
 | 
			
		||||
from ssh_audit.outputbuffer import OutputBuffer
 | 
			
		||||
from ssh_audit.protocol import Protocol
 | 
			
		||||
from ssh_audit.ssh_socket import SSH_Socket
 | 
			
		||||
 | 
			
		||||
@@ -40,7 +41,8 @@ class KexDHException(Exception):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KexDH:  # pragma: nocover
 | 
			
		||||
    def __init__(self, kex_name: str, hash_alg: str, g: int, p: int) -> None:
 | 
			
		||||
    def __init__(self, out: 'OutputBuffer', kex_name: str, hash_alg: str, g: int, p: int) -> None:
 | 
			
		||||
        self.out = out
 | 
			
		||||
        self.__kex_name = kex_name  # pylint: disable=unused-private-member
 | 
			
		||||
        self.__hash_alg = hash_alg  # pylint: disable=unused-private-member
 | 
			
		||||
        self.__g = 0
 | 
			
		||||
@@ -51,10 +53,11 @@ class KexDH:  # pragma: nocover
 | 
			
		||||
        self.set_params(g, p)
 | 
			
		||||
 | 
			
		||||
        self.__ed25519_pubkey: Optional[bytes] = None  # pylint: disable=unused-private-member
 | 
			
		||||
        self.__hostkey_type: Optional[bytes] = None
 | 
			
		||||
        self.__hostkey_type = ''
 | 
			
		||||
        self.__hostkey_e = 0  # pylint: disable=unused-private-member
 | 
			
		||||
        self.__hostkey_n = 0  # pylint: disable=unused-private-member
 | 
			
		||||
        self.__hostkey_n_len = 0  # Length of the host key modulus.
 | 
			
		||||
        self.__ca_key_type = ''  # Type of CA key ('ssh-rsa', etc).
 | 
			
		||||
        self.__ca_n_len = 0  # Length of the CA key modulus (if hostkey is a cert).
 | 
			
		||||
 | 
			
		||||
    def set_params(self, g: int, p: int) -> None:
 | 
			
		||||
@@ -76,6 +79,14 @@ class KexDH:  # pragma: nocover
 | 
			
		||||
    # contains the host key, among other things.  Function returns the host
 | 
			
		||||
    # key blob (from which the fingerprint can be calculated).
 | 
			
		||||
    def recv_reply(self, s: 'SSH_Socket', parse_host_key_size: bool = True) -> Optional[bytes]:
 | 
			
		||||
        # Reset the CA info, in case it was set from a prior invokation.
 | 
			
		||||
        self.__hostkey_type = ''
 | 
			
		||||
        self.__hostkey_e = 0  # pylint: disable=unused-private-member
 | 
			
		||||
        self.__hostkey_n = 0  # pylint: disable=unused-private-member
 | 
			
		||||
        self.__hostkey_n_len = 0
 | 
			
		||||
        self.__ca_key_type = ''
 | 
			
		||||
        self.__ca_n_len = 0
 | 
			
		||||
 | 
			
		||||
        packet_type, payload = s.read_packet(2)
 | 
			
		||||
 | 
			
		||||
        # Skip any & all MSG_DEBUG messages.
 | 
			
		||||
@@ -88,23 +99,12 @@ class KexDH:  # pragma: nocover
 | 
			
		||||
            # A connection error occurred.  We can't parse anything, so just
 | 
			
		||||
            # return.  The host key modulus (and perhaps certificate modulus)
 | 
			
		||||
            # will remain at length 0.
 | 
			
		||||
            self.out.d("KexDH.recv_reply(): received packge_type == -1.")
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        hostkey_len = 0  # pylint: disable=unused-variable
 | 
			
		||||
        hostkey_type_len = hostkey_e_len = 0  # pylint: disable=unused-variable
 | 
			
		||||
        key_id_len = principles_len = 0  # pylint: disable=unused-variable
 | 
			
		||||
        critical_options_len = extensions_len = 0  # pylint: disable=unused-variable
 | 
			
		||||
        nonce_len = ca_key_len = ca_key_type_len = 0  # pylint: disable=unused-variable
 | 
			
		||||
        ca_key_len = ca_key_type_len = ca_key_e_len = 0  # pylint: disable=unused-variable
 | 
			
		||||
 | 
			
		||||
        key_id = principles = None  # pylint: disable=unused-variable
 | 
			
		||||
        critical_options = extensions = None  # pylint: disable=unused-variable
 | 
			
		||||
        nonce = ca_key = ca_key_type = None  # pylint: disable=unused-variable
 | 
			
		||||
        ca_key_e = ca_key_n = None  # pylint: disable=unused-variable
 | 
			
		||||
 | 
			
		||||
        # Get the host key blob, F, and signature.
 | 
			
		||||
        ptr = 0
 | 
			
		||||
        hostkey, hostkey_len, ptr = KexDH.__get_bytes(payload, ptr)
 | 
			
		||||
        hostkey, _, ptr = KexDH.__get_bytes(payload, ptr)
 | 
			
		||||
 | 
			
		||||
        # If we are not supposed to parse the host key size (i.e.: it is a type that is of fixed size such as ed25519), then stop here.
 | 
			
		||||
        if not parse_host_key_size:
 | 
			
		||||
@@ -116,76 +116,106 @@ class KexDH:  # pragma: nocover
 | 
			
		||||
        # Now pick apart the host key blob.
 | 
			
		||||
        # Get the host key type (i.e.: 'ssh-rsa', 'ssh-ed25519', etc).
 | 
			
		||||
        ptr = 0
 | 
			
		||||
        self.__hostkey_type, hostkey_type_len, ptr = KexDH.__get_bytes(hostkey, ptr)
 | 
			
		||||
        hostkey_type, _, ptr = KexDH.__get_bytes(hostkey, ptr)
 | 
			
		||||
        self.__hostkey_type = hostkey_type.decode('ascii')
 | 
			
		||||
        self.out.d("Parsing host key type: %s" % self.__hostkey_type)
 | 
			
		||||
 | 
			
		||||
        # If this is an RSA certificate, skip over the nonce.
 | 
			
		||||
        if self.__hostkey_type.startswith(b'ssh-rsa-cert-v0'):
 | 
			
		||||
            nonce, nonce_len, ptr = KexDH.__get_bytes(hostkey, ptr)
 | 
			
		||||
        if self.__hostkey_type.startswith('ssh-rsa-cert-v0'):
 | 
			
		||||
            self.out.d("RSA certificate found, so skipping nonce.")
 | 
			
		||||
            _, _, ptr = KexDH.__get_bytes(hostkey, ptr)  # Read & skip over the nonce.
 | 
			
		||||
 | 
			
		||||
        # The public key exponent.
 | 
			
		||||
        hostkey_e, hostkey_e_len, ptr = KexDH.__get_bytes(hostkey, ptr)
 | 
			
		||||
        hostkey_e, _, ptr = KexDH.__get_bytes(hostkey, ptr)
 | 
			
		||||
        self.__hostkey_e = int(binascii.hexlify(hostkey_e), 16)  # pylint: disable=unused-private-member
 | 
			
		||||
 | 
			
		||||
        # Here is the modulus size & actual modulus of the host key public key.
 | 
			
		||||
        hostkey_n, self.__hostkey_n_len, ptr = KexDH.__get_bytes(hostkey, ptr)
 | 
			
		||||
        self.__hostkey_n = int(binascii.hexlify(hostkey_n), 16)  # pylint: disable=unused-private-member
 | 
			
		||||
        # ED25519 moduli are fixed at 32 bytes.
 | 
			
		||||
        if self.__hostkey_type == 'ssh-ed25519':
 | 
			
		||||
            self.out.d("%s has a fixed host key modulus of 32." % self.__hostkey_type)
 | 
			
		||||
            self.__hostkey_n_len = 32
 | 
			
		||||
        else:
 | 
			
		||||
            # Here is the modulus size & actual modulus of the host key public key.
 | 
			
		||||
            hostkey_n, self.__hostkey_n_len, ptr = KexDH.__get_bytes(hostkey, ptr)
 | 
			
		||||
            self.__hostkey_n = int(binascii.hexlify(hostkey_n), 16)  # pylint: disable=unused-private-member
 | 
			
		||||
 | 
			
		||||
        # If this is an RSA certificate, continue parsing to extract the CA
 | 
			
		||||
        # key.
 | 
			
		||||
        if self.__hostkey_type.startswith(b'ssh-rsa-cert-v0'):
 | 
			
		||||
            # Skip over the serial number.
 | 
			
		||||
            ptr += 8
 | 
			
		||||
 | 
			
		||||
            # Get the certificate type.
 | 
			
		||||
            cert_type = int(binascii.hexlify(hostkey[ptr:ptr + 4]), 16)
 | 
			
		||||
            ptr += 4
 | 
			
		||||
 | 
			
		||||
            # Only SSH2_CERT_TYPE_HOST (2) makes sense in this context.
 | 
			
		||||
            if cert_type == 2:
 | 
			
		||||
 | 
			
		||||
                # Skip the key ID (this is the serial number of the
 | 
			
		||||
                # certificate).
 | 
			
		||||
                key_id, key_id_len, ptr = KexDH.__get_bytes(hostkey, ptr)
 | 
			
		||||
 | 
			
		||||
                # The principles, which are... I don't know what.
 | 
			
		||||
                principles, principles_len, ptr = KexDH.__get_bytes(hostkey, ptr)
 | 
			
		||||
 | 
			
		||||
                # Skip over the timestamp that this certificate is valid after.
 | 
			
		||||
                ptr += 8
 | 
			
		||||
 | 
			
		||||
                # Skip over the timestamp that this certificate is valid before.
 | 
			
		||||
                ptr += 8
 | 
			
		||||
 | 
			
		||||
                # TODO: validate the principles, and time range.
 | 
			
		||||
 | 
			
		||||
                # The critical options.
 | 
			
		||||
                critical_options, critical_options_len, ptr = KexDH.__get_bytes(hostkey, ptr)
 | 
			
		||||
 | 
			
		||||
                # Certificate extensions.
 | 
			
		||||
                extensions, extensions_len, ptr = KexDH.__get_bytes(hostkey, ptr)
 | 
			
		||||
 | 
			
		||||
                # Another nonce.
 | 
			
		||||
                nonce, nonce_len, ptr = KexDH.__get_bytes(hostkey, ptr)
 | 
			
		||||
 | 
			
		||||
                # Finally, we get to the CA key.
 | 
			
		||||
                ca_key, ca_key_len, ptr = KexDH.__get_bytes(hostkey, ptr)
 | 
			
		||||
 | 
			
		||||
                # Last in the host key blob is the CA signature.  It isn't
 | 
			
		||||
                # interesting to us, so we won't bother parsing any further.
 | 
			
		||||
                # The CA key has the modulus, however...
 | 
			
		||||
                ptr = 0
 | 
			
		||||
 | 
			
		||||
                # 'ssh-rsa', 'rsa-sha2-256', etc.
 | 
			
		||||
                ca_key_type, ca_key_type_len, ptr = KexDH.__get_bytes(ca_key, ptr)
 | 
			
		||||
 | 
			
		||||
                # CA's public key exponent.
 | 
			
		||||
                ca_key_e, ca_key_e_len, ptr = KexDH.__get_bytes(ca_key, ptr)
 | 
			
		||||
 | 
			
		||||
                # CA's modulus.  Bingo.
 | 
			
		||||
                ca_key_n, self.__ca_n_len, ptr = KexDH.__get_bytes(ca_key, ptr)
 | 
			
		||||
        # If this is a certificate, continue parsing to extract the CA type and key length.  Even though a hostkey type might be 'ssh-ed25519-cert-v01@openssh.com', its CA may still be RSA.
 | 
			
		||||
        if self.__hostkey_type.startswith('ssh-rsa-cert-v0') or self.__hostkey_type.startswith('ssh-ed25519-cert-v0'):
 | 
			
		||||
            # Get the CA key type and key length.
 | 
			
		||||
            self.__ca_key_type, self.__ca_n_len = self.__parse_ca_key(hostkey, self.__hostkey_type, ptr)
 | 
			
		||||
            self.out.d("KexDH.__parse_ca_key(): CA key type: [%s]; CA key length: %u" % (self.__ca_key_type, self.__ca_n_len))
 | 
			
		||||
 | 
			
		||||
        return hostkey
 | 
			
		||||
 | 
			
		||||
    def __parse_ca_key(self, hostkey: bytes, hostkey_type: str, ptr: int) -> Tuple[str, int]:
 | 
			
		||||
        ca_key_type = ''
 | 
			
		||||
        ca_key_n_len = 0
 | 
			
		||||
 | 
			
		||||
        # If this is a certificate, continue parsing to extract the CA type and key length.  Even though a hostkey type might be 'ssh-ed25519-cert-v01@openssh.com', its CA may still be RSA.
 | 
			
		||||
        # if hostkey_type.startswith('ssh-rsa-cert-v0') or hostkey_type.startswith('ssh-ed25519-cert-v0'):
 | 
			
		||||
        self.out.d("Parsing CA for hostkey type [%s]..." % hostkey_type)
 | 
			
		||||
 | 
			
		||||
        # Skip over the serial number.
 | 
			
		||||
        ptr += 8
 | 
			
		||||
 | 
			
		||||
        # Get the certificate type.
 | 
			
		||||
        cert_type = int(binascii.hexlify(hostkey[ptr:ptr + 4]), 16)
 | 
			
		||||
        ptr += 4
 | 
			
		||||
 | 
			
		||||
        # Only SSH2_CERT_TYPE_HOST (2) makes sense in this context.
 | 
			
		||||
        if cert_type == 2:
 | 
			
		||||
 | 
			
		||||
            # Skip the key ID (this is the serial number of the
 | 
			
		||||
            # certificate).
 | 
			
		||||
            key_id, key_id_len, ptr = KexDH.__get_bytes(hostkey, ptr)  # pylint: disable=unused-variable
 | 
			
		||||
 | 
			
		||||
            # The principles, which are... I don't know what.
 | 
			
		||||
            principles, principles_len, ptr = KexDH.__get_bytes(hostkey, ptr)  # pylint: disable=unused-variable
 | 
			
		||||
 | 
			
		||||
            # Skip over the timestamp that this certificate is valid after.
 | 
			
		||||
            ptr += 8
 | 
			
		||||
 | 
			
		||||
            # Skip over the timestamp that this certificate is valid before.
 | 
			
		||||
            ptr += 8
 | 
			
		||||
 | 
			
		||||
            # TODO: validate the principles, and time range.
 | 
			
		||||
 | 
			
		||||
            # The critical options.
 | 
			
		||||
            critical_options, critical_options_len, ptr = KexDH.__get_bytes(hostkey, ptr)  # pylint: disable=unused-variable
 | 
			
		||||
 | 
			
		||||
            # Certificate extensions.
 | 
			
		||||
            extensions, extensions_len, ptr = KexDH.__get_bytes(hostkey, ptr)  # pylint: disable=unused-variable
 | 
			
		||||
 | 
			
		||||
            # Another nonce.
 | 
			
		||||
            nonce, nonce_len, ptr = KexDH.__get_bytes(hostkey, ptr)  # pylint: disable=unused-variable
 | 
			
		||||
 | 
			
		||||
            # Finally, we get to the CA key.
 | 
			
		||||
            ca_key, ca_key_len, ptr = KexDH.__get_bytes(hostkey, ptr)  # pylint: disable=unused-variable
 | 
			
		||||
 | 
			
		||||
            # Last in the host key blob is the CA signature.  It isn't
 | 
			
		||||
            # interesting to us, so we won't bother parsing any further.
 | 
			
		||||
            # The CA key has the modulus, however...
 | 
			
		||||
            ptr = 0
 | 
			
		||||
 | 
			
		||||
            # 'ssh-rsa', 'rsa-sha2-256', etc.
 | 
			
		||||
            ca_key_type_bytes, ca_key_type_len, ptr = KexDH.__get_bytes(ca_key, ptr)  # pylint: disable=unused-variable
 | 
			
		||||
            ca_key_type = ca_key_type_bytes.decode('ascii')
 | 
			
		||||
            self.out.d("Found CA type: [%s]" % ca_key_type)
 | 
			
		||||
 | 
			
		||||
            # ED25519 CA's don't explicitly include the modulus size in the public key, since its fixed at 32 in all cases.
 | 
			
		||||
            if ca_key_type == 'ssh-ed25519':
 | 
			
		||||
                ca_key_n_len = 32
 | 
			
		||||
            else:
 | 
			
		||||
                # CA's public key exponent.
 | 
			
		||||
                ca_key_e, ca_key_e_len, ptr = KexDH.__get_bytes(ca_key, ptr)  # pylint: disable=unused-variable
 | 
			
		||||
 | 
			
		||||
                # CA's modulus.  Bingo.
 | 
			
		||||
                ca_key_n, ca_key_n_len, ptr = KexDH.__get_bytes(ca_key, ptr)  # pylint: disable=unused-variable
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            self.out.d("Certificate type %u found; this is not usually valid in the context of a host key!  Skipping it..." % cert_type)
 | 
			
		||||
 | 
			
		||||
        return ca_key_type, ca_key_n_len
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def __get_bytes(buf: bytes, ptr: int) -> Tuple[bytes, int, int]:
 | 
			
		||||
        num_bytes = struct.unpack('>I', buf[ptr:ptr + 4])[0]
 | 
			
		||||
@@ -205,10 +235,18 @@ class KexDH:  # pragma: nocover
 | 
			
		||||
            size = size - 8
 | 
			
		||||
        return size
 | 
			
		||||
 | 
			
		||||
    # Returns the hostkey type.
 | 
			
		||||
    def get_hostkey_type(self) -> str:
 | 
			
		||||
        return self.__hostkey_type
 | 
			
		||||
 | 
			
		||||
    # Returns the size of the hostkey, in bits.
 | 
			
		||||
    def get_hostkey_size(self) -> int:
 | 
			
		||||
        return KexDH.__adjust_key_size(self.__hostkey_n_len)
 | 
			
		||||
 | 
			
		||||
    # Returns the CA type ('ssh-rsa', 'ssh-ed25519', etc).
 | 
			
		||||
    def get_ca_type(self) -> str:
 | 
			
		||||
        return self.__ca_key_type
 | 
			
		||||
 | 
			
		||||
    # Returns the size of the CA key, in bits.
 | 
			
		||||
    def get_ca_size(self) -> int:
 | 
			
		||||
        return KexDH.__adjust_key_size(self.__ca_n_len)
 | 
			
		||||
@@ -220,46 +258,46 @@ class KexDH:  # pragma: nocover
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KexGroup1(KexDH):  # pragma: nocover
 | 
			
		||||
    def __init__(self) -> None:
 | 
			
		||||
    def __init__(self, out: 'OutputBuffer') -> None:
 | 
			
		||||
        # rfc2409: second oakley group
 | 
			
		||||
        p = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece65381ffffffffffffffff', 16)
 | 
			
		||||
        super(KexGroup1, self).__init__('KexGroup1', 'sha1', 2, p)
 | 
			
		||||
        super(KexGroup1, self).__init__(out, 'KexGroup1', 'sha1', 2, p)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KexGroup14(KexDH):  # pragma: nocover
 | 
			
		||||
    def __init__(self, hash_alg: str) -> None:
 | 
			
		||||
    def __init__(self, out: 'OutputBuffer', hash_alg: str) -> None:
 | 
			
		||||
        # rfc3526: 2048-bit modp group
 | 
			
		||||
        p = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff', 16)
 | 
			
		||||
        super(KexGroup14, self).__init__('KexGroup14', hash_alg, 2, p)
 | 
			
		||||
        super(KexGroup14, self).__init__(out, 'KexGroup14', hash_alg, 2, p)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KexGroup14_SHA1(KexGroup14):
 | 
			
		||||
    def __init__(self) -> None:
 | 
			
		||||
        super(KexGroup14_SHA1, self).__init__('sha1')
 | 
			
		||||
    def __init__(self, out: 'OutputBuffer') -> None:
 | 
			
		||||
        super(KexGroup14_SHA1, self).__init__(out, 'sha1')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KexGroup14_SHA256(KexGroup14):
 | 
			
		||||
    def __init__(self) -> None:
 | 
			
		||||
        super(KexGroup14_SHA256, self).__init__('sha256')
 | 
			
		||||
    def __init__(self, out: 'OutputBuffer') -> None:
 | 
			
		||||
        super(KexGroup14_SHA256, self).__init__(out, 'sha256')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KexGroup16_SHA512(KexDH):
 | 
			
		||||
    def __init__(self) -> None:
 | 
			
		||||
    def __init__(self, out: 'OutputBuffer') -> None:
 | 
			
		||||
        # rfc3526: 4096-bit modp group
 | 
			
		||||
        p = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aaac42dad33170d04507a33a85521abdf1cba64ecfb850458dbef0a8aea71575d060c7db3970f85a6e1e4c7abf5ae8cdb0933d71e8c94e04a25619dcee3d2261ad2ee6bf12ffa06d98a0864d87602733ec86a64521f2b18177b200cbbe117577a615d6c770988c0bad946e208e24fa074e5ab3143db5bfce0fd108e4b82d120a92108011a723c12a787e6d788719a10bdba5b2699c327186af4e23c1a946834b6150bda2583e9ca2ad44ce8dbbbc2db04de8ef92e8efc141fbecaa6287c59474e6bc05d99b2964fa090c3a2233ba186515be7ed1f612970cee2d7afb81bdd762170481cd0069127d5b05aa993b4ea988d8fddc186ffb7dc90a6c08f4df435c934063199ffffffffffffffff', 16)
 | 
			
		||||
        super(KexGroup16_SHA512, self).__init__('KexGroup16_SHA512', 'sha512', 2, p)
 | 
			
		||||
        super(KexGroup16_SHA512, self).__init__(out, 'KexGroup16_SHA512', 'sha512', 2, p)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KexGroup18_SHA512(KexDH):
 | 
			
		||||
    def __init__(self) -> None:
 | 
			
		||||
    def __init__(self, out: 'OutputBuffer') -> None:
 | 
			
		||||
        # rfc3526: 8192-bit modp group
 | 
			
		||||
        p = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aaac42dad33170d04507a33a85521abdf1cba64ecfb850458dbef0a8aea71575d060c7db3970f85a6e1e4c7abf5ae8cdb0933d71e8c94e04a25619dcee3d2261ad2ee6bf12ffa06d98a0864d87602733ec86a64521f2b18177b200cbbe117577a615d6c770988c0bad946e208e24fa074e5ab3143db5bfce0fd108e4b82d120a92108011a723c12a787e6d788719a10bdba5b2699c327186af4e23c1a946834b6150bda2583e9ca2ad44ce8dbbbc2db04de8ef92e8efc141fbecaa6287c59474e6bc05d99b2964fa090c3a2233ba186515be7ed1f612970cee2d7afb81bdd762170481cd0069127d5b05aa993b4ea988d8fddc186ffb7dc90a6c08f4df435c93402849236c3fab4d27c7026c1d4dcb2602646dec9751e763dba37bdf8ff9406ad9e530ee5db382f413001aeb06a53ed9027d831179727b0865a8918da3edbebcf9b14ed44ce6cbaced4bb1bdb7f1447e6cc254b332051512bd7af426fb8f401378cd2bf5983ca01c64b92ecf032ea15d1721d03f482d7ce6e74fef6d55e702f46980c82b5a84031900b1c9e59e7c97fbec7e8f323a97a7e36cc88be0f1d45b7ff585ac54bd407b22b4154aacc8f6d7ebf48e1d814cc5ed20f8037e0a79715eef29be32806a1d58bb7c5da76f550aa3d8a1fbff0eb19ccb1a313d55cda56c9ec2ef29632387fe8d76e3c0468043e8f663f4860ee12bf2d5b0b7474d6e694f91e6dbe115974a3926f12fee5e438777cb6a932df8cd8bec4d073b931ba3bc832b68d9dd300741fa7bf8afc47ed2576f6936ba424663aab639c5ae4f5683423b4742bf1c978238f16cbe39d652de3fdb8befc848ad922222e04a4037c0713eb57a81a23f0c73473fc646cea306b4bcbc8862f8385ddfa9d4b7fa2c087e879683303ed5bdd3a062b3cf5b3a278a66d2a13f83f44f82ddf310ee074ab6a364597e899a0255dc164f31cc50846851df9ab48195ded7ea1b1d510bd7ee74d73faf36bc31ecfa268359046f4eb879f924009438b481c6cd7889a002ed5ee382bc9190da6fc026e479558e4475677e9aa9e3050e2765694dfc81f56e880b96e7160c980dd98edd3dfffffffffffffffff', 16)
 | 
			
		||||
        super(KexGroup18_SHA512, self).__init__('KexGroup18_SHA512', 'sha512', 2, p)
 | 
			
		||||
        super(KexGroup18_SHA512, self).__init__(out, 'KexGroup18_SHA512', 'sha512', 2, p)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KexCurve25519_SHA256(KexDH):
 | 
			
		||||
    def __init__(self) -> None:
 | 
			
		||||
        super(KexCurve25519_SHA256, self).__init__('KexCurve25519_SHA256', 'sha256', 0, 0)
 | 
			
		||||
    def __init__(self, out: 'OutputBuffer') -> None:
 | 
			
		||||
        super(KexCurve25519_SHA256, self).__init__(out, 'KexCurve25519_SHA256', 'sha256', 0, 0)
 | 
			
		||||
 | 
			
		||||
    # To start an ED25519 kex, we simply send a random 256-bit number as the
 | 
			
		||||
    # public key.
 | 
			
		||||
@@ -271,8 +309,8 @@ class KexCurve25519_SHA256(KexDH):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KexNISTP256(KexDH):
 | 
			
		||||
    def __init__(self) -> None:
 | 
			
		||||
        super(KexNISTP256, self).__init__('KexNISTP256', 'sha256', 0, 0)
 | 
			
		||||
    def __init__(self, out: 'OutputBuffer') -> None:
 | 
			
		||||
        super(KexNISTP256, self).__init__(out, 'KexNISTP256', 'sha256', 0, 0)
 | 
			
		||||
 | 
			
		||||
    # Because the server checks that the value sent here is valid (i.e.: it lies
 | 
			
		||||
    # on the curve, among other things), we would have to write a lot of code
 | 
			
		||||
@@ -286,8 +324,8 @@ class KexNISTP256(KexDH):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KexNISTP384(KexDH):
 | 
			
		||||
    def __init__(self) -> None:
 | 
			
		||||
        super(KexNISTP384, self).__init__('KexNISTP384', 'sha256', 0, 0)
 | 
			
		||||
    def __init__(self, out: 'OutputBuffer') -> None:
 | 
			
		||||
        super(KexNISTP384, self).__init__(out, 'KexNISTP384', 'sha256', 0, 0)
 | 
			
		||||
 | 
			
		||||
    # See comment for KexNISTP256.send_init().
 | 
			
		||||
    def send_init(self, s: 'SSH_Socket', init_msg: int = Protocol.MSG_KEXDH_INIT) -> None:
 | 
			
		||||
@@ -297,8 +335,8 @@ class KexNISTP384(KexDH):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KexNISTP521(KexDH):
 | 
			
		||||
    def __init__(self) -> None:
 | 
			
		||||
        super(KexNISTP521, self).__init__('KexNISTP521', 'sha256', 0, 0)
 | 
			
		||||
    def __init__(self, out: 'OutputBuffer') -> None:
 | 
			
		||||
        super(KexNISTP521, self).__init__(out, 'KexNISTP521', 'sha256', 0, 0)
 | 
			
		||||
 | 
			
		||||
    # See comment for KexNISTP256.send_init().
 | 
			
		||||
    def send_init(self, s: 'SSH_Socket', init_msg: int = Protocol.MSG_KEXDH_INIT) -> None:
 | 
			
		||||
@@ -308,8 +346,8 @@ class KexNISTP521(KexDH):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KexGroupExchange(KexDH):
 | 
			
		||||
    def __init__(self, classname: str, hash_alg: str) -> None:
 | 
			
		||||
        super(KexGroupExchange, self).__init__(classname, hash_alg, 0, 0)
 | 
			
		||||
    def __init__(self, out: 'OutputBuffer', classname: str, hash_alg: str) -> None:
 | 
			
		||||
        super(KexGroupExchange, self).__init__(out, classname, hash_alg, 0, 0)
 | 
			
		||||
 | 
			
		||||
    def send_init(self, s: 'SSH_Socket', init_msg: int = Protocol.MSG_KEXDH_GEX_REQUEST) -> None:
 | 
			
		||||
        self.send_init_gex(s)
 | 
			
		||||
@@ -358,10 +396,10 @@ class KexGroupExchange(KexDH):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KexGroupExchange_SHA1(KexGroupExchange):
 | 
			
		||||
    def __init__(self) -> None:
 | 
			
		||||
        super(KexGroupExchange_SHA1, self).__init__('KexGroupExchange_SHA1', 'sha1')
 | 
			
		||||
    def __init__(self, out: 'OutputBuffer') -> None:
 | 
			
		||||
        super(KexGroupExchange_SHA1, self).__init__(out, 'KexGroupExchange_SHA1', 'sha1')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KexGroupExchange_SHA256(KexGroupExchange):
 | 
			
		||||
    def __init__(self) -> None:
 | 
			
		||||
        super(KexGroupExchange_SHA256, self).__init__('KexGroupExchange_SHA256', 'sha256')
 | 
			
		||||
    def __init__(self, out: 'OutputBuffer') -> None:
 | 
			
		||||
        super(KexGroupExchange_SHA256, self).__init__(out, 'KexGroupExchange_SHA256', 'sha256')
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,8 @@
 | 
			
		||||
   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 | 
			
		||||
   THE SOFTWARE.
 | 
			
		||||
"""
 | 
			
		||||
import copy
 | 
			
		||||
import json
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
from typing import Dict, List, Tuple
 | 
			
		||||
@@ -28,9 +30,9 @@ from typing import Optional, Any, Union, cast
 | 
			
		||||
from datetime import date
 | 
			
		||||
 | 
			
		||||
from ssh_audit import exitcodes
 | 
			
		||||
from ssh_audit.ssh2_kex import SSH2_Kex  # pylint: disable=unused-import
 | 
			
		||||
from ssh_audit.banner import Banner  # pylint: disable=unused-import
 | 
			
		||||
from ssh_audit.banner import Banner
 | 
			
		||||
from ssh_audit.globals import SNAP_PACKAGE, SNAP_PERMISSIONS_ERROR
 | 
			
		||||
from ssh_audit.ssh2_kex import SSH2_Kex
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Validates policy files and performs policy testing
 | 
			
		||||
@@ -87,8 +89,9 @@ class Policy:
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    WARNING_DEPRECATED_DIRECTIVES = "\nWARNING: this policy is using deprecated features.  Future versions of ssh-audit may remove support for them.  Re-generating the policy file is perhaps the most straight-forward way of resolving this issue.  Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option.\n"
 | 
			
		||||
 | 
			
		||||
    def __init__(self, policy_file: Optional[str] = None, policy_data: Optional[str] = None, manual_load: bool = False) -> None:
 | 
			
		||||
    def __init__(self, policy_file: Optional[str] = None, policy_data: Optional[str] = None, manual_load: bool = False, json_output: bool = False) -> None:
 | 
			
		||||
        self._name: Optional[str] = None
 | 
			
		||||
        self._version: Optional[str] = None
 | 
			
		||||
        self._banner: Optional[str] = None
 | 
			
		||||
@@ -98,13 +101,19 @@ class Policy:
 | 
			
		||||
        self._kex: Optional[List[str]] = None
 | 
			
		||||
        self._ciphers: Optional[List[str]] = None
 | 
			
		||||
        self._macs: Optional[List[str]] = None
 | 
			
		||||
        self._hostkey_sizes: Optional[Dict[str, int]] = None
 | 
			
		||||
        self._hostkey_sizes: Optional[Dict[str, Dict[str, Union[int, str, bytes]]]] = None
 | 
			
		||||
        self._cakey_sizes: Optional[Dict[str, int]] = None
 | 
			
		||||
        self._dh_modulus_sizes: Optional[Dict[str, int]] = None
 | 
			
		||||
        self._server_policy = True
 | 
			
		||||
 | 
			
		||||
        self._name_and_version: str = ''
 | 
			
		||||
 | 
			
		||||
        # If invoked while JSON output is expected, send warnings to stderr instead of stdout (which would corrupt the JSON output).
 | 
			
		||||
        if json_output:
 | 
			
		||||
            self._warning_target = sys.stderr
 | 
			
		||||
        else:
 | 
			
		||||
            self._warning_target = sys.stdout
 | 
			
		||||
 | 
			
		||||
        # Ensure that only one mode was specified.
 | 
			
		||||
        num_modes = 0
 | 
			
		||||
        if policy_file is not None:
 | 
			
		||||
@@ -154,7 +163,7 @@ class Policy:
 | 
			
		||||
            key = key.strip()
 | 
			
		||||
            val = val.strip()
 | 
			
		||||
 | 
			
		||||
            if key not in ['name', 'version', 'banner', 'compressions', 'host keys', 'optional host keys', 'key exchanges', 'ciphers', 'macs', 'client policy'] and not key.startswith('hostkey_size_') and not key.startswith('cakey_size_') and not key.startswith('dh_modulus_size_'):
 | 
			
		||||
            if key not in ['name', 'version', 'banner', 'compressions', 'host keys', 'optional host keys', 'key exchanges', 'ciphers', 'macs', 'client policy', 'host_key_sizes', 'dh_modulus_sizes'] and not key.startswith('hostkey_size_') and not key.startswith('cakey_size_') and not key.startswith('dh_modulus_size_'):
 | 
			
		||||
                raise ValueError("invalid field found in policy: %s" % line)
 | 
			
		||||
 | 
			
		||||
            if key in ['name', 'banner']:
 | 
			
		||||
@@ -173,8 +182,10 @@ class Policy:
 | 
			
		||||
                    self._name = val
 | 
			
		||||
                elif key == 'banner':
 | 
			
		||||
                    self._banner = val
 | 
			
		||||
 | 
			
		||||
            elif key == 'version':
 | 
			
		||||
                self._version = val
 | 
			
		||||
 | 
			
		||||
            elif key in ['compressions', 'host keys', 'optional host keys', 'key exchanges', 'ciphers', 'macs']:
 | 
			
		||||
                try:
 | 
			
		||||
                    algs = val.split(',')
 | 
			
		||||
@@ -197,21 +208,59 @@ class Policy:
 | 
			
		||||
                    self._ciphers = algs
 | 
			
		||||
                elif key == 'macs':
 | 
			
		||||
                    self._macs = algs
 | 
			
		||||
            elif key.startswith('hostkey_size_'):
 | 
			
		||||
 | 
			
		||||
            elif key.startswith('hostkey_size_'):  # Old host key size format.
 | 
			
		||||
                print(Policy.WARNING_DEPRECATED_DIRECTIVES, file=self._warning_target)  # Warn the user that the policy file is using deprecated directives.
 | 
			
		||||
 | 
			
		||||
                hostkey_type = key[13:]
 | 
			
		||||
                hostkey_size = int(val)
 | 
			
		||||
 | 
			
		||||
                if self._hostkey_sizes is None:
 | 
			
		||||
                    self._hostkey_sizes = {}
 | 
			
		||||
                self._hostkey_sizes[hostkey_type] = int(val)
 | 
			
		||||
            elif key.startswith('cakey_size_'):
 | 
			
		||||
                cakey_type = key[11:]
 | 
			
		||||
                if self._cakey_sizes is None:
 | 
			
		||||
                    self._cakey_sizes = {}
 | 
			
		||||
                self._cakey_sizes[cakey_type] = int(val)
 | 
			
		||||
            elif key.startswith('dh_modulus_size_'):
 | 
			
		||||
                dh_modulus_type = key[16:]
 | 
			
		||||
 | 
			
		||||
                self._hostkey_sizes[hostkey_type] = {'hostkey_size': hostkey_size, 'ca_key_type': '', 'ca_key_size': 0}
 | 
			
		||||
 | 
			
		||||
            elif key.startswith('cakey_size_'):  # Old host key size format.
 | 
			
		||||
                print(Policy.WARNING_DEPRECATED_DIRECTIVES, file=self._warning_target)  # Warn the user that the policy file is using deprecated directives.
 | 
			
		||||
 | 
			
		||||
                hostkey_type = key[11:]
 | 
			
		||||
                ca_key_size = int(val)
 | 
			
		||||
 | 
			
		||||
                ca_key_type = 'ssh-ed25519'
 | 
			
		||||
                if hostkey_type in ['ssh-rsa-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com']:
 | 
			
		||||
                    ca_key_type = 'ssh-rsa'
 | 
			
		||||
 | 
			
		||||
                if self._hostkey_sizes is None:
 | 
			
		||||
                    self._hostkey_sizes = {}
 | 
			
		||||
                self._hostkey_sizes[hostkey_type] = {'hostkey_size': hostkey_size, 'ca_key_type': ca_key_type, 'ca_key_size': ca_key_size}
 | 
			
		||||
 | 
			
		||||
            elif key == 'host_key_sizes':  # New host key size format.
 | 
			
		||||
                self._hostkey_sizes = json.loads(val)
 | 
			
		||||
 | 
			
		||||
                # Fill in the trimmed fields that were omitted from the policy.
 | 
			
		||||
                if self._hostkey_sizes is not None:
 | 
			
		||||
                    for host_key_type in self._hostkey_sizes:
 | 
			
		||||
                        if 'ca_key_type' not in self._hostkey_sizes[host_key_type]:
 | 
			
		||||
                            self._hostkey_sizes[host_key_type]['ca_key_type'] = ''
 | 
			
		||||
                        if 'ca_key_size' not in self._hostkey_sizes[host_key_type]:
 | 
			
		||||
                            self._hostkey_sizes[host_key_type]['ca_key_size'] = 0
 | 
			
		||||
                        if 'raw_hostkey_bytes' not in self._hostkey_sizes[host_key_type]:
 | 
			
		||||
                            self._hostkey_sizes[host_key_type]['raw_hostkey_bytes'] = b''
 | 
			
		||||
 | 
			
		||||
            elif key.startswith('dh_modulus_size_'):  # Old DH modulus format.
 | 
			
		||||
                print(Policy.WARNING_DEPRECATED_DIRECTIVES, file=self._warning_target)  # Warn the user that the policy file is using deprecated directives.
 | 
			
		||||
 | 
			
		||||
                dh_type = key[16:]
 | 
			
		||||
                dh_size = int(val)
 | 
			
		||||
 | 
			
		||||
                if self._dh_modulus_sizes is None:
 | 
			
		||||
                    self._dh_modulus_sizes = {}
 | 
			
		||||
                self._dh_modulus_sizes[dh_modulus_type] = int(val)
 | 
			
		||||
 | 
			
		||||
                self._dh_modulus_sizes[dh_type] = dh_size
 | 
			
		||||
 | 
			
		||||
            elif key == 'dh_modulus_sizes':  # New DH modulus format.
 | 
			
		||||
                self._dh_modulus_sizes = json.loads(val)
 | 
			
		||||
 | 
			
		||||
            elif key.startswith('client policy') and val.lower() == 'true':
 | 
			
		||||
                self._server_policy = False
 | 
			
		||||
 | 
			
		||||
@@ -243,10 +292,9 @@ class Policy:
 | 
			
		||||
        kex_algs = None
 | 
			
		||||
        ciphers = None
 | 
			
		||||
        macs = None
 | 
			
		||||
        rsa_hostkey_sizes_str = ''
 | 
			
		||||
        rsa_cakey_sizes_str = ''
 | 
			
		||||
        dh_modulus_sizes_str = ''
 | 
			
		||||
        client_policy_str = ''
 | 
			
		||||
        host_keys_json = ''
 | 
			
		||||
 | 
			
		||||
        if client_audit:
 | 
			
		||||
            client_policy_str = "\n# Set to true to signify this is a policy for clients, not servers.\nclient policy = true\n"
 | 
			
		||||
@@ -262,26 +310,23 @@ class Policy:
 | 
			
		||||
                ciphers = ', '.join(kex.server.encryption)
 | 
			
		||||
            if kex.server.mac is not None:
 | 
			
		||||
                macs = ', '.join(kex.server.mac)
 | 
			
		||||
            if kex.rsa_key_sizes():
 | 
			
		||||
                rsa_key_sizes_dict = kex.rsa_key_sizes()
 | 
			
		||||
                for host_key_type in sorted(rsa_key_sizes_dict):
 | 
			
		||||
                    hostkey_size, cakey_size = rsa_key_sizes_dict[host_key_type]
 | 
			
		||||
 | 
			
		||||
                    rsa_hostkey_sizes_str = "%shostkey_size_%s = %d\n" % (rsa_hostkey_sizes_str, host_key_type, hostkey_size)
 | 
			
		||||
                    if cakey_size != -1:
 | 
			
		||||
                        rsa_cakey_sizes_str = "%scakey_size_%s = %d\n" % (rsa_cakey_sizes_str, host_key_type, cakey_size)
 | 
			
		||||
            if kex.host_keys():
 | 
			
		||||
 | 
			
		||||
                # Make a deep copy of the host keys dict, then delete all the raw hostkey bytes from the copy.
 | 
			
		||||
                host_keys_trimmed = copy.deepcopy(kex.host_keys())
 | 
			
		||||
                for hostkey_alg in host_keys_trimmed:
 | 
			
		||||
                    del host_keys_trimmed[hostkey_alg]['raw_hostkey_bytes']
 | 
			
		||||
 | 
			
		||||
                    # Delete the CA signature if any of its fields are empty.
 | 
			
		||||
                    if host_keys_trimmed[hostkey_alg]['ca_key_type'] == '' or host_keys_trimmed[hostkey_alg]['ca_key_size'] == 0:
 | 
			
		||||
                        del host_keys_trimmed[hostkey_alg]['ca_key_type']
 | 
			
		||||
                        del host_keys_trimmed[hostkey_alg]['ca_key_size']
 | 
			
		||||
 | 
			
		||||
                host_keys_json = "\n# Dictionary containing all host key and size information.  Optionally contains the certificate authority's signature algorithm ('ca_key_type') and signature length ('ca_key_size'), if any.\nhost_key_sizes = %s\n" % json.dumps(host_keys_trimmed)
 | 
			
		||||
 | 
			
		||||
                if len(rsa_hostkey_sizes_str) > 0:
 | 
			
		||||
                    rsa_hostkey_sizes_str = "\n# RSA host key sizes.\n%s" % rsa_hostkey_sizes_str
 | 
			
		||||
                if len(rsa_cakey_sizes_str) > 0:
 | 
			
		||||
                    rsa_cakey_sizes_str = "\n# RSA CA key sizes.\n%s" % rsa_cakey_sizes_str
 | 
			
		||||
            if kex.dh_modulus_sizes():
 | 
			
		||||
                dh_modulus_sizes_dict = kex.dh_modulus_sizes()
 | 
			
		||||
                for gex_type in sorted(dh_modulus_sizes_dict):
 | 
			
		||||
                    modulus_size, _ = dh_modulus_sizes_dict[gex_type]
 | 
			
		||||
                    dh_modulus_sizes_str = "%sdh_modulus_size_%s = %d\n" % (dh_modulus_sizes_str, gex_type, modulus_size)
 | 
			
		||||
                if len(dh_modulus_sizes_str) > 0:
 | 
			
		||||
                    dh_modulus_sizes_str = "\n# Group exchange DH modulus sizes.\n%s" % dh_modulus_sizes_str
 | 
			
		||||
                dh_modulus_sizes_str = "\n# Group exchange DH modulus sizes.\ndh_modulus_sizes = %s\n" % json.dumps(kex.dh_modulus_sizes())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        policy_data = '''#
 | 
			
		||||
@@ -299,7 +344,7 @@ version = 1
 | 
			
		||||
 | 
			
		||||
# The compression options that must match exactly (order matters).  Commented out to ignore by default.
 | 
			
		||||
# compressions = %s
 | 
			
		||||
%s%s%s
 | 
			
		||||
%s%s
 | 
			
		||||
# The host key types that must match exactly (order matters).
 | 
			
		||||
host keys = %s
 | 
			
		||||
 | 
			
		||||
@@ -314,7 +359,7 @@ ciphers = %s
 | 
			
		||||
 | 
			
		||||
# The MACs that must match exactly (order matters).
 | 
			
		||||
macs = %s
 | 
			
		||||
''' % (source, today, client_policy_str, source, today, banner, compressions, rsa_hostkey_sizes_str, rsa_cakey_sizes_str, dh_modulus_sizes_str, host_keys, kex_algs, ciphers, macs)
 | 
			
		||||
''' % (source, today, client_policy_str, source, today, banner, compressions, host_keys_json, dh_modulus_sizes_str, host_keys, kex_algs, ciphers, macs)
 | 
			
		||||
 | 
			
		||||
        return policy_data
 | 
			
		||||
 | 
			
		||||
@@ -351,23 +396,29 @@ macs = %s
 | 
			
		||||
            hostkey_types = list(self._hostkey_sizes.keys())
 | 
			
		||||
            hostkey_types.sort()  # Sorted to make testing output repeatable.
 | 
			
		||||
            for hostkey_type in hostkey_types:
 | 
			
		||||
                expected_hostkey_size = self._hostkey_sizes[hostkey_type]
 | 
			
		||||
                if hostkey_type in kex.rsa_key_sizes():
 | 
			
		||||
                    actual_hostkey_size, actual_cakey_size = kex.rsa_key_sizes()[hostkey_type]
 | 
			
		||||
                expected_hostkey_size = self._hostkey_sizes[hostkey_type]['hostkey_size']
 | 
			
		||||
                server_host_keys = kex.host_keys()
 | 
			
		||||
                if hostkey_type in server_host_keys:
 | 
			
		||||
                    actual_hostkey_size = server_host_keys[hostkey_type]['hostkey_size']
 | 
			
		||||
                    if actual_hostkey_size != expected_hostkey_size:
 | 
			
		||||
                        ret = False
 | 
			
		||||
                        self._append_error(errors, 'RSA host key (%s) sizes' % hostkey_type, [str(expected_hostkey_size)], None, [str(actual_hostkey_size)])
 | 
			
		||||
                        self._append_error(errors, 'Host key (%s) sizes' % hostkey_type, [str(expected_hostkey_size)], None, [str(actual_hostkey_size)])
 | 
			
		||||
 | 
			
		||||
        if self._cakey_sizes is not None:
 | 
			
		||||
            hostkey_types = list(self._cakey_sizes.keys())
 | 
			
		||||
            hostkey_types.sort()  # Sorted to make testing output repeatable.
 | 
			
		||||
            for hostkey_type in hostkey_types:
 | 
			
		||||
                expected_cakey_size = self._cakey_sizes[hostkey_type]
 | 
			
		||||
                if hostkey_type in kex.rsa_key_sizes():
 | 
			
		||||
                    actual_hostkey_size, actual_cakey_size = kex.rsa_key_sizes()[hostkey_type]
 | 
			
		||||
                    if actual_cakey_size != expected_cakey_size:
 | 
			
		||||
                        ret = False
 | 
			
		||||
                        self._append_error(errors, 'RSA CA key (%s) sizes' % hostkey_type, [str(expected_cakey_size)], None, [str(actual_cakey_size)])
 | 
			
		||||
                    # If we have expected CA signatures set, check them against what the server returned.
 | 
			
		||||
                    if self._hostkey_sizes is not None and len(cast(str, self._hostkey_sizes[hostkey_type]['ca_key_type'])) > 0 and cast(int, self._hostkey_sizes[hostkey_type]['ca_key_size']) > 0:
 | 
			
		||||
                        expected_ca_key_type = cast(str, self._hostkey_sizes[hostkey_type]['ca_key_type'])
 | 
			
		||||
                        expected_ca_key_size = cast(int, self._hostkey_sizes[hostkey_type]['ca_key_size'])
 | 
			
		||||
                        actual_ca_key_type = cast(str, server_host_keys[hostkey_type]['ca_key_type'])
 | 
			
		||||
                        actual_ca_key_size = cast(int, server_host_keys[hostkey_type]['ca_key_size'])
 | 
			
		||||
 | 
			
		||||
                        # Ensure that the CA signature type is what's expected (i.e.: the server doesn't have an RSA sig when we're expecting an ED25519 sig).
 | 
			
		||||
                        if actual_ca_key_type != expected_ca_key_type:
 | 
			
		||||
                            ret = False
 | 
			
		||||
                            self._append_error(errors, 'CA signature type', [expected_ca_key_type], None, [actual_ca_key_type])
 | 
			
		||||
                        # Ensure that the actual and expected signature sizes match.
 | 
			
		||||
                        elif actual_ca_key_size != expected_ca_key_size:
 | 
			
		||||
                            ret = False
 | 
			
		||||
                            self._append_error(errors, 'CA signature size (%s)' % actual_ca_key_type, [str(expected_ca_key_size)], None, [str(actual_ca_key_size)])
 | 
			
		||||
 | 
			
		||||
        if kex.kex_algorithms != self._kex:
 | 
			
		||||
            ret = False
 | 
			
		||||
@@ -387,7 +438,7 @@ macs = %s
 | 
			
		||||
            for dh_modulus_type in dh_modulus_types:
 | 
			
		||||
                expected_dh_modulus_size = self._dh_modulus_sizes[dh_modulus_type]
 | 
			
		||||
                if dh_modulus_type in kex.dh_modulus_sizes():
 | 
			
		||||
                    actual_dh_modulus_size, _ = kex.dh_modulus_sizes()[dh_modulus_type]
 | 
			
		||||
                    actual_dh_modulus_size = kex.dh_modulus_sizes()[dh_modulus_type]
 | 
			
		||||
                    if expected_dh_modulus_size != actual_dh_modulus_size:
 | 
			
		||||
                        ret = False
 | 
			
		||||
                        self._append_error(errors, 'Group exchange (%s) modulus sizes' % dh_modulus_type, [str(expected_dh_modulus_size)], None, [str(actual_dh_modulus_size)])
 | 
			
		||||
@@ -449,12 +500,12 @@ macs = %s
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def load_builtin_policy(policy_name: str) -> Optional['Policy']:
 | 
			
		||||
    def load_builtin_policy(policy_name: str, json_output: bool = False) -> Optional['Policy']:
 | 
			
		||||
        '''Returns a Policy with the specified built-in policy name loaded, or None if no policy of that name exists.'''
 | 
			
		||||
        p = None
 | 
			
		||||
        if policy_name in Policy.BUILTIN_POLICIES:
 | 
			
		||||
            policy_struct = Policy.BUILTIN_POLICIES[policy_name]
 | 
			
		||||
            p = Policy(manual_load=True)
 | 
			
		||||
            p = Policy(manual_load=True, json_output=json_output)
 | 
			
		||||
            policy_name_without_version = policy_name[0:policy_name.rfind(' (')]
 | 
			
		||||
            p._name = policy_name_without_version  # pylint: disable=protected-access
 | 
			
		||||
            p._version = cast(str, policy_struct['version'])  # pylint: disable=protected-access
 | 
			
		||||
@@ -465,7 +516,7 @@ macs = %s
 | 
			
		||||
            p._kex = cast(Optional[List[str]], policy_struct['kex'])  # pylint: disable=protected-access
 | 
			
		||||
            p._ciphers = cast(Optional[List[str]], policy_struct['ciphers'])  # pylint: disable=protected-access
 | 
			
		||||
            p._macs = cast(Optional[List[str]], policy_struct['macs'])  # pylint: disable=protected-access
 | 
			
		||||
            p._hostkey_sizes = cast(Optional[Dict[str, int]], policy_struct['hostkey_sizes'])  # pylint: disable=protected-access
 | 
			
		||||
            p._hostkey_sizes = cast(Optional[Dict[str, Dict[str, Union[int, str, bytes]]]], policy_struct['hostkey_sizes'])  # pylint: disable=protected-access
 | 
			
		||||
            p._cakey_sizes = cast(Optional[Dict[str, int]], policy_struct['cakey_sizes'])  # pylint: disable=protected-access
 | 
			
		||||
            p._dh_modulus_sizes = cast(Optional[Dict[str, int]], policy_struct['dh_modulus_sizes'])  # pylint: disable=protected-access
 | 
			
		||||
            p._server_policy = cast(bool, policy_struct['server_policy'])  # pylint: disable=protected-access
 | 
			
		||||
 
 | 
			
		||||
@@ -22,17 +22,18 @@
 | 
			
		||||
   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 | 
			
		||||
   THE SOFTWARE.
 | 
			
		||||
"""
 | 
			
		||||
# pylint: disable=unused-import
 | 
			
		||||
from typing import Dict, List, Set, Sequence, Tuple, Iterable  # noqa: F401
 | 
			
		||||
from typing import Callable, Optional, Union, Any  # noqa: F401
 | 
			
		||||
from typing import Dict, List
 | 
			
		||||
from typing import Union
 | 
			
		||||
 | 
			
		||||
from ssh_audit.ssh2_kexparty import SSH2_KexParty
 | 
			
		||||
from ssh_audit.outputbuffer import OutputBuffer
 | 
			
		||||
from ssh_audit.readbuf import ReadBuf
 | 
			
		||||
from ssh_audit.ssh2_kexparty import SSH2_KexParty
 | 
			
		||||
from ssh_audit.writebuf import WriteBuf
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SSH2_Kex:
 | 
			
		||||
    def __init__(self, cookie: bytes, kex_algs: List[str], key_algs: List[str], cli: 'SSH2_KexParty', srv: 'SSH2_KexParty', follows: bool, unused: int = 0) -> None:
 | 
			
		||||
    def __init__(self, outputbuffer: 'OutputBuffer', cookie: bytes, kex_algs: List[str], key_algs: List[str], cli: 'SSH2_KexParty', srv: 'SSH2_KexParty', follows: bool, unused: int = 0) -> None:  # pylint: disable=too-many-arguments
 | 
			
		||||
        self.__outputbuffer = outputbuffer
 | 
			
		||||
        self.__cookie = cookie
 | 
			
		||||
        self.__kex_algs = kex_algs
 | 
			
		||||
        self.__key_algs = key_algs
 | 
			
		||||
@@ -41,9 +42,8 @@ class SSH2_Kex:
 | 
			
		||||
        self.__follows = follows
 | 
			
		||||
        self.__unused = unused
 | 
			
		||||
 | 
			
		||||
        self.__rsa_key_sizes: Dict[str, Tuple[int, int]] = {}
 | 
			
		||||
        self.__dh_modulus_sizes: Dict[str, Tuple[int, int]] = {}
 | 
			
		||||
        self.__host_keys: Dict[str, bytes] = {}
 | 
			
		||||
        self.__dh_modulus_sizes: Dict[str, int] = {}
 | 
			
		||||
        self.__host_keys: Dict[str, Dict[str, Union[bytes, str, int]]] = {}
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def cookie(self) -> bytes:
 | 
			
		||||
@@ -75,22 +75,20 @@ class SSH2_Kex:
 | 
			
		||||
    def unused(self) -> int:
 | 
			
		||||
        return self.__unused
 | 
			
		||||
 | 
			
		||||
    def set_rsa_key_size(self, rsa_type: str, hostkey_size: int, ca_size: int = -1) -> None:
 | 
			
		||||
        self.__rsa_key_sizes[rsa_type] = (hostkey_size, ca_size)
 | 
			
		||||
 | 
			
		||||
    def rsa_key_sizes(self) -> Dict[str, Tuple[int, int]]:
 | 
			
		||||
        return self.__rsa_key_sizes
 | 
			
		||||
 | 
			
		||||
    def set_dh_modulus_size(self, gex_alg: str, modulus_size: int) -> None:
 | 
			
		||||
        self.__dh_modulus_sizes[gex_alg] = (modulus_size, -1)
 | 
			
		||||
        self.__dh_modulus_sizes[gex_alg] = modulus_size
 | 
			
		||||
 | 
			
		||||
    def dh_modulus_sizes(self) -> Dict[str, Tuple[int, int]]:
 | 
			
		||||
    def dh_modulus_sizes(self) -> Dict[str, int]:
 | 
			
		||||
        return self.__dh_modulus_sizes
 | 
			
		||||
 | 
			
		||||
    def set_host_key(self, key_type: str, hostkey: bytes) -> None:
 | 
			
		||||
        self.__host_keys[key_type] = hostkey
 | 
			
		||||
    def set_host_key(self, key_type: str, raw_hostkey_bytes: bytes, hostkey_size: int, ca_key_type: str, ca_key_size: int) -> None:
 | 
			
		||||
 | 
			
		||||
    def host_keys(self) -> Dict[str, bytes]:
 | 
			
		||||
        if key_type not in self.__host_keys:
 | 
			
		||||
            self.__host_keys[key_type] = {'raw_hostkey_bytes': raw_hostkey_bytes, 'hostkey_size': hostkey_size, 'ca_key_type': ca_key_type, 'ca_key_size': ca_key_size}
 | 
			
		||||
        else:  # A host key may only have one CA signature...
 | 
			
		||||
            self.__outputbuffer.d("WARNING: called SSH2_Kex.set_host_key() multiple times with the same host key type (%s)!  Existing info: %r, %r, %r; Duplicate (ignored) info: %r, %r, %r" % (key_type, self.__host_keys[key_type]['hostkey_size'], self.__host_keys[key_type]['ca_key_type'], self.__host_keys[key_type]['ca_key_size'], hostkey_size, ca_key_type, ca_key_size))
 | 
			
		||||
 | 
			
		||||
    def host_keys(self) -> Dict[str, Dict[str, Union[bytes, str, int]]]:
 | 
			
		||||
        return self.__host_keys
 | 
			
		||||
 | 
			
		||||
    def write(self, wbuf: 'WriteBuf') -> None:
 | 
			
		||||
@@ -115,7 +113,7 @@ class SSH2_Kex:
 | 
			
		||||
        return wbuf.write_flush()
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def parse(cls, payload: bytes) -> 'SSH2_Kex':
 | 
			
		||||
    def parse(cls, outputbuffer: 'OutputBuffer', payload: bytes) -> 'SSH2_Kex':
 | 
			
		||||
        buf = ReadBuf(payload)
 | 
			
		||||
        cookie = buf.read(16)
 | 
			
		||||
        kex_algs = buf.read_list()
 | 
			
		||||
@@ -132,5 +130,5 @@ class SSH2_Kex:
 | 
			
		||||
        unused = buf.read_int()
 | 
			
		||||
        cli = SSH2_KexParty(cli_enc, cli_mac, cli_compression, cli_languages)
 | 
			
		||||
        srv = SSH2_KexParty(srv_enc, srv_mac, srv_compression, srv_languages)
 | 
			
		||||
        kex = cls(cookie, kex_algs, key_algs, cli, srv, follows, unused)
 | 
			
		||||
        kex = cls(outputbuffer, cookie, kex_algs, key_algs, cli, srv, follows, unused)
 | 
			
		||||
        return kex
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@ import traceback
 | 
			
		||||
 | 
			
		||||
# pylint: disable=unused-import
 | 
			
		||||
from typing import Dict, List, Set, Sequence, Tuple, Iterable  # noqa: F401
 | 
			
		||||
from typing import Callable, Optional, Union, Any  # noqa: F401
 | 
			
		||||
from typing import cast, Callable, Optional, Union, Any  # noqa: F401
 | 
			
		||||
 | 
			
		||||
from ssh_audit.globals import SNAP_PACKAGE
 | 
			
		||||
from ssh_audit.globals import SNAP_PERMISSIONS_ERROR
 | 
			
		||||
@@ -107,10 +107,10 @@ def usage(uout: OutputBuffer, err: Optional[str] = None) -> None:
 | 
			
		||||
    sys.exit(retval)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def output_algorithms(out: OutputBuffer, title: str, alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], alg_type: str, algorithms: List[str], unknown_algs: List[str], is_json_output: bool, program_retval: int, maxlen: int = 0, alg_sizes: Optional[Dict[str, Tuple[int, int]]] = None) -> int:  # pylint: disable=too-many-arguments
 | 
			
		||||
def output_algorithms(out: OutputBuffer, title: str, alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], alg_type: str, algorithms: List[str], unknown_algs: List[str], is_json_output: bool, program_retval: int, maxlen: int = 0, host_keys: Optional[Dict[str, Dict[str, Union[bytes, str, int]]]] = None, dh_modulus_sizes: Optional[Dict[str, int]] = None) -> int:  # pylint: disable=too-many-arguments
 | 
			
		||||
    with out:
 | 
			
		||||
        for algorithm in algorithms:
 | 
			
		||||
            program_retval = output_algorithm(out, alg_db, alg_type, algorithm, unknown_algs, program_retval, maxlen, alg_sizes)
 | 
			
		||||
            program_retval = output_algorithm(out, alg_db, alg_type, algorithm, unknown_algs, program_retval, maxlen, host_keys=host_keys, dh_modulus_sizes=dh_modulus_sizes)
 | 
			
		||||
    if not out.is_section_empty() and not is_json_output:
 | 
			
		||||
        out.head('# ' + title)
 | 
			
		||||
        out.flush_section()
 | 
			
		||||
@@ -119,7 +119,7 @@ def output_algorithms(out: OutputBuffer, title: str, alg_db: Dict[str, Dict[str,
 | 
			
		||||
    return program_retval
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def output_algorithm(out: OutputBuffer, alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], alg_type: str, alg_name: str, unknown_algs: List[str], program_retval: int, alg_max_len: int = 0, alg_sizes: Optional[Dict[str, Tuple[int, int]]] = None) -> int:
 | 
			
		||||
def output_algorithm(out: OutputBuffer, alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], alg_type: str, alg_name: str, unknown_algs: List[str], program_retval: int, alg_max_len: int = 0, host_keys: Optional[Dict[str, Dict[str, Union[bytes, str, int]]]] = None, dh_modulus_sizes: Optional[Dict[str, int]] = None) -> int:  # pylint: disable=too-many-arguments
 | 
			
		||||
    prefix = '(' + alg_type + ') '
 | 
			
		||||
    if alg_max_len == 0:
 | 
			
		||||
        alg_max_len = len(alg_name)
 | 
			
		||||
@@ -128,13 +128,23 @@ def output_algorithm(out: OutputBuffer, alg_db: Dict[str, Dict[str, List[List[Op
 | 
			
		||||
    # If this is an RSA host key or DH GEX, append the size to its name and fix
 | 
			
		||||
    # the padding.
 | 
			
		||||
    alg_name_with_size = None
 | 
			
		||||
    if (alg_sizes is not None) and (alg_name in alg_sizes):
 | 
			
		||||
        hostkey_size, ca_size = alg_sizes[alg_name]
 | 
			
		||||
        if ca_size > 0:
 | 
			
		||||
            alg_name_with_size = '%s (%d-bit cert/%d-bit CA)' % (alg_name, hostkey_size, ca_size)
 | 
			
		||||
    if (dh_modulus_sizes is not None) and (alg_name in dh_modulus_sizes):
 | 
			
		||||
        alg_name_with_size = '%s (%u-bit)' % (alg_name, dh_modulus_sizes[alg_name])
 | 
			
		||||
        padding = padding[0:-11]
 | 
			
		||||
    elif (host_keys is not None) and (alg_name in host_keys):
 | 
			
		||||
        hostkey_size = cast(int, host_keys[alg_name]['hostkey_size'])
 | 
			
		||||
        ca_key_type = cast(str, host_keys[alg_name]['ca_key_type'])
 | 
			
		||||
        ca_key_size = cast(int, host_keys[alg_name]['ca_key_size'])
 | 
			
		||||
 | 
			
		||||
        # If this is an RSA variant, just print "RSA".
 | 
			
		||||
        if ca_key_type in HostKeyTest.RSA_FAMILY:
 | 
			
		||||
            ca_key_type = "RSA"
 | 
			
		||||
 | 
			
		||||
        if len(ca_key_type) > 0 and ca_key_size > 0:
 | 
			
		||||
            alg_name_with_size = '%s (%u-bit cert/%u-bit %s CA)' % (alg_name, hostkey_size, ca_key_size, ca_key_type)
 | 
			
		||||
            padding = padding[0:-15]
 | 
			
		||||
        else:
 | 
			
		||||
            alg_name_with_size = '%s (%d-bit)' % (alg_name, hostkey_size)
 | 
			
		||||
        elif alg_name in HostKeyTest.RSA_FAMILY:
 | 
			
		||||
            alg_name_with_size = '%s (%u-bit)' % (alg_name, hostkey_size)
 | 
			
		||||
            padding = padding[0:-11]
 | 
			
		||||
 | 
			
		||||
    # If this is a kex algorithm and starts with 'gss-', then normalize its name (i.e.: 'gss-gex-sha1-vz8J1E9PzLr8b1K+0remTg==' => 'gss-gex-sha1-*').  The base64 field can vary, so we'll convert it to the wildcard that our database uses and we'll just resume doing a straight match like all other algorithm names.
 | 
			
		||||
@@ -289,36 +299,36 @@ def output_security(out: OutputBuffer, banner: Optional[Banner], client_audit: b
 | 
			
		||||
 | 
			
		||||
def output_fingerprints(out: OutputBuffer, algs: Algorithms, is_json_output: bool) -> None:
 | 
			
		||||
    with out:
 | 
			
		||||
        fps = []
 | 
			
		||||
        fps = {}
 | 
			
		||||
        if algs.ssh1kex is not None:
 | 
			
		||||
            name = 'ssh-rsa1'
 | 
			
		||||
            fp = Fingerprint(algs.ssh1kex.host_key_fingerprint_data)
 | 
			
		||||
            # bits = algs.ssh1kex.host_key_bits
 | 
			
		||||
            fps.append((name, fp))
 | 
			
		||||
            fps[name] = fp
 | 
			
		||||
        if algs.ssh2kex is not None:
 | 
			
		||||
            host_keys = algs.ssh2kex.host_keys()
 | 
			
		||||
            for host_key_type in algs.ssh2kex.host_keys():
 | 
			
		||||
                if host_keys[host_key_type] is None:
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                fp = Fingerprint(host_keys[host_key_type])
 | 
			
		||||
                fp = Fingerprint(cast(bytes, host_keys[host_key_type]['raw_hostkey_bytes']))
 | 
			
		||||
 | 
			
		||||
                # Workaround for Python's order-indifference in dicts.  We might get a random RSA type (ssh-rsa, rsa-sha2-256, or rsa-sha2-512), so running the tool against the same server three times may give three different host key types here.  So if we have any RSA type, we will simply hard-code it to 'ssh-rsa'.
 | 
			
		||||
                if host_key_type in HostKeyTest.RSA_FAMILY:
 | 
			
		||||
                    host_key_type = 'ssh-rsa'
 | 
			
		||||
 | 
			
		||||
                # Skip over certificate host types (or we would return invalid fingerprints).
 | 
			
		||||
                # Skip over certificate host types (or we would return invalid fingerprints), and only add one fingerprint in the RSA family.
 | 
			
		||||
                if '-cert-' not in host_key_type:
 | 
			
		||||
                    fps.append((host_key_type, fp))
 | 
			
		||||
                    fps[host_key_type] = fp
 | 
			
		||||
        # Similarly, the host keys can be processed in random order due to Python's order-indifference in dicts.  So we sort this list before printing; this makes automated testing possible.
 | 
			
		||||
        fps = sorted(fps)
 | 
			
		||||
        for fpp in fps:
 | 
			
		||||
            name, fp = fpp
 | 
			
		||||
            out.good('(fin) {}: {}'.format(name, fp.sha256))
 | 
			
		||||
        fp_types = sorted(fps.keys())
 | 
			
		||||
        for fp_type in fp_types:
 | 
			
		||||
            fp = fps[fp_type]
 | 
			
		||||
            out.good('(fin) {}: {}'.format(fp_type, fp.sha256))
 | 
			
		||||
 | 
			
		||||
            # Output the MD5 hash too if verbose mode is enabled.
 | 
			
		||||
            if out.verbose:
 | 
			
		||||
                out.info('(fin) {}: {} -- [info] do not rely on MD5 fingerprints for server identification; it is insecure for this use case'.format(name, fp.md5))
 | 
			
		||||
                out.info('(fin) {}: {} -- [info] do not rely on MD5 fingerprints for server identification; it is insecure for this use case'.format(fp_type, fp.md5))
 | 
			
		||||
 | 
			
		||||
    if not out.is_section_empty() and not is_json_output:
 | 
			
		||||
        out.head('# fingerprints')
 | 
			
		||||
@@ -422,7 +432,7 @@ def post_process_findings(banner: Optional[Banner], algs: Algorithms) -> List[st
 | 
			
		||||
    algorithm_recommendation_suppress_list = []
 | 
			
		||||
 | 
			
		||||
    # If the server is OpenSSH, and the diffie-hellman-group-exchange-sha256 key exchange was found with modulus size 2048, add a note regarding the bug that causes the server to support 2048-bit moduli no matter the configuration.
 | 
			
		||||
    if (algs.ssh2kex is not None and 'diffie-hellman-group-exchange-sha256' in algs.ssh2kex.kex_algorithms and 'diffie-hellman-group-exchange-sha256' in algs.ssh2kex.dh_modulus_sizes() and algs.ssh2kex.dh_modulus_sizes()['diffie-hellman-group-exchange-sha256'][0] == 2048) and (banner is not None and banner.software is not None and banner.software.find('OpenSSH') != -1):
 | 
			
		||||
    if (algs.ssh2kex is not None and 'diffie-hellman-group-exchange-sha256' in algs.ssh2kex.kex_algorithms and 'diffie-hellman-group-exchange-sha256' in algs.ssh2kex.dh_modulus_sizes() and algs.ssh2kex.dh_modulus_sizes()['diffie-hellman-group-exchange-sha256'] == 2048) and (banner is not None and banner.software is not None and banner.software.find('OpenSSH') != -1):
 | 
			
		||||
 | 
			
		||||
        # Ensure a list for notes exists.
 | 
			
		||||
        while len(SSH2_KexDB.ALGORITHMS['kex']['diffie-hellman-group-exchange-sha256']) < 4:
 | 
			
		||||
@@ -498,6 +508,8 @@ def output(out: OutputBuffer, aconf: AuditConf, banner: Optional[Banner], header
 | 
			
		||||
    cves = output_security(out, banner, client_audit, maxlen, aconf.json)
 | 
			
		||||
    # Filled in by output_algorithms() with unidentified algs.
 | 
			
		||||
    unknown_algorithms: List[str] = []
 | 
			
		||||
 | 
			
		||||
    # SSHv1
 | 
			
		||||
    if pkm is not None:
 | 
			
		||||
        adb = SSH1_KexDB.ALGORITHMS
 | 
			
		||||
        ciphers = pkm.supported_ciphers
 | 
			
		||||
@@ -508,16 +520,19 @@ def output(out: OutputBuffer, aconf: AuditConf, banner: Optional[Banner], header
 | 
			
		||||
        program_retval = output_algorithms(out, title, adb, atype, ciphers, unknown_algorithms, aconf.json, program_retval, maxlen)
 | 
			
		||||
        title, atype = 'SSH1 authentication types', 'aut'
 | 
			
		||||
        program_retval = output_algorithms(out, title, adb, atype, auths, unknown_algorithms, aconf.json, program_retval, maxlen)
 | 
			
		||||
 | 
			
		||||
    # SSHv2
 | 
			
		||||
    if kex is not None:
 | 
			
		||||
        adb = SSH2_KexDB.ALGORITHMS
 | 
			
		||||
        title, atype = 'key exchange algorithms', 'kex'
 | 
			
		||||
        program_retval = output_algorithms(out, title, adb, atype, kex.kex_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, kex.dh_modulus_sizes())
 | 
			
		||||
        program_retval = output_algorithms(out, title, adb, atype, kex.kex_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, dh_modulus_sizes=kex.dh_modulus_sizes())
 | 
			
		||||
        title, atype = 'host-key algorithms', 'key'
 | 
			
		||||
        program_retval = output_algorithms(out, title, adb, atype, kex.key_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, kex.rsa_key_sizes())
 | 
			
		||||
        program_retval = output_algorithms(out, title, adb, atype, kex.key_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, host_keys=kex.host_keys())
 | 
			
		||||
        title, atype = 'encryption algorithms (ciphers)', 'enc'
 | 
			
		||||
        program_retval = output_algorithms(out, title, adb, atype, kex.server.encryption, unknown_algorithms, aconf.json, program_retval, maxlen)
 | 
			
		||||
        title, atype = 'message authentication code algorithms', 'mac'
 | 
			
		||||
        program_retval = output_algorithms(out, title, adb, atype, kex.server.mac, unknown_algorithms, aconf.json, program_retval, maxlen)
 | 
			
		||||
 | 
			
		||||
    output_fingerprints(out, algs, aconf.json)
 | 
			
		||||
    perfect_config = output_recommendations(out, algs, algorithm_recommendation_suppress_list, software, aconf.json, maxlen)
 | 
			
		||||
    output_info(out, software, client_audit, not perfect_config, aconf.json)
 | 
			
		||||
@@ -830,10 +845,10 @@ def process_commandline(out: OutputBuffer, args: List[str], usage_cb: Callable[.
 | 
			
		||||
    if (aconf.policy_file is not None) and (aconf.make_policy is False):
 | 
			
		||||
 | 
			
		||||
        # First, see if this is a built-in policy name.  If not, assume a file path was provided, and try to load it from disk.
 | 
			
		||||
        aconf.policy = Policy.load_builtin_policy(aconf.policy_file)
 | 
			
		||||
        aconf.policy = Policy.load_builtin_policy(aconf.policy_file, json_output=aconf.json)
 | 
			
		||||
        if aconf.policy is None:
 | 
			
		||||
            try:
 | 
			
		||||
                aconf.policy = Policy(policy_file=aconf.policy_file)
 | 
			
		||||
                aconf.policy = Policy(policy_file=aconf.policy_file, json_output=aconf.json)
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                out.fail("Error while loading policy file: %s: %s" % (str(e), traceback.format_exc()))
 | 
			
		||||
                out.write()
 | 
			
		||||
@@ -885,28 +900,37 @@ def build_struct(target_host: str, banner: Optional['Banner'], cves: List[Dict[s
 | 
			
		||||
        res['compression'] = kex.server.compression
 | 
			
		||||
 | 
			
		||||
        res['kex'] = []
 | 
			
		||||
        alg_sizes = kex.dh_modulus_sizes()
 | 
			
		||||
        dh_alg_sizes = kex.dh_modulus_sizes()
 | 
			
		||||
        for algorithm in kex.kex_algorithms:
 | 
			
		||||
            entry: Any = {
 | 
			
		||||
                'algorithm': algorithm,
 | 
			
		||||
            }
 | 
			
		||||
            if algorithm in alg_sizes:
 | 
			
		||||
                hostkey_size, ca_size = alg_sizes[algorithm]
 | 
			
		||||
            if algorithm in dh_alg_sizes:
 | 
			
		||||
                hostkey_size = dh_alg_sizes[algorithm]
 | 
			
		||||
                entry['keysize'] = hostkey_size
 | 
			
		||||
                if ca_size > 0:
 | 
			
		||||
                    entry['casize'] = ca_size
 | 
			
		||||
            res['kex'].append(entry)
 | 
			
		||||
 | 
			
		||||
        res['key'] = []
 | 
			
		||||
        alg_sizes = kex.rsa_key_sizes()
 | 
			
		||||
        host_keys = kex.host_keys()
 | 
			
		||||
        for algorithm in kex.key_algorithms:
 | 
			
		||||
            entry = {
 | 
			
		||||
                'algorithm': algorithm,
 | 
			
		||||
            }
 | 
			
		||||
            if algorithm in alg_sizes:
 | 
			
		||||
                hostkey_size, ca_size = alg_sizes[algorithm]
 | 
			
		||||
                entry['keysize'] = hostkey_size
 | 
			
		||||
            if algorithm in host_keys:
 | 
			
		||||
                hostkey_info = host_keys[algorithm]
 | 
			
		||||
                hostkey_size = cast(int, hostkey_info['hostkey_size'])
 | 
			
		||||
 | 
			
		||||
                ca_type = ''
 | 
			
		||||
                ca_size = 0
 | 
			
		||||
                if 'ca_key_type' in hostkey_info:
 | 
			
		||||
                    ca_type = cast(str, hostkey_info['ca_key_type'])
 | 
			
		||||
                if 'ca_key_size' in hostkey_info:
 | 
			
		||||
                    ca_size = cast(int, hostkey_info['ca_key_size'])
 | 
			
		||||
 | 
			
		||||
                if algorithm in HostKeyTest.RSA_FAMILY or algorithm.startswith('ssh-rsa-cert-v0'):
 | 
			
		||||
                    entry['keysize'] = hostkey_size
 | 
			
		||||
                if ca_size > 0:
 | 
			
		||||
                    entry['ca_algorithm'] = ca_type
 | 
			
		||||
                    entry['casize'] = ca_size
 | 
			
		||||
            res['key'].append(entry)
 | 
			
		||||
 | 
			
		||||
@@ -926,7 +950,7 @@ def build_struct(target_host: str, banner: Optional['Banner'], cves: List[Dict[s
 | 
			
		||||
            if host_keys[host_key_type] is None:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            fp = Fingerprint(host_keys[host_key_type])
 | 
			
		||||
            fp = Fingerprint(cast(bytes, host_keys[host_key_type]['raw_hostkey_bytes']))
 | 
			
		||||
 | 
			
		||||
            # Skip over certificate host types (or we would return invalid fingerprints).
 | 
			
		||||
            if '-cert-' in host_key_type:
 | 
			
		||||
@@ -1041,7 +1065,7 @@ def audit(out: OutputBuffer, aconf: AuditConf, sshv: Optional[int] = None, print
 | 
			
		||||
        program_retval = output(out, aconf, banner, header, pkm=SSH1_PublicKeyMessage.parse(payload))
 | 
			
		||||
    elif sshv == 2:
 | 
			
		||||
        try:
 | 
			
		||||
            kex = SSH2_Kex.parse(payload)
 | 
			
		||||
            kex = SSH2_Kex.parse(out, payload)
 | 
			
		||||
        except Exception:
 | 
			
		||||
            out.fail("Failed to parse server's kex.  Stack trace:\n%s" % str(traceback.format_exc()))
 | 
			
		||||
            return exitcodes.CONNECTION_ERROR
 | 
			
		||||
 
 | 
			
		||||
@@ -236,7 +236,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
 | 
			
		||||
        self.__outputbuffer.d('KEX initialisation...', write_now=True)
 | 
			
		||||
 | 
			
		||||
        kexparty = SSH2_KexParty(ciphers, macs, compressions, languages)
 | 
			
		||||
        kex = SSH2_Kex(os.urandom(16), key_exchanges, hostkeys, kexparty, kexparty, False, 0)
 | 
			
		||||
        kex = SSH2_Kex(self.__outputbuffer, os.urandom(16), key_exchanges, hostkeys, kexparty, kexparty, False, 0)
 | 
			
		||||
 | 
			
		||||
        self.write_byte(Protocol.MSG_KEXINIT)
 | 
			
		||||
        kex.write(self)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user