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

@ -28,7 +28,7 @@ from typing import Callable, Optional, Union, Any # noqa: F401
import traceback
from ssh_audit.kexdh import KexDH, KexGroup1, KexGroup14_SHA1, KexGroup14_SHA256, KexCurve25519_SHA256, KexGroup16_SHA512, KexGroup18_SHA512, KexGroupExchange_SHA1, KexGroupExchange_SHA256, KexNISTP256, KexNISTP384, KexNISTP521
from ssh_audit.kexdh import KexDH, KexDHException, KexGroup1, KexGroup14_SHA1, KexGroup14_SHA256, KexCurve25519_SHA256, KexGroup16_SHA512, KexGroup18_SHA512, KexGroupExchange_SHA1, KexGroupExchange_SHA256, KexNISTP256, KexNISTP384, KexNISTP521
from ssh_audit.ssh2_kex import SSH2_Kex
from ssh_audit.ssh2_kexdb import SSH2_KexDB
from ssh_audit.ssh_socket import SSH_Socket
@ -55,6 +55,7 @@ class HostKeyTest:
}
TWO2K_MODULUS_WARNING = '2048-bit modulus only provides 112-bits of symmetric strength'
SMALL_ECC_MODULUS_WARNING = '224-bit ECC modulus only provides 112-bits of symmetric strength'
@staticmethod
@ -82,7 +83,7 @@ class HostKeyTest:
for server_kex_alg in server_kex.kex_algorithms:
if server_kex_alg in KEX_TO_DHGROUP:
kex_str = server_kex_alg
kex_group = KEX_TO_DHGROUP[kex_str]()
kex_group = KEX_TO_DHGROUP[kex_str](out)
break
if kex_str is not None and kex_group is not None:
@ -110,7 +111,6 @@ class HostKeyTest:
out.d('Preparing to obtain ' + host_key_type + ' host key...', write_now=True)
cert = host_key_types[host_key_type]['cert']
variable_key_len = host_key_types[host_key_type]['variable_key_len']
# If the connection is closed, re-open it and get the kex again.
if not s.is_connected():
@ -131,7 +131,7 @@ class HostKeyTest:
try:
# Parse the server's KEX.
_, payload = s.read_packet()
SSH2_Kex.parse(payload)
SSH2_Kex.parse(out, payload)
except Exception:
out.v("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
return
@ -139,15 +139,29 @@ class HostKeyTest:
# Do the initial DH exchange. The server responds back
# with the host key and its length. Bingo. We also get back the host key fingerprint.
kex_group.send_init(s)
raw_hostkey_bytes = b''
try:
host_key = kex_group.recv_reply(s, variable_key_len)
if host_key is not None:
server_kex.set_host_key(host_key_type, host_key)
except Exception:
pass
kex_reply = kex_group.recv_reply(s)
raw_hostkey_bytes = kex_reply if kex_reply is not None else b''
except KexDHException:
out.v("Failed to parse server's host key. Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
# Since parsing this host key failed, there's nothing more to do but close the socket and move on to the next host key type.
s.close()
continue
hostkey_modulus_size = kex_group.get_hostkey_size()
ca_key_type = kex_group.get_ca_type()
ca_modulus_size = kex_group.get_ca_size()
out.d("Hostkey type: [%s]; hostkey size: %u; CA type: [%s]; CA modulus size: %u" % (host_key_type, hostkey_modulus_size, ca_key_type, ca_modulus_size), write_now=True)
# Record all the host key info.
server_kex.set_host_key(host_key_type, raw_hostkey_bytes, hostkey_modulus_size, ca_key_type, ca_modulus_size)
# Set the hostkey size for all RSA key types since 'ssh-rsa', 'rsa-sha2-256', etc. are all using the same host key. Note, however, that this may change in the future.
if cert is False and host_key_type in HostKeyTest.RSA_FAMILY:
for rsa_type in HostKeyTest.RSA_FAMILY:
server_kex.set_host_key(rsa_type, raw_hostkey_bytes, hostkey_modulus_size, ca_key_type, ca_modulus_size)
# Close the socket, as the connection has
# been put in a state that later tests can't use.
@ -155,43 +169,53 @@ class HostKeyTest:
# If the host key modulus or CA modulus was successfully parsed, check to see that its a safe size.
if hostkey_modulus_size > 0 or ca_modulus_size > 0:
# Set the hostkey size for all RSA key types since 'ssh-rsa',
# 'rsa-sha2-256', etc. are all using the same host key.
# Note, however, that this may change in the future.
if cert is False and host_key_type in HostKeyTest.RSA_FAMILY:
for rsa_type in HostKeyTest.RSA_FAMILY:
server_kex.set_rsa_key_size(rsa_type, hostkey_modulus_size)
elif cert is True:
server_kex.set_rsa_key_size(host_key_type, hostkey_modulus_size, ca_modulus_size)
# The minimum good modulus size for RSA host keys is 3072. However, since ECC cryptosystems are fundamentally different, the minimum good is 256.
hostkey_min_good = cakey_min_good = 3072
hostkey_min_warn = cakey_min_warn = 2048
hostkey_warn_str = cakey_warn_str = HostKeyTest.TWO2K_MODULUS_WARNING
if host_key_type.startswith('ssh-ed25519') or host_key_type.startswith('ecdsa-sha2-nistp'):
hostkey_min_good = 256
hostkey_min_warn = 224
hostkey_warn_str = HostKeyTest.SMALL_ECC_MODULUS_WARNING
if ca_key_type.startswith('ssh-ed25519') or host_key_type.startswith('ecdsa-sha2-nistp'):
cakey_min_good = 256
cakey_min_warn = 224
cakey_warn_str = HostKeyTest.SMALL_ECC_MODULUS_WARNING
# Keys smaller than 2048 result in a failure. Keys smaller 3072 result in a warning. Update the database accordingly.
if (cert is False) and (hostkey_modulus_size < 3072):
for rsa_type in HostKeyTest.RSA_FAMILY:
alg_list = SSH2_KexDB.ALGORITHMS['key'][rsa_type]
# Ensure that failure & warning lists exist.
while len(alg_list) < 3:
alg_list.append([])
# If the key is under 2048, add to the failure list.
if hostkey_modulus_size < 2048:
alg_list[1].append('using small %d-bit modulus' % hostkey_modulus_size)
elif HostKeyTest.TWO2K_MODULUS_WARNING not in alg_list[2]: # Issue a warning about 2048-bit moduli.
alg_list[2].append(HostKeyTest.TWO2K_MODULUS_WARNING)
elif (cert is True) and ((hostkey_modulus_size < 3072) or (ca_modulus_size > 0 and ca_modulus_size < 3072)): # pylint: disable=chained-comparison
if (cert is False) and (hostkey_modulus_size < hostkey_min_good):
alg_list = SSH2_KexDB.ALGORITHMS['key'][host_key_type]
min_modulus = min(hostkey_modulus_size, ca_modulus_size)
min_modulus = min_modulus if min_modulus > 0 else max(hostkey_modulus_size, ca_modulus_size)
# Ensure that failure & warning lists exist.
while len(alg_list) < 3:
alg_list.append([])
if (hostkey_modulus_size < 2048) or (ca_modulus_size > 0 and ca_modulus_size < 2048): # pylint: disable=chained-comparison
alg_list[1].append('using small %d-bit modulus' % min_modulus)
elif HostKeyTest.TWO2K_MODULUS_WARNING not in alg_list[2]:
alg_list[2].append(HostKeyTest.TWO2K_MODULUS_WARNING)
# If the key is under 2048, add to the failure list.
if hostkey_modulus_size < hostkey_min_warn:
alg_list[1].append('using small %d-bit modulus' % hostkey_modulus_size)
elif hostkey_warn_str not in alg_list[2]: # Issue a warning about 2048-bit moduli.
alg_list[2].append(hostkey_warn_str)
elif (cert is True) and ((hostkey_modulus_size < hostkey_min_good) or (0 < ca_modulus_size < cakey_min_good)):
alg_list = SSH2_KexDB.ALGORITHMS['key'][host_key_type]
# Ensure that failure & warning lists exist.
while len(alg_list) < 3:
alg_list.append([])
# If the host key is smaller than 2048-bit/224-bit, flag this as a failure.
if hostkey_modulus_size < hostkey_min_warn:
alg_list[1].append('using small %d-bit hostkey modulus' % hostkey_modulus_size)
# Otherwise, this is just a warning.
elif (hostkey_modulus_size < hostkey_min_good) and (hostkey_warn_str not in alg_list[2]):
alg_list[2].append(hostkey_warn_str)
# If the CA key is smaller than 2048-bit/224-bit, flag this as a failure.
if 0 < ca_modulus_size < cakey_min_warn:
alg_list[1].append('using small %d-bit CA key modulus' % ca_modulus_size)
# Otherwise, this is just a warning.
elif (0 < ca_modulus_size < cakey_min_good) and (cakey_warn_str not in alg_list[2]):
alg_list[2].append(cakey_warn_str)
# If this host key type is in the RSA family, then mark them all as parsed (since results in one are valid for them all).
if host_key_type in HostKeyTest.RSA_FAMILY: