Added support for mixed host key/CA key types (i.e.: RSA host keys signed by ED25519 CAs) (#120).

This commit is contained in:
Joe Testa 2023-04-25 09:17:32 -04:00
parent 4f31304b66
commit 263267c5ad
34 changed files with 556 additions and 308 deletions

View File

@ -187,6 +187,7 @@ For convenience, a web front-end on top of the command-line tool is available at
- Snap packages now print more user-friendly error messages when permission errors are encountered. - Snap packages now print more user-friendly error messages when permission errors are encountered.
- JSON 'target' field now always includes port number; credit [tomatohater1337](https://github.com/tomatohater1337). - JSON 'target' field now always includes port number; credit [tomatohater1337](https://github.com/tomatohater1337).
- JSON output now includes recommendations and CVE data. - JSON output now includes recommendations and CVE data.
- Mixed host key/CA key types (i.e.: RSA host keys signed with ED25519 CAs, etc.) are now properly handled.
- Warnings are now printed for 2048-bit moduli. - Warnings are now printed for 2048-bit moduli.
- SHA-1 algorithms now cause failures. - SHA-1 algorithms now cause failures.
- CBC mode ciphers are now warnings instead of failures. - CBC mode ciphers are now warnings instead of failures.

View File

@ -616,8 +616,8 @@ run_policy_test() {
exit 1 exit 1
fi fi
#echo "Running: ./ssh-audit.py -P \"${policy_path}\" -jj localhost:2222 > ${test_result_json}" #echo "Running: ./ssh-audit.py -P \"${policy_path}\" -jj localhost:2222 > ${test_result_json} 2> /dev/null"
./ssh-audit.py -P "${policy_path}" -jj localhost:2222 > "${test_result_json}" ./ssh-audit.py -P "${policy_path}" -jj localhost:2222 > "${test_result_json}" 2> /dev/null
actual_exit_code=$? actual_exit_code=$?
if [[ ${actual_exit_code} != "${expected_exit_code}" ]]; then if [[ ${actual_exit_code} != "${expected_exit_code}" ]]; then
echo -e "${test_name} ${REDB}FAILED${CLR} (expected exit code: ${expected_exit_code}; actual exit code: ${actual_exit_code}\n" echo -e "${test_name} ${REDB}FAILED${CLR} (expected exit code: ${expected_exit_code}; actual exit code: ${actual_exit_code}\n"

View File

@ -27,7 +27,7 @@ import traceback
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401 from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
from typing import Callable, Optional, Union, Any # 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_kexdb import SSH2_KexDB
from ssh_audit.ssh2_kex import SSH2_Kex from ssh_audit.ssh2_kex import SSH2_Kex
from ssh_audit.ssh_socket import SSH_Socket from ssh_audit.ssh_socket import SSH_Socket
@ -63,8 +63,8 @@ class GEXTest:
try: try:
# Parse the server's KEX. # Parse the server's KEX.
_, payload = s.read_packet(2) _, payload = s.read_packet(2)
SSH2_Kex.parse(payload) SSH2_Kex.parse(out, payload)
except Exception: except KexDHException:
out.v("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()), write_now=True) out.v("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
return False return False
@ -98,7 +98,7 @@ class GEXTest:
if gex_alg not in kex.kex_algorithms: if gex_alg not in kex.kex_algorithms:
out.d('Server does not support the algorithm "' + gex_alg + '".', write_now=True) out.d('Server does not support the algorithm "' + gex_alg + '".', write_now=True)
else: 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) 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 # It has been observed that reconnecting to some SSH servers
@ -115,7 +115,7 @@ class GEXTest:
kex_group.recv_reply(s, False) kex_group.recv_reply(s, False)
modulus_size_returned = kex_group.get_dh_modulus_size() modulus_size_returned = kex_group.get_dh_modulus_size()
out.d('Modulus size returned by server: ' + str(modulus_size_returned) + ' bits', write_now=True) 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) out.d('[exception] ' + str(traceback.format_exc()), write_now=True)
finally: finally:
# The server is in a state that is not re-testable, # 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: if GEXTest.reconnect(out, s, kex, gex_alg) is False:
break break
kex_group = kex_group_class() kex_group = kex_group_class(out)
smallest_modulus = -1 smallest_modulus = -1
# First try a range of weak sizes. # First try a range of weak sizes.
@ -169,7 +169,7 @@ class GEXTest:
smallest_modulus = kex_group.get_dh_modulus_size() smallest_modulus = kex_group.get_dh_modulus_size()
out.d('Modulus size returned by server: ' + str(smallest_modulus) + ' bits', write_now=True) 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) out.d('[exception] ' + str(traceback.format_exc()), write_now=True)
finally: finally:
s.close() s.close()
@ -194,8 +194,8 @@ class GEXTest:
kex_group.recv_reply(s, False) kex_group.recv_reply(s, False)
smallest_modulus = kex_group.get_dh_modulus_size() smallest_modulus = kex_group.get_dh_modulus_size()
out.d('Modulus size returned by server: ' + str(smallest_modulus) + ' bits', write_now=True) out.d('Modulus size returned by server: ' + str(smallest_modulus) + ' bits', write_now=True)
except Exception: except KexDHException as e:
out.d('[exception] ' + str(traceback.format_exc()), write_now=True) 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: finally:
# The server is in a state that is not re-testable, # The server is in a state that is not re-testable,
# so there's nothing else to do with this open # so there's nothing else to do with this open

View File

@ -28,7 +28,7 @@ from typing import Callable, Optional, Union, Any # noqa: F401
import traceback 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_kex import SSH2_Kex
from ssh_audit.ssh2_kexdb import SSH2_KexDB from ssh_audit.ssh2_kexdb import SSH2_KexDB
from ssh_audit.ssh_socket import SSH_Socket 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' 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 @staticmethod
@ -82,7 +83,7 @@ class HostKeyTest:
for server_kex_alg in server_kex.kex_algorithms: for server_kex_alg in server_kex.kex_algorithms:
if server_kex_alg in KEX_TO_DHGROUP: if server_kex_alg in KEX_TO_DHGROUP:
kex_str = server_kex_alg kex_str = server_kex_alg
kex_group = KEX_TO_DHGROUP[kex_str]() kex_group = KEX_TO_DHGROUP[kex_str](out)
break break
if kex_str is not None and kex_group is not None: 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) out.d('Preparing to obtain ' + host_key_type + ' host key...', write_now=True)
cert = host_key_types[host_key_type]['cert'] 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 the connection is closed, re-open it and get the kex again.
if not s.is_connected(): if not s.is_connected():
@ -131,7 +131,7 @@ class HostKeyTest:
try: try:
# Parse the server's KEX. # Parse the server's KEX.
_, payload = s.read_packet() _, payload = s.read_packet()
SSH2_Kex.parse(payload) SSH2_Kex.parse(out, payload)
except Exception: except Exception:
out.v("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()), write_now=True) out.v("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
return return
@ -139,15 +139,29 @@ class HostKeyTest:
# Do the initial DH exchange. The server responds back # 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. # with the host key and its length. Bingo. We also get back the host key fingerprint.
kex_group.send_init(s) kex_group.send_init(s)
raw_hostkey_bytes = b''
try: try:
host_key = kex_group.recv_reply(s, variable_key_len) kex_reply = kex_group.recv_reply(s)
if host_key is not None: raw_hostkey_bytes = kex_reply if kex_reply is not None else b''
server_kex.set_host_key(host_key_type, host_key) except KexDHException:
except Exception: out.v("Failed to parse server's host key. Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
pass
# 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() hostkey_modulus_size = kex_group.get_hostkey_size()
ca_key_type = kex_group.get_ca_type()
ca_modulus_size = kex_group.get_ca_size() 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 # Close the socket, as the connection has
# been put in a state that later tests can't use. # 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 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: if hostkey_modulus_size > 0 or ca_modulus_size > 0:
# Set the hostkey size for all RSA key types since 'ssh-rsa', # The minimum good modulus size for RSA host keys is 3072. However, since ECC cryptosystems are fundamentally different, the minimum good is 256.
# 'rsa-sha2-256', etc. are all using the same host key. hostkey_min_good = cakey_min_good = 3072
# Note, however, that this may change in the future. hostkey_min_warn = cakey_min_warn = 2048
if cert is False and host_key_type in HostKeyTest.RSA_FAMILY: hostkey_warn_str = cakey_warn_str = HostKeyTest.TWO2K_MODULUS_WARNING
for rsa_type in HostKeyTest.RSA_FAMILY: if host_key_type.startswith('ssh-ed25519') or host_key_type.startswith('ecdsa-sha2-nistp'):
server_kex.set_rsa_key_size(rsa_type, hostkey_modulus_size) hostkey_min_good = 256
elif cert is True: hostkey_min_warn = 224
server_kex.set_rsa_key_size(host_key_type, hostkey_modulus_size, ca_modulus_size) 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. # 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): if (cert is False) and (hostkey_modulus_size < hostkey_min_good):
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
alg_list = SSH2_KexDB.ALGORITHMS['key'][host_key_type] 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. # Ensure that failure & warning lists exist.
while len(alg_list) < 3: while len(alg_list) < 3:
alg_list.append([]) alg_list.append([])
if (hostkey_modulus_size < 2048) or (ca_modulus_size > 0 and ca_modulus_size < 2048): # pylint: disable=chained-comparison # If the key is under 2048, add to the failure list.
alg_list[1].append('using small %d-bit modulus' % min_modulus) if hostkey_modulus_size < hostkey_min_warn:
elif HostKeyTest.TWO2K_MODULUS_WARNING not in alg_list[2]: alg_list[1].append('using small %d-bit modulus' % hostkey_modulus_size)
alg_list[2].append(HostKeyTest.TWO2K_MODULUS_WARNING) 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 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: if host_key_type in HostKeyTest.RSA_FAMILY:

View File

@ -1,7 +1,7 @@
""" """
The MIT License (MIT) 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) Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
Permission is hereby granted, free of charge, to any person obtaining a copy 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 Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
from typing import Callable, Optional, Union, Any # 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.protocol import Protocol
from ssh_audit.ssh_socket import SSH_Socket from ssh_audit.ssh_socket import SSH_Socket
@ -40,7 +41,8 @@ class KexDHException(Exception):
class KexDH: # pragma: nocover 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.__kex_name = kex_name # pylint: disable=unused-private-member
self.__hash_alg = hash_alg # pylint: disable=unused-private-member self.__hash_alg = hash_alg # pylint: disable=unused-private-member
self.__g = 0 self.__g = 0
@ -51,10 +53,11 @@ class KexDH: # pragma: nocover
self.set_params(g, p) self.set_params(g, p)
self.__ed25519_pubkey: Optional[bytes] = None # pylint: disable=unused-private-member 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_e = 0 # pylint: disable=unused-private-member
self.__hostkey_n = 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.__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). 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: 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 # contains the host key, among other things. Function returns the host
# key blob (from which the fingerprint can be calculated). # key blob (from which the fingerprint can be calculated).
def recv_reply(self, s: 'SSH_Socket', parse_host_key_size: bool = True) -> Optional[bytes]: 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) packet_type, payload = s.read_packet(2)
# Skip any & all MSG_DEBUG messages. # 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 # A connection error occurred. We can't parse anything, so just
# return. The host key modulus (and perhaps certificate modulus) # return. The host key modulus (and perhaps certificate modulus)
# will remain at length 0. # will remain at length 0.
self.out.d("KexDH.recv_reply(): received packge_type == -1.")
return None 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. # Get the host key blob, F, and signature.
ptr = 0 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 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: if not parse_host_key_size:
@ -116,76 +116,106 @@ class KexDH: # pragma: nocover
# Now pick apart the host key blob. # Now pick apart the host key blob.
# Get the host key type (i.e.: 'ssh-rsa', 'ssh-ed25519', etc). # Get the host key type (i.e.: 'ssh-rsa', 'ssh-ed25519', etc).
ptr = 0 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 this is an RSA certificate, skip over the nonce.
if self.__hostkey_type.startswith(b'ssh-rsa-cert-v0'): if self.__hostkey_type.startswith('ssh-rsa-cert-v0'):
nonce, nonce_len, ptr = KexDH.__get_bytes(hostkey, ptr) self.out.d("RSA certificate found, so skipping nonce.")
_, _, ptr = KexDH.__get_bytes(hostkey, ptr) # Read & skip over the nonce.
# The public key exponent. # 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 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. # ED25519 moduli are fixed at 32 bytes.
hostkey_n, self.__hostkey_n_len, ptr = KexDH.__get_bytes(hostkey, ptr) if self.__hostkey_type == 'ssh-ed25519':
self.__hostkey_n = int(binascii.hexlify(hostkey_n), 16) # pylint: disable=unused-private-member 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 # 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.
# key. if self.__hostkey_type.startswith('ssh-rsa-cert-v0') or self.__hostkey_type.startswith('ssh-ed25519-cert-v0'):
if self.__hostkey_type.startswith(b'ssh-rsa-cert-v0'): # Get the CA key type and key length.
# Skip over the serial number. self.__ca_key_type, self.__ca_n_len = self.__parse_ca_key(hostkey, self.__hostkey_type, ptr)
ptr += 8 self.out.d("KexDH.__parse_ca_key(): CA key type: [%s]; CA key length: %u" % (self.__ca_key_type, self.__ca_n_len))
# 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)
return hostkey 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 @staticmethod
def __get_bytes(buf: bytes, ptr: int) -> Tuple[bytes, int, int]: def __get_bytes(buf: bytes, ptr: int) -> Tuple[bytes, int, int]:
num_bytes = struct.unpack('>I', buf[ptr:ptr + 4])[0] num_bytes = struct.unpack('>I', buf[ptr:ptr + 4])[0]
@ -205,10 +235,18 @@ class KexDH: # pragma: nocover
size = size - 8 size = size - 8
return size return size
# Returns the hostkey type.
def get_hostkey_type(self) -> str:
return self.__hostkey_type
# Returns the size of the hostkey, in bits. # Returns the size of the hostkey, in bits.
def get_hostkey_size(self) -> int: def get_hostkey_size(self) -> int:
return KexDH.__adjust_key_size(self.__hostkey_n_len) 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. # Returns the size of the CA key, in bits.
def get_ca_size(self) -> int: def get_ca_size(self) -> int:
return KexDH.__adjust_key_size(self.__ca_n_len) return KexDH.__adjust_key_size(self.__ca_n_len)
@ -220,46 +258,46 @@ class KexDH: # pragma: nocover
class KexGroup1(KexDH): # pragma: nocover class KexGroup1(KexDH): # pragma: nocover
def __init__(self) -> None: def __init__(self, out: 'OutputBuffer') -> None:
# rfc2409: second oakley group # rfc2409: second oakley group
p = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece65381ffffffffffffffff', 16) 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 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 # rfc3526: 2048-bit modp group
p = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff', 16) 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): class KexGroup14_SHA1(KexGroup14):
def __init__(self) -> None: def __init__(self, out: 'OutputBuffer') -> None:
super(KexGroup14_SHA1, self).__init__('sha1') super(KexGroup14_SHA1, self).__init__(out, 'sha1')
class KexGroup14_SHA256(KexGroup14): class KexGroup14_SHA256(KexGroup14):
def __init__(self) -> None: def __init__(self, out: 'OutputBuffer') -> None:
super(KexGroup14_SHA256, self).__init__('sha256') super(KexGroup14_SHA256, self).__init__(out, 'sha256')
class KexGroup16_SHA512(KexDH): class KexGroup16_SHA512(KexDH):
def __init__(self) -> None: def __init__(self, out: 'OutputBuffer') -> None:
# rfc3526: 4096-bit modp group # rfc3526: 4096-bit modp group
p = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aaac42dad33170d04507a33a85521abdf1cba64ecfb850458dbef0a8aea71575d060c7db3970f85a6e1e4c7abf5ae8cdb0933d71e8c94e04a25619dcee3d2261ad2ee6bf12ffa06d98a0864d87602733ec86a64521f2b18177b200cbbe117577a615d6c770988c0bad946e208e24fa074e5ab3143db5bfce0fd108e4b82d120a92108011a723c12a787e6d788719a10bdba5b2699c327186af4e23c1a946834b6150bda2583e9ca2ad44ce8dbbbc2db04de8ef92e8efc141fbecaa6287c59474e6bc05d99b2964fa090c3a2233ba186515be7ed1f612970cee2d7afb81bdd762170481cd0069127d5b05aa993b4ea988d8fddc186ffb7dc90a6c08f4df435c934063199ffffffffffffffff', 16) 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): class KexGroup18_SHA512(KexDH):
def __init__(self) -> None: def __init__(self, out: 'OutputBuffer') -> None:
# rfc3526: 8192-bit modp group # rfc3526: 8192-bit modp group
p = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aaac42dad33170d04507a33a85521abdf1cba64ecfb850458dbef0a8aea71575d060c7db3970f85a6e1e4c7abf5ae8cdb0933d71e8c94e04a25619dcee3d2261ad2ee6bf12ffa06d98a0864d87602733ec86a64521f2b18177b200cbbe117577a615d6c770988c0bad946e208e24fa074e5ab3143db5bfce0fd108e4b82d120a92108011a723c12a787e6d788719a10bdba5b2699c327186af4e23c1a946834b6150bda2583e9ca2ad44ce8dbbbc2db04de8ef92e8efc141fbecaa6287c59474e6bc05d99b2964fa090c3a2233ba186515be7ed1f612970cee2d7afb81bdd762170481cd0069127d5b05aa993b4ea988d8fddc186ffb7dc90a6c08f4df435c93402849236c3fab4d27c7026c1d4dcb2602646dec9751e763dba37bdf8ff9406ad9e530ee5db382f413001aeb06a53ed9027d831179727b0865a8918da3edbebcf9b14ed44ce6cbaced4bb1bdb7f1447e6cc254b332051512bd7af426fb8f401378cd2bf5983ca01c64b92ecf032ea15d1721d03f482d7ce6e74fef6d55e702f46980c82b5a84031900b1c9e59e7c97fbec7e8f323a97a7e36cc88be0f1d45b7ff585ac54bd407b22b4154aacc8f6d7ebf48e1d814cc5ed20f8037e0a79715eef29be32806a1d58bb7c5da76f550aa3d8a1fbff0eb19ccb1a313d55cda56c9ec2ef29632387fe8d76e3c0468043e8f663f4860ee12bf2d5b0b7474d6e694f91e6dbe115974a3926f12fee5e438777cb6a932df8cd8bec4d073b931ba3bc832b68d9dd300741fa7bf8afc47ed2576f6936ba424663aab639c5ae4f5683423b4742bf1c978238f16cbe39d652de3fdb8befc848ad922222e04a4037c0713eb57a81a23f0c73473fc646cea306b4bcbc8862f8385ddfa9d4b7fa2c087e879683303ed5bdd3a062b3cf5b3a278a66d2a13f83f44f82ddf310ee074ab6a364597e899a0255dc164f31cc50846851df9ab48195ded7ea1b1d510bd7ee74d73faf36bc31ecfa268359046f4eb879f924009438b481c6cd7889a002ed5ee382bc9190da6fc026e479558e4475677e9aa9e3050e2765694dfc81f56e880b96e7160c980dd98edd3dfffffffffffffffff', 16) 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): class KexCurve25519_SHA256(KexDH):
def __init__(self) -> None: def __init__(self, out: 'OutputBuffer') -> None:
super(KexCurve25519_SHA256, self).__init__('KexCurve25519_SHA256', 'sha256', 0, 0) 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 # To start an ED25519 kex, we simply send a random 256-bit number as the
# public key. # public key.
@ -271,8 +309,8 @@ class KexCurve25519_SHA256(KexDH):
class KexNISTP256(KexDH): class KexNISTP256(KexDH):
def __init__(self) -> None: def __init__(self, out: 'OutputBuffer') -> None:
super(KexNISTP256, self).__init__('KexNISTP256', 'sha256', 0, 0) super(KexNISTP256, self).__init__(out, 'KexNISTP256', 'sha256', 0, 0)
# Because the server checks that the value sent here is valid (i.e.: it lies # 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 # 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): class KexNISTP384(KexDH):
def __init__(self) -> None: def __init__(self, out: 'OutputBuffer') -> None:
super(KexNISTP384, self).__init__('KexNISTP384', 'sha256', 0, 0) super(KexNISTP384, self).__init__(out, 'KexNISTP384', 'sha256', 0, 0)
# See comment for KexNISTP256.send_init(). # See comment for KexNISTP256.send_init().
def send_init(self, s: 'SSH_Socket', init_msg: int = Protocol.MSG_KEXDH_INIT) -> None: 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): class KexNISTP521(KexDH):
def __init__(self) -> None: def __init__(self, out: 'OutputBuffer') -> None:
super(KexNISTP521, self).__init__('KexNISTP521', 'sha256', 0, 0) super(KexNISTP521, self).__init__(out, 'KexNISTP521', 'sha256', 0, 0)
# See comment for KexNISTP256.send_init(). # See comment for KexNISTP256.send_init().
def send_init(self, s: 'SSH_Socket', init_msg: int = Protocol.MSG_KEXDH_INIT) -> None: 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): class KexGroupExchange(KexDH):
def __init__(self, classname: str, hash_alg: str) -> None: def __init__(self, out: 'OutputBuffer', classname: str, hash_alg: str) -> None:
super(KexGroupExchange, self).__init__(classname, hash_alg, 0, 0) 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: def send_init(self, s: 'SSH_Socket', init_msg: int = Protocol.MSG_KEXDH_GEX_REQUEST) -> None:
self.send_init_gex(s) self.send_init_gex(s)
@ -358,10 +396,10 @@ class KexGroupExchange(KexDH):
class KexGroupExchange_SHA1(KexGroupExchange): class KexGroupExchange_SHA1(KexGroupExchange):
def __init__(self) -> None: def __init__(self, out: 'OutputBuffer') -> None:
super(KexGroupExchange_SHA1, self).__init__('KexGroupExchange_SHA1', 'sha1') super(KexGroupExchange_SHA1, self).__init__(out, 'KexGroupExchange_SHA1', 'sha1')
class KexGroupExchange_SHA256(KexGroupExchange): class KexGroupExchange_SHA256(KexGroupExchange):
def __init__(self) -> None: def __init__(self, out: 'OutputBuffer') -> None:
super(KexGroupExchange_SHA256, self).__init__('KexGroupExchange_SHA256', 'sha256') super(KexGroupExchange_SHA256, self).__init__(out, 'KexGroupExchange_SHA256', 'sha256')

View File

@ -21,6 +21,8 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
""" """
import copy
import json
import sys import sys
from typing import Dict, List, Tuple from typing import Dict, List, Tuple
@ -28,9 +30,9 @@ from typing import Optional, Any, Union, cast
from datetime import date from datetime import date
from ssh_audit import exitcodes from ssh_audit import exitcodes
from ssh_audit.ssh2_kex import SSH2_Kex # pylint: disable=unused-import from ssh_audit.banner import Banner
from ssh_audit.banner import Banner # pylint: disable=unused-import
from ssh_audit.globals import SNAP_PACKAGE, SNAP_PERMISSIONS_ERROR 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 # 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._name: Optional[str] = None
self._version: Optional[str] = None self._version: Optional[str] = None
self._banner: Optional[str] = None self._banner: Optional[str] = None
@ -98,13 +101,19 @@ class Policy:
self._kex: Optional[List[str]] = None self._kex: Optional[List[str]] = None
self._ciphers: Optional[List[str]] = None self._ciphers: Optional[List[str]] = None
self._macs: 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._cakey_sizes: Optional[Dict[str, int]] = None
self._dh_modulus_sizes: Optional[Dict[str, int]] = None self._dh_modulus_sizes: Optional[Dict[str, int]] = None
self._server_policy = True self._server_policy = True
self._name_and_version: str = '' 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. # Ensure that only one mode was specified.
num_modes = 0 num_modes = 0
if policy_file is not None: if policy_file is not None:
@ -154,7 +163,7 @@ class Policy:
key = key.strip() key = key.strip()
val = val.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) raise ValueError("invalid field found in policy: %s" % line)
if key in ['name', 'banner']: if key in ['name', 'banner']:
@ -173,8 +182,10 @@ class Policy:
self._name = val self._name = val
elif key == 'banner': elif key == 'banner':
self._banner = val self._banner = val
elif key == 'version': elif key == 'version':
self._version = val self._version = val
elif key in ['compressions', 'host keys', 'optional host keys', 'key exchanges', 'ciphers', 'macs']: elif key in ['compressions', 'host keys', 'optional host keys', 'key exchanges', 'ciphers', 'macs']:
try: try:
algs = val.split(',') algs = val.split(',')
@ -197,21 +208,59 @@ class Policy:
self._ciphers = algs self._ciphers = algs
elif key == 'macs': elif key == 'macs':
self._macs = algs 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_type = key[13:]
hostkey_size = int(val)
if self._hostkey_sizes is None: if self._hostkey_sizes is None:
self._hostkey_sizes = {} self._hostkey_sizes = {}
self._hostkey_sizes[hostkey_type] = int(val)
elif key.startswith('cakey_size_'): self._hostkey_sizes[hostkey_type] = {'hostkey_size': hostkey_size, 'ca_key_type': '', 'ca_key_size': 0}
cakey_type = key[11:]
if self._cakey_sizes is None: elif key.startswith('cakey_size_'): # Old host key size format.
self._cakey_sizes = {} print(Policy.WARNING_DEPRECATED_DIRECTIVES, file=self._warning_target) # Warn the user that the policy file is using deprecated directives.
self._cakey_sizes[cakey_type] = int(val)
elif key.startswith('dh_modulus_size_'): hostkey_type = key[11:]
dh_modulus_type = key[16:] 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: if self._dh_modulus_sizes is None:
self._dh_modulus_sizes = {} 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': elif key.startswith('client policy') and val.lower() == 'true':
self._server_policy = False self._server_policy = False
@ -243,10 +292,9 @@ class Policy:
kex_algs = None kex_algs = None
ciphers = None ciphers = None
macs = None macs = None
rsa_hostkey_sizes_str = ''
rsa_cakey_sizes_str = ''
dh_modulus_sizes_str = '' dh_modulus_sizes_str = ''
client_policy_str = '' client_policy_str = ''
host_keys_json = ''
if client_audit: if client_audit:
client_policy_str = "\n# Set to true to signify this is a policy for clients, not servers.\nclient policy = true\n" 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) ciphers = ', '.join(kex.server.encryption)
if kex.server.mac is not None: if kex.server.mac is not None:
macs = ', '.join(kex.server.mac) 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 kex.host_keys():
if cakey_size != -1:
rsa_cakey_sizes_str = "%scakey_size_%s = %d\n" % (rsa_cakey_sizes_str, host_key_type, cakey_size) # 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(): if kex.dh_modulus_sizes():
dh_modulus_sizes_dict = kex.dh_modulus_sizes() dh_modulus_sizes_str = "\n# Group exchange DH modulus sizes.\ndh_modulus_sizes = %s\n" % json.dumps(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
policy_data = '''# policy_data = '''#
@ -299,7 +344,7 @@ version = 1
# The compression options that must match exactly (order matters). Commented out to ignore by default. # The compression options that must match exactly (order matters). Commented out to ignore by default.
# compressions = %s # compressions = %s
%s%s%s %s%s
# The host key types that must match exactly (order matters). # The host key types that must match exactly (order matters).
host keys = %s host keys = %s
@ -314,7 +359,7 @@ ciphers = %s
# The MACs that must match exactly (order matters). # The MACs that must match exactly (order matters).
macs = %s 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 return policy_data
@ -351,23 +396,29 @@ macs = %s
hostkey_types = list(self._hostkey_sizes.keys()) hostkey_types = list(self._hostkey_sizes.keys())
hostkey_types.sort() # Sorted to make testing output repeatable. hostkey_types.sort() # Sorted to make testing output repeatable.
for hostkey_type in hostkey_types: for hostkey_type in hostkey_types:
expected_hostkey_size = self._hostkey_sizes[hostkey_type] expected_hostkey_size = self._hostkey_sizes[hostkey_type]['hostkey_size']
if hostkey_type in kex.rsa_key_sizes(): server_host_keys = kex.host_keys()
actual_hostkey_size, actual_cakey_size = kex.rsa_key_sizes()[hostkey_type] if hostkey_type in server_host_keys:
actual_hostkey_size = server_host_keys[hostkey_type]['hostkey_size']
if actual_hostkey_size != expected_hostkey_size: if actual_hostkey_size != expected_hostkey_size:
ret = False 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: # If we have expected CA signatures set, check them against what the server returned.
hostkey_types = list(self._cakey_sizes.keys()) 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:
hostkey_types.sort() # Sorted to make testing output repeatable. expected_ca_key_type = cast(str, self._hostkey_sizes[hostkey_type]['ca_key_type'])
for hostkey_type in hostkey_types: expected_ca_key_size = cast(int, self._hostkey_sizes[hostkey_type]['ca_key_size'])
expected_cakey_size = self._cakey_sizes[hostkey_type] actual_ca_key_type = cast(str, server_host_keys[hostkey_type]['ca_key_type'])
if hostkey_type in kex.rsa_key_sizes(): actual_ca_key_size = cast(int, server_host_keys[hostkey_type]['ca_key_size'])
actual_hostkey_size, actual_cakey_size = kex.rsa_key_sizes()[hostkey_type]
if actual_cakey_size != expected_cakey_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).
ret = False if actual_ca_key_type != expected_ca_key_type:
self._append_error(errors, 'RSA CA key (%s) sizes' % hostkey_type, [str(expected_cakey_size)], None, [str(actual_cakey_size)]) 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: if kex.kex_algorithms != self._kex:
ret = False ret = False
@ -387,7 +438,7 @@ macs = %s
for dh_modulus_type in dh_modulus_types: for dh_modulus_type in dh_modulus_types:
expected_dh_modulus_size = self._dh_modulus_sizes[dh_modulus_type] expected_dh_modulus_size = self._dh_modulus_sizes[dh_modulus_type]
if dh_modulus_type in kex.dh_modulus_sizes(): 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: if expected_dh_modulus_size != actual_dh_modulus_size:
ret = False 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)]) 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 @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.''' '''Returns a Policy with the specified built-in policy name loaded, or None if no policy of that name exists.'''
p = None p = None
if policy_name in Policy.BUILTIN_POLICIES: if policy_name in Policy.BUILTIN_POLICIES:
policy_struct = Policy.BUILTIN_POLICIES[policy_name] 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(' (')] policy_name_without_version = policy_name[0:policy_name.rfind(' (')]
p._name = policy_name_without_version # pylint: disable=protected-access p._name = policy_name_without_version # pylint: disable=protected-access
p._version = cast(str, policy_struct['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._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._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._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._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._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 p._server_policy = cast(bool, policy_struct['server_policy']) # pylint: disable=protected-access

View File

@ -22,17 +22,18 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
""" """
# pylint: disable=unused-import from typing import Dict, List
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401 from typing import Union
from typing import Callable, Optional, Union, Any # noqa: F401
from ssh_audit.ssh2_kexparty import SSH2_KexParty from ssh_audit.outputbuffer import OutputBuffer
from ssh_audit.readbuf import ReadBuf from ssh_audit.readbuf import ReadBuf
from ssh_audit.ssh2_kexparty import SSH2_KexParty
from ssh_audit.writebuf import WriteBuf from ssh_audit.writebuf import WriteBuf
class SSH2_Kex: 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.__cookie = cookie
self.__kex_algs = kex_algs self.__kex_algs = kex_algs
self.__key_algs = key_algs self.__key_algs = key_algs
@ -41,9 +42,8 @@ class SSH2_Kex:
self.__follows = follows self.__follows = follows
self.__unused = unused self.__unused = unused
self.__rsa_key_sizes: Dict[str, Tuple[int, int]] = {} self.__dh_modulus_sizes: Dict[str, int] = {}
self.__dh_modulus_sizes: Dict[str, Tuple[int, int]] = {} self.__host_keys: Dict[str, Dict[str, Union[bytes, str, int]]] = {}
self.__host_keys: Dict[str, bytes] = {}
@property @property
def cookie(self) -> bytes: def cookie(self) -> bytes:
@ -75,22 +75,20 @@ class SSH2_Kex:
def unused(self) -> int: def unused(self) -> int:
return self.__unused 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: 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 return self.__dh_modulus_sizes
def set_host_key(self, key_type: str, hostkey: bytes) -> None: def set_host_key(self, key_type: str, raw_hostkey_bytes: bytes, hostkey_size: int, ca_key_type: str, ca_key_size: int) -> None:
self.__host_keys[key_type] = hostkey
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 return self.__host_keys
def write(self, wbuf: 'WriteBuf') -> None: def write(self, wbuf: 'WriteBuf') -> None:
@ -115,7 +113,7 @@ class SSH2_Kex:
return wbuf.write_flush() return wbuf.write_flush()
@classmethod @classmethod
def parse(cls, payload: bytes) -> 'SSH2_Kex': def parse(cls, outputbuffer: 'OutputBuffer', payload: bytes) -> 'SSH2_Kex':
buf = ReadBuf(payload) buf = ReadBuf(payload)
cookie = buf.read(16) cookie = buf.read(16)
kex_algs = buf.read_list() kex_algs = buf.read_list()
@ -132,5 +130,5 @@ class SSH2_Kex:
unused = buf.read_int() unused = buf.read_int()
cli = SSH2_KexParty(cli_enc, cli_mac, cli_compression, cli_languages) cli = SSH2_KexParty(cli_enc, cli_mac, cli_compression, cli_languages)
srv = SSH2_KexParty(srv_enc, srv_mac, srv_compression, srv_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 return kex

View File

@ -34,7 +34,7 @@ import traceback
# pylint: disable=unused-import # pylint: disable=unused-import
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401 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_PACKAGE
from ssh_audit.globals import SNAP_PERMISSIONS_ERROR from ssh_audit.globals import SNAP_PERMISSIONS_ERROR
@ -107,10 +107,10 @@ def usage(uout: OutputBuffer, err: Optional[str] = None) -> None:
sys.exit(retval) 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: with out:
for algorithm in algorithms: 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: if not out.is_section_empty() and not is_json_output:
out.head('# ' + title) out.head('# ' + title)
out.flush_section() out.flush_section()
@ -119,7 +119,7 @@ def output_algorithms(out: OutputBuffer, title: str, alg_db: Dict[str, Dict[str,
return program_retval 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 + ') ' prefix = '(' + alg_type + ') '
if alg_max_len == 0: if alg_max_len == 0:
alg_max_len = len(alg_name) 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 # If this is an RSA host key or DH GEX, append the size to its name and fix
# the padding. # the padding.
alg_name_with_size = None alg_name_with_size = None
if (alg_sizes is not None) and (alg_name in alg_sizes): if (dh_modulus_sizes is not None) and (alg_name in dh_modulus_sizes):
hostkey_size, ca_size = alg_sizes[alg_name] alg_name_with_size = '%s (%u-bit)' % (alg_name, dh_modulus_sizes[alg_name])
if ca_size > 0: padding = padding[0:-11]
alg_name_with_size = '%s (%d-bit cert/%d-bit CA)' % (alg_name, hostkey_size, ca_size) 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] padding = padding[0:-15]
else: elif alg_name in HostKeyTest.RSA_FAMILY:
alg_name_with_size = '%s (%d-bit)' % (alg_name, hostkey_size) alg_name_with_size = '%s (%u-bit)' % (alg_name, hostkey_size)
padding = padding[0:-11] 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. # 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: def output_fingerprints(out: OutputBuffer, algs: Algorithms, is_json_output: bool) -> None:
with out: with out:
fps = [] fps = {}
if algs.ssh1kex is not None: if algs.ssh1kex is not None:
name = 'ssh-rsa1' name = 'ssh-rsa1'
fp = Fingerprint(algs.ssh1kex.host_key_fingerprint_data) fp = Fingerprint(algs.ssh1kex.host_key_fingerprint_data)
# bits = algs.ssh1kex.host_key_bits # bits = algs.ssh1kex.host_key_bits
fps.append((name, fp)) fps[name] = fp
if algs.ssh2kex is not None: if algs.ssh2kex is not None:
host_keys = algs.ssh2kex.host_keys() host_keys = algs.ssh2kex.host_keys()
for host_key_type in algs.ssh2kex.host_keys(): for host_key_type in algs.ssh2kex.host_keys():
if host_keys[host_key_type] is None: if host_keys[host_key_type] is None:
continue 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'. # 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: if host_key_type in HostKeyTest.RSA_FAMILY:
host_key_type = 'ssh-rsa' 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: 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. # 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) fp_types = sorted(fps.keys())
for fpp in fps: for fp_type in fp_types:
name, fp = fpp fp = fps[fp_type]
out.good('(fin) {}: {}'.format(name, fp.sha256)) out.good('(fin) {}: {}'.format(fp_type, fp.sha256))
# Output the MD5 hash too if verbose mode is enabled. # Output the MD5 hash too if verbose mode is enabled.
if out.verbose: 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: if not out.is_section_empty() and not is_json_output:
out.head('# fingerprints') out.head('# fingerprints')
@ -422,7 +432,7 @@ def post_process_findings(banner: Optional[Banner], algs: Algorithms) -> List[st
algorithm_recommendation_suppress_list = [] 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 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. # Ensure a list for notes exists.
while len(SSH2_KexDB.ALGORITHMS['kex']['diffie-hellman-group-exchange-sha256']) < 4: 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) cves = output_security(out, banner, client_audit, maxlen, aconf.json)
# Filled in by output_algorithms() with unidentified algs. # Filled in by output_algorithms() with unidentified algs.
unknown_algorithms: List[str] = [] unknown_algorithms: List[str] = []
# SSHv1
if pkm is not None: if pkm is not None:
adb = SSH1_KexDB.ALGORITHMS adb = SSH1_KexDB.ALGORITHMS
ciphers = pkm.supported_ciphers 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) program_retval = output_algorithms(out, title, adb, atype, ciphers, unknown_algorithms, aconf.json, program_retval, maxlen)
title, atype = 'SSH1 authentication types', 'aut' title, atype = 'SSH1 authentication types', 'aut'
program_retval = output_algorithms(out, title, adb, atype, auths, unknown_algorithms, aconf.json, program_retval, maxlen) program_retval = output_algorithms(out, title, adb, atype, auths, unknown_algorithms, aconf.json, program_retval, maxlen)
# SSHv2
if kex is not None: if kex is not None:
adb = SSH2_KexDB.ALGORITHMS adb = SSH2_KexDB.ALGORITHMS
title, atype = 'key exchange algorithms', 'kex' 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' 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' title, atype = 'encryption algorithms (ciphers)', 'enc'
program_retval = output_algorithms(out, title, adb, atype, kex.server.encryption, unknown_algorithms, aconf.json, program_retval, maxlen) 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' 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) program_retval = output_algorithms(out, title, adb, atype, kex.server.mac, unknown_algorithms, aconf.json, program_retval, maxlen)
output_fingerprints(out, algs, aconf.json) output_fingerprints(out, algs, aconf.json)
perfect_config = output_recommendations(out, algs, algorithm_recommendation_suppress_list, software, aconf.json, maxlen) 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) 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): 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. # 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: if aconf.policy is None:
try: 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: except Exception as e:
out.fail("Error while loading policy file: %s: %s" % (str(e), traceback.format_exc())) out.fail("Error while loading policy file: %s: %s" % (str(e), traceback.format_exc()))
out.write() 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['compression'] = kex.server.compression
res['kex'] = [] res['kex'] = []
alg_sizes = kex.dh_modulus_sizes() dh_alg_sizes = kex.dh_modulus_sizes()
for algorithm in kex.kex_algorithms: for algorithm in kex.kex_algorithms:
entry: Any = { entry: Any = {
'algorithm': algorithm, 'algorithm': algorithm,
} }
if algorithm in alg_sizes: if algorithm in dh_alg_sizes:
hostkey_size, ca_size = alg_sizes[algorithm] hostkey_size = dh_alg_sizes[algorithm]
entry['keysize'] = hostkey_size entry['keysize'] = hostkey_size
if ca_size > 0:
entry['casize'] = ca_size
res['kex'].append(entry) res['kex'].append(entry)
res['key'] = [] res['key'] = []
alg_sizes = kex.rsa_key_sizes() host_keys = kex.host_keys()
for algorithm in kex.key_algorithms: for algorithm in kex.key_algorithms:
entry = { entry = {
'algorithm': algorithm, 'algorithm': algorithm,
} }
if algorithm in alg_sizes: if algorithm in host_keys:
hostkey_size, ca_size = alg_sizes[algorithm] hostkey_info = host_keys[algorithm]
entry['keysize'] = hostkey_size 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: if ca_size > 0:
entry['ca_algorithm'] = ca_type
entry['casize'] = ca_size entry['casize'] = ca_size
res['key'].append(entry) 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: if host_keys[host_key_type] is None:
continue 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). # Skip over certificate host types (or we would return invalid fingerprints).
if '-cert-' in host_key_type: 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)) program_retval = output(out, aconf, banner, header, pkm=SSH1_PublicKeyMessage.parse(payload))
elif sshv == 2: elif sshv == 2:
try: try:
kex = SSH2_Kex.parse(payload) kex = SSH2_Kex.parse(out, payload)
except Exception: except Exception:
out.fail("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc())) out.fail("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()))
return exitcodes.CONNECTION_ERROR return exitcodes.CONNECTION_ERROR

View File

@ -236,7 +236,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
self.__outputbuffer.d('KEX initialisation...', write_now=True) self.__outputbuffer.d('KEX initialisation...', write_now=True)
kexparty = SSH2_KexParty(ciphers, macs, compressions, languages) 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) self.write_byte(Protocol.MSG_KEXINIT)
kex.write(self) kex.write(self)

View File

@ -10,7 +10,7 @@
"expected_required": [ "expected_required": [
"4096" "4096"
], ],
"mismatched_field": "RSA host key (ssh-rsa-cert-v01@openssh.com) sizes" "mismatched_field": "Host key (ssh-rsa-cert-v01@openssh.com) sizes"
}, },
{ {
"actual": [ "actual": [
@ -22,7 +22,7 @@
"expected_required": [ "expected_required": [
"4096" "4096"
], ],
"mismatched_field": "RSA CA key (ssh-rsa-cert-v01@openssh.com) sizes" "mismatched_field": "CA signature size (ssh-rsa)"
} }
], ],
"host": "localhost", "host": "localhost",

View File

@ -1,13 +1,28 @@
WARNING: 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.
WARNING: 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.
WARNING: 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.
WARNING: 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.
WARNING: 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.
Host: localhost:2222 Host: localhost:2222
Policy: Docker poliicy: test10 (version 1) Policy: Docker poliicy: test10 (version 1)
Result: ❌ Failed! Result: ❌ Failed!
 
Errors: Errors:
* RSA CA key (ssh-rsa-cert-v01@openssh.com) sizes did not match. * CA signature size (ssh-rsa) did not match.
- Expected: 4096 - Expected: 4096
- Actual: 1024 - Actual: 1024
* RSA host key (ssh-rsa-cert-v01@openssh.com) sizes did not match. * Host key (ssh-rsa-cert-v01@openssh.com) sizes did not match.
- Expected: 4096 - Expected: 4096
- Actual: 3072 - Actual: 3072
 

View File

@ -1,3 +1,18 @@
WARNING: 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.
WARNING: 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.
WARNING: 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.
WARNING: 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.
WARNING: 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.
Host: localhost:2222 Host: localhost:2222
Policy: Docker poliicy: test7 (version 1) Policy: Docker poliicy: test7 (version 1)
Result: ✔ Passed Result: ✔ Passed

View File

@ -10,7 +10,7 @@
"expected_required": [ "expected_required": [
"2048" "2048"
], ],
"mismatched_field": "RSA CA key (ssh-rsa-cert-v01@openssh.com) sizes" "mismatched_field": "CA signature size (ssh-rsa)"
} }
], ],
"host": "localhost", "host": "localhost",

View File

@ -1,9 +1,24 @@
WARNING: 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.
WARNING: 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.
WARNING: 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.
WARNING: 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.
WARNING: 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.
Host: localhost:2222 Host: localhost:2222
Policy: Docker poliicy: test8 (version 1) Policy: Docker poliicy: test8 (version 1)
Result: ❌ Failed! Result: ❌ Failed!
 
Errors: Errors:
* RSA CA key (ssh-rsa-cert-v01@openssh.com) sizes did not match. * CA signature size (ssh-rsa) did not match.
- Expected: 2048 - Expected: 2048
- Actual: 1024 - Actual: 1024
 

View File

@ -10,7 +10,7 @@
"expected_required": [ "expected_required": [
"4096" "4096"
], ],
"mismatched_field": "RSA host key (ssh-rsa-cert-v01@openssh.com) sizes" "mismatched_field": "Host key (ssh-rsa-cert-v01@openssh.com) sizes"
} }
], ],
"host": "localhost", "host": "localhost",

View File

@ -1,9 +1,24 @@
WARNING: 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.
WARNING: 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.
WARNING: 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.
WARNING: 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.
WARNING: 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.
Host: localhost:2222 Host: localhost:2222
Policy: Docker poliicy: test9 (version 1) Policy: Docker poliicy: test9 (version 1)
Result: ❌ Failed! Result: ❌ Failed!
 
Errors: Errors:
* RSA host key (ssh-rsa-cert-v01@openssh.com) sizes did not match. * Host key (ssh-rsa-cert-v01@openssh.com) sizes did not match.
- Expected: 4096 - Expected: 4096
- Actual: 3072 - Actual: 3072
 

View File

@ -139,6 +139,7 @@
}, },
{ {
"algorithm": "ssh-rsa-cert-v01@openssh.com", "algorithm": "ssh-rsa-cert-v01@openssh.com",
"ca_algorithm": "ssh-rsa",
"casize": 1024, "casize": 1024,
"keysize": 1024 "keysize": 1024
} }

View File

@ -40,10 +40,11 @@
 `- [fail] using small 1024-bit modulus  `- [fail] using small 1024-bit modulus
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
`- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8 `- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8
(key) ssh-rsa-cert-v01@openssh.com (1024-bit cert/1024-bit CA) -- [fail] using broken SHA-1 hash algorithm (key) ssh-rsa-cert-v01@openssh.com (1024-bit cert/1024-bit RSA CA) -- [fail] using broken SHA-1 hash algorithm
 `- [fail] using small 1024-bit modulus  `- [fail] using small 1024-bit hostkey modulus
`- [info] available since OpenSSH 5.6  `- [fail] using small 1024-bit CA key modulus
`- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8 `- [info] available since OpenSSH 5.6
`- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8
# encryption algorithms (ciphers) # encryption algorithms (ciphers)
(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 (enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52

View File

@ -139,6 +139,7 @@
}, },
{ {
"algorithm": "ssh-rsa-cert-v01@openssh.com", "algorithm": "ssh-rsa-cert-v01@openssh.com",
"ca_algorithm": "ssh-rsa",
"casize": 3072, "casize": 3072,
"keysize": 1024 "keysize": 1024
} }

View File

@ -40,10 +40,10 @@
 `- [fail] using small 1024-bit modulus  `- [fail] using small 1024-bit modulus
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
`- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8 `- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8
(key) ssh-rsa-cert-v01@openssh.com (1024-bit cert/3072-bit CA) -- [fail] using broken SHA-1 hash algorithm (key) ssh-rsa-cert-v01@openssh.com (1024-bit cert/3072-bit RSA CA) -- [fail] using broken SHA-1 hash algorithm
 `- [fail] using small 1024-bit modulus  `- [fail] using small 1024-bit hostkey modulus
`- [info] available since OpenSSH 5.6 `- [info] available since OpenSSH 5.6
`- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8 `- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8
# encryption algorithms (ciphers) # encryption algorithms (ciphers)
(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 (enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52

View File

@ -139,6 +139,7 @@
}, },
{ {
"algorithm": "ssh-rsa-cert-v01@openssh.com", "algorithm": "ssh-rsa-cert-v01@openssh.com",
"ca_algorithm": "ssh-rsa",
"casize": 1024, "casize": 1024,
"keysize": 3072 "keysize": 3072
} }

View File

@ -39,10 +39,10 @@
(key) ssh-rsa (3072-bit) -- [fail] using broken SHA-1 hash algorithm (key) ssh-rsa (3072-bit) -- [fail] using broken SHA-1 hash algorithm
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
`- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8 `- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8
(key) ssh-rsa-cert-v01@openssh.com (3072-bit cert/1024-bit CA) -- [fail] using broken SHA-1 hash algorithm (key) ssh-rsa-cert-v01@openssh.com (3072-bit cert/1024-bit RSA CA) -- [fail] using broken SHA-1 hash algorithm
 `- [fail] using small 1024-bit modulus  `- [fail] using small 1024-bit CA key modulus
`- [info] available since OpenSSH 5.6 `- [info] available since OpenSSH 5.6
`- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8 `- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8
# encryption algorithms (ciphers) # encryption algorithms (ciphers)
(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 (enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52

View File

@ -139,6 +139,7 @@
}, },
{ {
"algorithm": "ssh-rsa-cert-v01@openssh.com", "algorithm": "ssh-rsa-cert-v01@openssh.com",
"ca_algorithm": "ssh-rsa",
"casize": 3072, "casize": 3072,
"keysize": 3072 "keysize": 3072
} }

View File

@ -39,9 +39,9 @@
(key) ssh-rsa (3072-bit) -- [fail] using broken SHA-1 hash algorithm (key) ssh-rsa (3072-bit) -- [fail] using broken SHA-1 hash algorithm
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
`- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8 `- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8
(key) ssh-rsa-cert-v01@openssh.com (3072-bit cert/3072-bit CA) -- [fail] using broken SHA-1 hash algorithm (key) ssh-rsa-cert-v01@openssh.com (3072-bit cert/3072-bit RSA CA) -- [fail] using broken SHA-1 hash algorithm
`- [info] available since OpenSSH 5.6 `- [info] available since OpenSSH 5.6
`- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8 `- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8
# encryption algorithms (ciphers) # encryption algorithms (ciphers)
(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 (enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52

View File

@ -1,3 +1,12 @@
WARNING: 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.
WARNING: 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.
WARNING: 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.
Host: localhost:2222 Host: localhost:2222
Policy: Docker policy: test11 (version 1) Policy: Docker policy: test11 (version 1)
Result: ✔ Passed Result: ✔ Passed

View File

@ -10,7 +10,7 @@
"expected_required": [ "expected_required": [
"4096" "4096"
], ],
"mismatched_field": "RSA host key (rsa-sha2-256) sizes" "mismatched_field": "Host key (rsa-sha2-256) sizes"
}, },
{ {
"actual": [ "actual": [
@ -22,7 +22,7 @@
"expected_required": [ "expected_required": [
"4096" "4096"
], ],
"mismatched_field": "RSA host key (rsa-sha2-512) sizes" "mismatched_field": "Host key (rsa-sha2-512) sizes"
}, },
{ {
"actual": [ "actual": [
@ -34,7 +34,7 @@
"expected_required": [ "expected_required": [
"4096" "4096"
], ],
"mismatched_field": "RSA host key (ssh-rsa) sizes" "mismatched_field": "Host key (ssh-rsa) sizes"
} }
], ],
"host": "localhost", "host": "localhost",

View File

@ -1,17 +1,26 @@
WARNING: 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.
WARNING: 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.
WARNING: 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.
Host: localhost:2222 Host: localhost:2222
Policy: Docker policy: test12 (version 1) Policy: Docker policy: test12 (version 1)
Result: ❌ Failed! Result: ❌ Failed!
 
Errors: Errors:
* RSA host key (rsa-sha2-256) sizes did not match. * Host key (rsa-sha2-256) sizes did not match.
- Expected: 4096 - Expected: 4096
- Actual: 3072 - Actual: 3072
* RSA host key (rsa-sha2-512) sizes did not match. * Host key (rsa-sha2-512) sizes did not match.
- Expected: 4096 - Expected: 4096
- Actual: 3072 - Actual: 3072
* RSA host key (ssh-rsa) sizes did not match. * Host key (ssh-rsa) sizes did not match.
- Expected: 4096 - Expected: 4096
- Actual: 3072 - Actual: 3072
 

View File

@ -1,3 +1,15 @@
WARNING: 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.
WARNING: 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.
WARNING: 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.
WARNING: 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.
Host: localhost:2222 Host: localhost:2222
Policy: Docker policy: test13 (version 1) Policy: Docker policy: test13 (version 1)
Result: ✔ Passed Result: ✔ Passed

View File

@ -1,3 +1,15 @@
WARNING: 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.
WARNING: 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.
WARNING: 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.
WARNING: 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.
Host: localhost:2222 Host: localhost:2222
Policy: Docker policy: test14 (version 1) Policy: Docker policy: test14 (version 1)
Result: ❌ Failed! Result: ❌ Failed!

View File

@ -92,7 +92,9 @@
"algorithm": "ssh-ed25519" "algorithm": "ssh-ed25519"
}, },
{ {
"algorithm": "ssh-ed25519-cert-v01@openssh.com" "algorithm": "ssh-ed25519-cert-v01@openssh.com",
"ca_algorithm": "ssh-ed25519",
"casize": 256
} }
], ],
"mac": [ "mac": [

View File

@ -34,7 +34,7 @@
# host-key algorithms # host-key algorithms
(key) ssh-ed25519 -- [info] available since OpenSSH 6.5 (key) ssh-ed25519 -- [info] available since OpenSSH 6.5
(key) ssh-ed25519-cert-v01@openssh.com -- [info] available since OpenSSH 6.5 (key) ssh-ed25519-cert-v01@openssh.com (256-bit cert/256-bit ssh-ed25519 CA) -- [info] available since OpenSSH 6.5
# encryption algorithms (ciphers) # encryption algorithms (ciphers)
(enc) chacha20-poly1305@openssh.com -- [info] available since OpenSSH 6.5 (enc) chacha20-poly1305@openssh.com -- [info] available since OpenSSH 6.5

View File

@ -1,6 +1,7 @@
import os import os
import pytest import pytest
from ssh_audit.outputbuffer import OutputBuffer
from ssh_audit.ssh2_kex import SSH2_Kex from ssh_audit.ssh2_kex import SSH2_Kex
from ssh_audit.ssh2_kexparty import SSH2_KexParty from ssh_audit.ssh2_kexparty import SSH2_KexParty
@ -13,7 +14,7 @@ def kex(ssh_audit):
enc, mac, compression, languages = [], [], ['none'], [] enc, mac, compression, languages = [], [], ['none'], []
srv = SSH2_KexParty(enc, mac, compression, languages) srv = SSH2_KexParty(enc, mac, compression, languages)
cookie = os.urandom(16) cookie = os.urandom(16)
kex = SSH2_Kex(cookie, kex_algs, key_algs, cli, srv, 0) kex = SSH2_Kex(OutputBuffer, cookie, kex_algs, key_algs, cli, srv, 0)
return kex return kex
@ -25,15 +26,15 @@ def test_prevent_runtime_error_regression(ssh_audit, kex):
keys, and an error occurred when iterating and modifying them at the keys, and an error occurred when iterating and modifying them at the
same time. same time.
""" """
kex.set_host_key("ssh-rsa", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00") kex.set_host_key("ssh-rsa", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00", 1024, '', 0)
kex.set_host_key("ssh-rsa1", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00") kex.set_host_key("ssh-rsa1", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00", 1024, '', 0)
kex.set_host_key("ssh-rsa2", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00") kex.set_host_key("ssh-rsa2", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00", 1024, '', 0)
kex.set_host_key("ssh-rsa3", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00") kex.set_host_key("ssh-rsa3", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00", 1024, '', 0)
kex.set_host_key("ssh-rsa4", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00") kex.set_host_key("ssh-rsa4", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00", 1024, '', 0)
kex.set_host_key("ssh-rsa5", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00") kex.set_host_key("ssh-rsa5", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00", 1024, '', 0)
kex.set_host_key("ssh-rsa6", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00") kex.set_host_key("ssh-rsa6", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00", 1024, '', 0)
kex.set_host_key("ssh-rsa7", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00") kex.set_host_key("ssh-rsa7", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00", 1024, '', 0)
kex.set_host_key("ssh-rsa8", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00") kex.set_host_key("ssh-rsa8", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00", 1024, '', 0)
rv = ssh_audit.build_struct('localhost', None, [], kex=kex) rv = ssh_audit.build_struct('localhost', None, [], kex=kex)

View File

@ -2,6 +2,7 @@ import hashlib
import pytest import pytest
from datetime import date from datetime import date
from ssh_audit.outputbuffer import OutputBuffer
from ssh_audit.policy import Policy from ssh_audit.policy import Policy
from ssh_audit.ssh2_kex import SSH2_Kex from ssh_audit.ssh2_kex import SSH2_Kex
from ssh_audit.writebuf import WriteBuf from ssh_audit.writebuf import WriteBuf
@ -10,6 +11,7 @@ from ssh_audit.writebuf import WriteBuf
class TestPolicy: class TestPolicy:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def init(self, ssh_audit): def init(self, ssh_audit):
self.OutputBuffer = OutputBuffer
self.Policy = Policy self.Policy = Policy
self.wbuf = WriteBuf self.wbuf = WriteBuf
self.ssh2_kex = SSH2_Kex self.ssh2_kex = SSH2_Kex
@ -32,7 +34,7 @@ class TestPolicy:
w.write_list(['']) w.write_list([''])
w.write_byte(False) w.write_byte(False)
w.write_int(0) w.write_int(0)
return self.ssh2_kex.parse(w.write_flush()) return self.ssh2_kex.parse(self.OutputBuffer, w.write_flush())
def test_builtin_policy_consistency(self): def test_builtin_policy_consistency(self):

View File

@ -79,7 +79,7 @@ class TestSSH2:
return w.write_flush() return w.write_flush()
def test_kex_read(self): def test_kex_read(self):
kex = self.ssh2_kex.parse(self._kex_payload()) kex = self.ssh2_kex.parse(self.OutputBuffer, self._kex_payload())
assert kex is not None assert kex is not None
assert kex.cookie == b'\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff' assert kex.cookie == b'\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff'
assert kex.kex_algorithms == ['bogus_kex1', 'bogus_kex2'] assert kex.kex_algorithms == ['bogus_kex1', 'bogus_kex2']
@ -105,7 +105,7 @@ class TestSSH2:
srv = self.ssh2_kexparty(enc, mac, compression, languages) srv = self.ssh2_kexparty(enc, mac, compression, languages)
if cookie is None: if cookie is None:
cookie = os.urandom(16) cookie = os.urandom(16)
kex = self.ssh2_kex(cookie, kex_algs, key_algs, cli, srv, 0) kex = self.ssh2_kex(self.OutputBuffer, cookie, kex_algs, key_algs, cli, srv, 0)
return kex return kex
def _get_kex_variat1(self): def _get_kex_variat1(self):
@ -149,7 +149,7 @@ class TestSSH2:
def test_key_payload(self): def test_key_payload(self):
kex1 = self._get_kex_variat1() kex1 = self._get_kex_variat1()
kex2 = self.ssh2_kex.parse(self._kex_payload()) kex2 = self.ssh2_kex.parse(self.OutputBuffer, self._kex_payload())
assert kex1.payload == kex2.payload assert kex1.payload == kex2.payload
def test_ssh2_server_simple(self, output_spy, virtual_socket): def test_ssh2_server_simple(self, output_spy, virtual_socket):