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.
- JSON 'target' field now always includes port number; credit [tomatohater1337](https://github.com/tomatohater1337).
- 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.
- SHA-1 algorithms now cause failures.
- CBC mode ciphers are now warnings instead of failures.

View File

@ -616,8 +616,8 @@ run_policy_test() {
exit 1
fi
#echo "Running: ./ssh-audit.py -P \"${policy_path}\" -jj localhost:2222 > ${test_result_json}"
./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}" 2> /dev/null
actual_exit_code=$?
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"

View File

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

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:

View File

@ -1,7 +1,7 @@
"""
The MIT License (MIT)
Copyright (C) 2017-2021 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017-2023 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
Permission is hereby granted, free of charge, to any person obtaining a copy
@ -31,6 +31,7 @@ import struct
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
from typing import Callable, Optional, Union, Any # noqa: F401
from ssh_audit.outputbuffer import OutputBuffer
from ssh_audit.protocol import Protocol
from ssh_audit.ssh_socket import SSH_Socket
@ -40,7 +41,8 @@ class KexDHException(Exception):
class KexDH: # pragma: nocover
def __init__(self, kex_name: str, hash_alg: str, g: int, p: int) -> None:
def __init__(self, out: 'OutputBuffer', kex_name: str, hash_alg: str, g: int, p: int) -> None:
self.out = out
self.__kex_name = kex_name # pylint: disable=unused-private-member
self.__hash_alg = hash_alg # pylint: disable=unused-private-member
self.__g = 0
@ -51,10 +53,11 @@ class KexDH: # pragma: nocover
self.set_params(g, p)
self.__ed25519_pubkey: Optional[bytes] = None # pylint: disable=unused-private-member
self.__hostkey_type: Optional[bytes] = None
self.__hostkey_type = ''
self.__hostkey_e = 0 # pylint: disable=unused-private-member
self.__hostkey_n = 0 # pylint: disable=unused-private-member
self.__hostkey_n_len = 0 # Length of the host key modulus.
self.__ca_key_type = '' # Type of CA key ('ssh-rsa', etc).
self.__ca_n_len = 0 # Length of the CA key modulus (if hostkey is a cert).
def set_params(self, g: int, p: int) -> None:
@ -76,6 +79,14 @@ class KexDH: # pragma: nocover
# contains the host key, among other things. Function returns the host
# key blob (from which the fingerprint can be calculated).
def recv_reply(self, s: 'SSH_Socket', parse_host_key_size: bool = True) -> Optional[bytes]:
# Reset the CA info, in case it was set from a prior invokation.
self.__hostkey_type = ''
self.__hostkey_e = 0 # pylint: disable=unused-private-member
self.__hostkey_n = 0 # pylint: disable=unused-private-member
self.__hostkey_n_len = 0
self.__ca_key_type = ''
self.__ca_n_len = 0
packet_type, payload = s.read_packet(2)
# Skip any & all MSG_DEBUG messages.
@ -88,23 +99,12 @@ class KexDH: # pragma: nocover
# A connection error occurred. We can't parse anything, so just
# return. The host key modulus (and perhaps certificate modulus)
# will remain at length 0.
self.out.d("KexDH.recv_reply(): received packge_type == -1.")
return None
hostkey_len = 0 # pylint: disable=unused-variable
hostkey_type_len = hostkey_e_len = 0 # pylint: disable=unused-variable
key_id_len = principles_len = 0 # pylint: disable=unused-variable
critical_options_len = extensions_len = 0 # pylint: disable=unused-variable
nonce_len = ca_key_len = ca_key_type_len = 0 # pylint: disable=unused-variable
ca_key_len = ca_key_type_len = ca_key_e_len = 0 # pylint: disable=unused-variable
key_id = principles = None # pylint: disable=unused-variable
critical_options = extensions = None # pylint: disable=unused-variable
nonce = ca_key = ca_key_type = None # pylint: disable=unused-variable
ca_key_e = ca_key_n = None # pylint: disable=unused-variable
# Get the host key blob, F, and signature.
ptr = 0
hostkey, hostkey_len, ptr = KexDH.__get_bytes(payload, ptr)
hostkey, _, ptr = KexDH.__get_bytes(payload, ptr)
# If we are not supposed to parse the host key size (i.e.: it is a type that is of fixed size such as ed25519), then stop here.
if not parse_host_key_size:
@ -116,76 +116,106 @@ class KexDH: # pragma: nocover
# Now pick apart the host key blob.
# Get the host key type (i.e.: 'ssh-rsa', 'ssh-ed25519', etc).
ptr = 0
self.__hostkey_type, hostkey_type_len, ptr = KexDH.__get_bytes(hostkey, ptr)
hostkey_type, _, ptr = KexDH.__get_bytes(hostkey, ptr)
self.__hostkey_type = hostkey_type.decode('ascii')
self.out.d("Parsing host key type: %s" % self.__hostkey_type)
# If this is an RSA certificate, skip over the nonce.
if self.__hostkey_type.startswith(b'ssh-rsa-cert-v0'):
nonce, nonce_len, ptr = KexDH.__get_bytes(hostkey, ptr)
if self.__hostkey_type.startswith('ssh-rsa-cert-v0'):
self.out.d("RSA certificate found, so skipping nonce.")
_, _, ptr = KexDH.__get_bytes(hostkey, ptr) # Read & skip over the nonce.
# The public key exponent.
hostkey_e, hostkey_e_len, ptr = KexDH.__get_bytes(hostkey, ptr)
hostkey_e, _, ptr = KexDH.__get_bytes(hostkey, ptr)
self.__hostkey_e = int(binascii.hexlify(hostkey_e), 16) # pylint: disable=unused-private-member
# Here is the modulus size & actual modulus of the host key public key.
hostkey_n, self.__hostkey_n_len, ptr = KexDH.__get_bytes(hostkey, ptr)
self.__hostkey_n = int(binascii.hexlify(hostkey_n), 16) # pylint: disable=unused-private-member
# ED25519 moduli are fixed at 32 bytes.
if self.__hostkey_type == 'ssh-ed25519':
self.out.d("%s has a fixed host key modulus of 32." % self.__hostkey_type)
self.__hostkey_n_len = 32
else:
# Here is the modulus size & actual modulus of the host key public key.
hostkey_n, self.__hostkey_n_len, ptr = KexDH.__get_bytes(hostkey, ptr)
self.__hostkey_n = int(binascii.hexlify(hostkey_n), 16) # pylint: disable=unused-private-member
# If this is an RSA certificate, continue parsing to extract the CA
# key.
if self.__hostkey_type.startswith(b'ssh-rsa-cert-v0'):
# Skip over the serial number.
ptr += 8
# Get the certificate type.
cert_type = int(binascii.hexlify(hostkey[ptr:ptr + 4]), 16)
ptr += 4
# Only SSH2_CERT_TYPE_HOST (2) makes sense in this context.
if cert_type == 2:
# Skip the key ID (this is the serial number of the
# certificate).
key_id, key_id_len, ptr = KexDH.__get_bytes(hostkey, ptr)
# The principles, which are... I don't know what.
principles, principles_len, ptr = KexDH.__get_bytes(hostkey, ptr)
# Skip over the timestamp that this certificate is valid after.
ptr += 8
# Skip over the timestamp that this certificate is valid before.
ptr += 8
# TODO: validate the principles, and time range.
# The critical options.
critical_options, critical_options_len, ptr = KexDH.__get_bytes(hostkey, ptr)
# Certificate extensions.
extensions, extensions_len, ptr = KexDH.__get_bytes(hostkey, ptr)
# Another nonce.
nonce, nonce_len, ptr = KexDH.__get_bytes(hostkey, ptr)
# Finally, we get to the CA key.
ca_key, ca_key_len, ptr = KexDH.__get_bytes(hostkey, ptr)
# Last in the host key blob is the CA signature. It isn't
# interesting to us, so we won't bother parsing any further.
# The CA key has the modulus, however...
ptr = 0
# 'ssh-rsa', 'rsa-sha2-256', etc.
ca_key_type, ca_key_type_len, ptr = KexDH.__get_bytes(ca_key, ptr)
# CA's public key exponent.
ca_key_e, ca_key_e_len, ptr = KexDH.__get_bytes(ca_key, ptr)
# CA's modulus. Bingo.
ca_key_n, self.__ca_n_len, ptr = KexDH.__get_bytes(ca_key, ptr)
# If this is a certificate, continue parsing to extract the CA type and key length. Even though a hostkey type might be 'ssh-ed25519-cert-v01@openssh.com', its CA may still be RSA.
if self.__hostkey_type.startswith('ssh-rsa-cert-v0') or self.__hostkey_type.startswith('ssh-ed25519-cert-v0'):
# Get the CA key type and key length.
self.__ca_key_type, self.__ca_n_len = self.__parse_ca_key(hostkey, self.__hostkey_type, ptr)
self.out.d("KexDH.__parse_ca_key(): CA key type: [%s]; CA key length: %u" % (self.__ca_key_type, self.__ca_n_len))
return hostkey
def __parse_ca_key(self, hostkey: bytes, hostkey_type: str, ptr: int) -> Tuple[str, int]:
ca_key_type = ''
ca_key_n_len = 0
# If this is a certificate, continue parsing to extract the CA type and key length. Even though a hostkey type might be 'ssh-ed25519-cert-v01@openssh.com', its CA may still be RSA.
# if hostkey_type.startswith('ssh-rsa-cert-v0') or hostkey_type.startswith('ssh-ed25519-cert-v0'):
self.out.d("Parsing CA for hostkey type [%s]..." % hostkey_type)
# Skip over the serial number.
ptr += 8
# Get the certificate type.
cert_type = int(binascii.hexlify(hostkey[ptr:ptr + 4]), 16)
ptr += 4
# Only SSH2_CERT_TYPE_HOST (2) makes sense in this context.
if cert_type == 2:
# Skip the key ID (this is the serial number of the
# certificate).
key_id, key_id_len, ptr = KexDH.__get_bytes(hostkey, ptr) # pylint: disable=unused-variable
# The principles, which are... I don't know what.
principles, principles_len, ptr = KexDH.__get_bytes(hostkey, ptr) # pylint: disable=unused-variable
# Skip over the timestamp that this certificate is valid after.
ptr += 8
# Skip over the timestamp that this certificate is valid before.
ptr += 8
# TODO: validate the principles, and time range.
# The critical options.
critical_options, critical_options_len, ptr = KexDH.__get_bytes(hostkey, ptr) # pylint: disable=unused-variable
# Certificate extensions.
extensions, extensions_len, ptr = KexDH.__get_bytes(hostkey, ptr) # pylint: disable=unused-variable
# Another nonce.
nonce, nonce_len, ptr = KexDH.__get_bytes(hostkey, ptr) # pylint: disable=unused-variable
# Finally, we get to the CA key.
ca_key, ca_key_len, ptr = KexDH.__get_bytes(hostkey, ptr) # pylint: disable=unused-variable
# Last in the host key blob is the CA signature. It isn't
# interesting to us, so we won't bother parsing any further.
# The CA key has the modulus, however...
ptr = 0
# 'ssh-rsa', 'rsa-sha2-256', etc.
ca_key_type_bytes, ca_key_type_len, ptr = KexDH.__get_bytes(ca_key, ptr) # pylint: disable=unused-variable
ca_key_type = ca_key_type_bytes.decode('ascii')
self.out.d("Found CA type: [%s]" % ca_key_type)
# ED25519 CA's don't explicitly include the modulus size in the public key, since its fixed at 32 in all cases.
if ca_key_type == 'ssh-ed25519':
ca_key_n_len = 32
else:
# CA's public key exponent.
ca_key_e, ca_key_e_len, ptr = KexDH.__get_bytes(ca_key, ptr) # pylint: disable=unused-variable
# CA's modulus. Bingo.
ca_key_n, ca_key_n_len, ptr = KexDH.__get_bytes(ca_key, ptr) # pylint: disable=unused-variable
else:
self.out.d("Certificate type %u found; this is not usually valid in the context of a host key! Skipping it..." % cert_type)
return ca_key_type, ca_key_n_len
@staticmethod
def __get_bytes(buf: bytes, ptr: int) -> Tuple[bytes, int, int]:
num_bytes = struct.unpack('>I', buf[ptr:ptr + 4])[0]
@ -205,10 +235,18 @@ class KexDH: # pragma: nocover
size = size - 8
return size
# Returns the hostkey type.
def get_hostkey_type(self) -> str:
return self.__hostkey_type
# Returns the size of the hostkey, in bits.
def get_hostkey_size(self) -> int:
return KexDH.__adjust_key_size(self.__hostkey_n_len)
# Returns the CA type ('ssh-rsa', 'ssh-ed25519', etc).
def get_ca_type(self) -> str:
return self.__ca_key_type
# Returns the size of the CA key, in bits.
def get_ca_size(self) -> int:
return KexDH.__adjust_key_size(self.__ca_n_len)
@ -220,46 +258,46 @@ class KexDH: # pragma: nocover
class KexGroup1(KexDH): # pragma: nocover
def __init__(self) -> None:
def __init__(self, out: 'OutputBuffer') -> None:
# rfc2409: second oakley group
p = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece65381ffffffffffffffff', 16)
super(KexGroup1, self).__init__('KexGroup1', 'sha1', 2, p)
super(KexGroup1, self).__init__(out, 'KexGroup1', 'sha1', 2, p)
class KexGroup14(KexDH): # pragma: nocover
def __init__(self, hash_alg: str) -> None:
def __init__(self, out: 'OutputBuffer', hash_alg: str) -> None:
# rfc3526: 2048-bit modp group
p = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff', 16)
super(KexGroup14, self).__init__('KexGroup14', hash_alg, 2, p)
super(KexGroup14, self).__init__(out, 'KexGroup14', hash_alg, 2, p)
class KexGroup14_SHA1(KexGroup14):
def __init__(self) -> None:
super(KexGroup14_SHA1, self).__init__('sha1')
def __init__(self, out: 'OutputBuffer') -> None:
super(KexGroup14_SHA1, self).__init__(out, 'sha1')
class KexGroup14_SHA256(KexGroup14):
def __init__(self) -> None:
super(KexGroup14_SHA256, self).__init__('sha256')
def __init__(self, out: 'OutputBuffer') -> None:
super(KexGroup14_SHA256, self).__init__(out, 'sha256')
class KexGroup16_SHA512(KexDH):
def __init__(self) -> None:
def __init__(self, out: 'OutputBuffer') -> None:
# rfc3526: 4096-bit modp group
p = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aaac42dad33170d04507a33a85521abdf1cba64ecfb850458dbef0a8aea71575d060c7db3970f85a6e1e4c7abf5ae8cdb0933d71e8c94e04a25619dcee3d2261ad2ee6bf12ffa06d98a0864d87602733ec86a64521f2b18177b200cbbe117577a615d6c770988c0bad946e208e24fa074e5ab3143db5bfce0fd108e4b82d120a92108011a723c12a787e6d788719a10bdba5b2699c327186af4e23c1a946834b6150bda2583e9ca2ad44ce8dbbbc2db04de8ef92e8efc141fbecaa6287c59474e6bc05d99b2964fa090c3a2233ba186515be7ed1f612970cee2d7afb81bdd762170481cd0069127d5b05aa993b4ea988d8fddc186ffb7dc90a6c08f4df435c934063199ffffffffffffffff', 16)
super(KexGroup16_SHA512, self).__init__('KexGroup16_SHA512', 'sha512', 2, p)
super(KexGroup16_SHA512, self).__init__(out, 'KexGroup16_SHA512', 'sha512', 2, p)
class KexGroup18_SHA512(KexDH):
def __init__(self) -> None:
def __init__(self, out: 'OutputBuffer') -> None:
# rfc3526: 8192-bit modp group
p = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aaac42dad33170d04507a33a85521abdf1cba64ecfb850458dbef0a8aea71575d060c7db3970f85a6e1e4c7abf5ae8cdb0933d71e8c94e04a25619dcee3d2261ad2ee6bf12ffa06d98a0864d87602733ec86a64521f2b18177b200cbbe117577a615d6c770988c0bad946e208e24fa074e5ab3143db5bfce0fd108e4b82d120a92108011a723c12a787e6d788719a10bdba5b2699c327186af4e23c1a946834b6150bda2583e9ca2ad44ce8dbbbc2db04de8ef92e8efc141fbecaa6287c59474e6bc05d99b2964fa090c3a2233ba186515be7ed1f612970cee2d7afb81bdd762170481cd0069127d5b05aa993b4ea988d8fddc186ffb7dc90a6c08f4df435c93402849236c3fab4d27c7026c1d4dcb2602646dec9751e763dba37bdf8ff9406ad9e530ee5db382f413001aeb06a53ed9027d831179727b0865a8918da3edbebcf9b14ed44ce6cbaced4bb1bdb7f1447e6cc254b332051512bd7af426fb8f401378cd2bf5983ca01c64b92ecf032ea15d1721d03f482d7ce6e74fef6d55e702f46980c82b5a84031900b1c9e59e7c97fbec7e8f323a97a7e36cc88be0f1d45b7ff585ac54bd407b22b4154aacc8f6d7ebf48e1d814cc5ed20f8037e0a79715eef29be32806a1d58bb7c5da76f550aa3d8a1fbff0eb19ccb1a313d55cda56c9ec2ef29632387fe8d76e3c0468043e8f663f4860ee12bf2d5b0b7474d6e694f91e6dbe115974a3926f12fee5e438777cb6a932df8cd8bec4d073b931ba3bc832b68d9dd300741fa7bf8afc47ed2576f6936ba424663aab639c5ae4f5683423b4742bf1c978238f16cbe39d652de3fdb8befc848ad922222e04a4037c0713eb57a81a23f0c73473fc646cea306b4bcbc8862f8385ddfa9d4b7fa2c087e879683303ed5bdd3a062b3cf5b3a278a66d2a13f83f44f82ddf310ee074ab6a364597e899a0255dc164f31cc50846851df9ab48195ded7ea1b1d510bd7ee74d73faf36bc31ecfa268359046f4eb879f924009438b481c6cd7889a002ed5ee382bc9190da6fc026e479558e4475677e9aa9e3050e2765694dfc81f56e880b96e7160c980dd98edd3dfffffffffffffffff', 16)
super(KexGroup18_SHA512, self).__init__('KexGroup18_SHA512', 'sha512', 2, p)
super(KexGroup18_SHA512, self).__init__(out, 'KexGroup18_SHA512', 'sha512', 2, p)
class KexCurve25519_SHA256(KexDH):
def __init__(self) -> None:
super(KexCurve25519_SHA256, self).__init__('KexCurve25519_SHA256', 'sha256', 0, 0)
def __init__(self, out: 'OutputBuffer') -> None:
super(KexCurve25519_SHA256, self).__init__(out, 'KexCurve25519_SHA256', 'sha256', 0, 0)
# To start an ED25519 kex, we simply send a random 256-bit number as the
# public key.
@ -271,8 +309,8 @@ class KexCurve25519_SHA256(KexDH):
class KexNISTP256(KexDH):
def __init__(self) -> None:
super(KexNISTP256, self).__init__('KexNISTP256', 'sha256', 0, 0)
def __init__(self, out: 'OutputBuffer') -> None:
super(KexNISTP256, self).__init__(out, 'KexNISTP256', 'sha256', 0, 0)
# Because the server checks that the value sent here is valid (i.e.: it lies
# on the curve, among other things), we would have to write a lot of code
@ -286,8 +324,8 @@ class KexNISTP256(KexDH):
class KexNISTP384(KexDH):
def __init__(self) -> None:
super(KexNISTP384, self).__init__('KexNISTP384', 'sha256', 0, 0)
def __init__(self, out: 'OutputBuffer') -> None:
super(KexNISTP384, self).__init__(out, 'KexNISTP384', 'sha256', 0, 0)
# See comment for KexNISTP256.send_init().
def send_init(self, s: 'SSH_Socket', init_msg: int = Protocol.MSG_KEXDH_INIT) -> None:
@ -297,8 +335,8 @@ class KexNISTP384(KexDH):
class KexNISTP521(KexDH):
def __init__(self) -> None:
super(KexNISTP521, self).__init__('KexNISTP521', 'sha256', 0, 0)
def __init__(self, out: 'OutputBuffer') -> None:
super(KexNISTP521, self).__init__(out, 'KexNISTP521', 'sha256', 0, 0)
# See comment for KexNISTP256.send_init().
def send_init(self, s: 'SSH_Socket', init_msg: int = Protocol.MSG_KEXDH_INIT) -> None:
@ -308,8 +346,8 @@ class KexNISTP521(KexDH):
class KexGroupExchange(KexDH):
def __init__(self, classname: str, hash_alg: str) -> None:
super(KexGroupExchange, self).__init__(classname, hash_alg, 0, 0)
def __init__(self, out: 'OutputBuffer', classname: str, hash_alg: str) -> None:
super(KexGroupExchange, self).__init__(out, classname, hash_alg, 0, 0)
def send_init(self, s: 'SSH_Socket', init_msg: int = Protocol.MSG_KEXDH_GEX_REQUEST) -> None:
self.send_init_gex(s)
@ -358,10 +396,10 @@ class KexGroupExchange(KexDH):
class KexGroupExchange_SHA1(KexGroupExchange):
def __init__(self) -> None:
super(KexGroupExchange_SHA1, self).__init__('KexGroupExchange_SHA1', 'sha1')
def __init__(self, out: 'OutputBuffer') -> None:
super(KexGroupExchange_SHA1, self).__init__(out, 'KexGroupExchange_SHA1', 'sha1')
class KexGroupExchange_SHA256(KexGroupExchange):
def __init__(self) -> None:
super(KexGroupExchange_SHA256, self).__init__('KexGroupExchange_SHA256', 'sha256')
def __init__(self, out: 'OutputBuffer') -> None:
super(KexGroupExchange_SHA256, self).__init__(out, 'KexGroupExchange_SHA256', 'sha256')

View File

@ -21,6 +21,8 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import copy
import json
import sys
from typing import Dict, List, Tuple
@ -28,9 +30,9 @@ from typing import Optional, Any, Union, cast
from datetime import date
from ssh_audit import exitcodes
from ssh_audit.ssh2_kex import SSH2_Kex # pylint: disable=unused-import
from ssh_audit.banner import Banner # pylint: disable=unused-import
from ssh_audit.banner import Banner
from ssh_audit.globals import SNAP_PACKAGE, SNAP_PERMISSIONS_ERROR
from ssh_audit.ssh2_kex import SSH2_Kex
# Validates policy files and performs policy testing
@ -87,8 +89,9 @@ class Policy:
}
WARNING_DEPRECATED_DIRECTIVES = "\nWARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option.\n"
def __init__(self, policy_file: Optional[str] = None, policy_data: Optional[str] = None, manual_load: bool = False) -> None:
def __init__(self, policy_file: Optional[str] = None, policy_data: Optional[str] = None, manual_load: bool = False, json_output: bool = False) -> None:
self._name: Optional[str] = None
self._version: Optional[str] = None
self._banner: Optional[str] = None
@ -98,13 +101,19 @@ class Policy:
self._kex: Optional[List[str]] = None
self._ciphers: Optional[List[str]] = None
self._macs: Optional[List[str]] = None
self._hostkey_sizes: Optional[Dict[str, int]] = None
self._hostkey_sizes: Optional[Dict[str, Dict[str, Union[int, str, bytes]]]] = None
self._cakey_sizes: Optional[Dict[str, int]] = None
self._dh_modulus_sizes: Optional[Dict[str, int]] = None
self._server_policy = True
self._name_and_version: str = ''
# If invoked while JSON output is expected, send warnings to stderr instead of stdout (which would corrupt the JSON output).
if json_output:
self._warning_target = sys.stderr
else:
self._warning_target = sys.stdout
# Ensure that only one mode was specified.
num_modes = 0
if policy_file is not None:
@ -154,7 +163,7 @@ class Policy:
key = key.strip()
val = val.strip()
if key not in ['name', 'version', 'banner', 'compressions', 'host keys', 'optional host keys', 'key exchanges', 'ciphers', 'macs', 'client policy'] and not key.startswith('hostkey_size_') and not key.startswith('cakey_size_') and not key.startswith('dh_modulus_size_'):
if key not in ['name', 'version', 'banner', 'compressions', 'host keys', 'optional host keys', 'key exchanges', 'ciphers', 'macs', 'client policy', 'host_key_sizes', 'dh_modulus_sizes'] and not key.startswith('hostkey_size_') and not key.startswith('cakey_size_') and not key.startswith('dh_modulus_size_'):
raise ValueError("invalid field found in policy: %s" % line)
if key in ['name', 'banner']:
@ -173,8 +182,10 @@ class Policy:
self._name = val
elif key == 'banner':
self._banner = val
elif key == 'version':
self._version = val
elif key in ['compressions', 'host keys', 'optional host keys', 'key exchanges', 'ciphers', 'macs']:
try:
algs = val.split(',')
@ -197,21 +208,59 @@ class Policy:
self._ciphers = algs
elif key == 'macs':
self._macs = algs
elif key.startswith('hostkey_size_'):
elif key.startswith('hostkey_size_'): # Old host key size format.
print(Policy.WARNING_DEPRECATED_DIRECTIVES, file=self._warning_target) # Warn the user that the policy file is using deprecated directives.
hostkey_type = key[13:]
hostkey_size = int(val)
if self._hostkey_sizes is None:
self._hostkey_sizes = {}
self._hostkey_sizes[hostkey_type] = int(val)
elif key.startswith('cakey_size_'):
cakey_type = key[11:]
if self._cakey_sizes is None:
self._cakey_sizes = {}
self._cakey_sizes[cakey_type] = int(val)
elif key.startswith('dh_modulus_size_'):
dh_modulus_type = key[16:]
self._hostkey_sizes[hostkey_type] = {'hostkey_size': hostkey_size, 'ca_key_type': '', 'ca_key_size': 0}
elif key.startswith('cakey_size_'): # Old host key size format.
print(Policy.WARNING_DEPRECATED_DIRECTIVES, file=self._warning_target) # Warn the user that the policy file is using deprecated directives.
hostkey_type = key[11:]
ca_key_size = int(val)
ca_key_type = 'ssh-ed25519'
if hostkey_type in ['ssh-rsa-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com']:
ca_key_type = 'ssh-rsa'
if self._hostkey_sizes is None:
self._hostkey_sizes = {}
self._hostkey_sizes[hostkey_type] = {'hostkey_size': hostkey_size, 'ca_key_type': ca_key_type, 'ca_key_size': ca_key_size}
elif key == 'host_key_sizes': # New host key size format.
self._hostkey_sizes = json.loads(val)
# Fill in the trimmed fields that were omitted from the policy.
if self._hostkey_sizes is not None:
for host_key_type in self._hostkey_sizes:
if 'ca_key_type' not in self._hostkey_sizes[host_key_type]:
self._hostkey_sizes[host_key_type]['ca_key_type'] = ''
if 'ca_key_size' not in self._hostkey_sizes[host_key_type]:
self._hostkey_sizes[host_key_type]['ca_key_size'] = 0
if 'raw_hostkey_bytes' not in self._hostkey_sizes[host_key_type]:
self._hostkey_sizes[host_key_type]['raw_hostkey_bytes'] = b''
elif key.startswith('dh_modulus_size_'): # Old DH modulus format.
print(Policy.WARNING_DEPRECATED_DIRECTIVES, file=self._warning_target) # Warn the user that the policy file is using deprecated directives.
dh_type = key[16:]
dh_size = int(val)
if self._dh_modulus_sizes is None:
self._dh_modulus_sizes = {}
self._dh_modulus_sizes[dh_modulus_type] = int(val)
self._dh_modulus_sizes[dh_type] = dh_size
elif key == 'dh_modulus_sizes': # New DH modulus format.
self._dh_modulus_sizes = json.loads(val)
elif key.startswith('client policy') and val.lower() == 'true':
self._server_policy = False
@ -243,10 +292,9 @@ class Policy:
kex_algs = None
ciphers = None
macs = None
rsa_hostkey_sizes_str = ''
rsa_cakey_sizes_str = ''
dh_modulus_sizes_str = ''
client_policy_str = ''
host_keys_json = ''
if client_audit:
client_policy_str = "\n# Set to true to signify this is a policy for clients, not servers.\nclient policy = true\n"
@ -262,26 +310,23 @@ class Policy:
ciphers = ', '.join(kex.server.encryption)
if kex.server.mac is not None:
macs = ', '.join(kex.server.mac)
if kex.rsa_key_sizes():
rsa_key_sizes_dict = kex.rsa_key_sizes()
for host_key_type in sorted(rsa_key_sizes_dict):
hostkey_size, cakey_size = rsa_key_sizes_dict[host_key_type]
rsa_hostkey_sizes_str = "%shostkey_size_%s = %d\n" % (rsa_hostkey_sizes_str, host_key_type, hostkey_size)
if cakey_size != -1:
rsa_cakey_sizes_str = "%scakey_size_%s = %d\n" % (rsa_cakey_sizes_str, host_key_type, cakey_size)
if kex.host_keys():
# Make a deep copy of the host keys dict, then delete all the raw hostkey bytes from the copy.
host_keys_trimmed = copy.deepcopy(kex.host_keys())
for hostkey_alg in host_keys_trimmed:
del host_keys_trimmed[hostkey_alg]['raw_hostkey_bytes']
# Delete the CA signature if any of its fields are empty.
if host_keys_trimmed[hostkey_alg]['ca_key_type'] == '' or host_keys_trimmed[hostkey_alg]['ca_key_size'] == 0:
del host_keys_trimmed[hostkey_alg]['ca_key_type']
del host_keys_trimmed[hostkey_alg]['ca_key_size']
host_keys_json = "\n# Dictionary containing all host key and size information. Optionally contains the certificate authority's signature algorithm ('ca_key_type') and signature length ('ca_key_size'), if any.\nhost_key_sizes = %s\n" % json.dumps(host_keys_trimmed)
if len(rsa_hostkey_sizes_str) > 0:
rsa_hostkey_sizes_str = "\n# RSA host key sizes.\n%s" % rsa_hostkey_sizes_str
if len(rsa_cakey_sizes_str) > 0:
rsa_cakey_sizes_str = "\n# RSA CA key sizes.\n%s" % rsa_cakey_sizes_str
if kex.dh_modulus_sizes():
dh_modulus_sizes_dict = kex.dh_modulus_sizes()
for gex_type in sorted(dh_modulus_sizes_dict):
modulus_size, _ = dh_modulus_sizes_dict[gex_type]
dh_modulus_sizes_str = "%sdh_modulus_size_%s = %d\n" % (dh_modulus_sizes_str, gex_type, modulus_size)
if len(dh_modulus_sizes_str) > 0:
dh_modulus_sizes_str = "\n# Group exchange DH modulus sizes.\n%s" % dh_modulus_sizes_str
dh_modulus_sizes_str = "\n# Group exchange DH modulus sizes.\ndh_modulus_sizes = %s\n" % json.dumps(kex.dh_modulus_sizes())
policy_data = '''#
@ -299,7 +344,7 @@ version = 1
# The compression options that must match exactly (order matters). Commented out to ignore by default.
# compressions = %s
%s%s%s
%s%s
# The host key types that must match exactly (order matters).
host keys = %s
@ -314,7 +359,7 @@ ciphers = %s
# The MACs that must match exactly (order matters).
macs = %s
''' % (source, today, client_policy_str, source, today, banner, compressions, rsa_hostkey_sizes_str, rsa_cakey_sizes_str, dh_modulus_sizes_str, host_keys, kex_algs, ciphers, macs)
''' % (source, today, client_policy_str, source, today, banner, compressions, host_keys_json, dh_modulus_sizes_str, host_keys, kex_algs, ciphers, macs)
return policy_data
@ -351,23 +396,29 @@ macs = %s
hostkey_types = list(self._hostkey_sizes.keys())
hostkey_types.sort() # Sorted to make testing output repeatable.
for hostkey_type in hostkey_types:
expected_hostkey_size = self._hostkey_sizes[hostkey_type]
if hostkey_type in kex.rsa_key_sizes():
actual_hostkey_size, actual_cakey_size = kex.rsa_key_sizes()[hostkey_type]
expected_hostkey_size = self._hostkey_sizes[hostkey_type]['hostkey_size']
server_host_keys = kex.host_keys()
if hostkey_type in server_host_keys:
actual_hostkey_size = server_host_keys[hostkey_type]['hostkey_size']
if actual_hostkey_size != expected_hostkey_size:
ret = False
self._append_error(errors, 'RSA host key (%s) sizes' % hostkey_type, [str(expected_hostkey_size)], None, [str(actual_hostkey_size)])
self._append_error(errors, 'Host key (%s) sizes' % hostkey_type, [str(expected_hostkey_size)], None, [str(actual_hostkey_size)])
if self._cakey_sizes is not None:
hostkey_types = list(self._cakey_sizes.keys())
hostkey_types.sort() # Sorted to make testing output repeatable.
for hostkey_type in hostkey_types:
expected_cakey_size = self._cakey_sizes[hostkey_type]
if hostkey_type in kex.rsa_key_sizes():
actual_hostkey_size, actual_cakey_size = kex.rsa_key_sizes()[hostkey_type]
if actual_cakey_size != expected_cakey_size:
ret = False
self._append_error(errors, 'RSA CA key (%s) sizes' % hostkey_type, [str(expected_cakey_size)], None, [str(actual_cakey_size)])
# If we have expected CA signatures set, check them against what the server returned.
if self._hostkey_sizes is not None and len(cast(str, self._hostkey_sizes[hostkey_type]['ca_key_type'])) > 0 and cast(int, self._hostkey_sizes[hostkey_type]['ca_key_size']) > 0:
expected_ca_key_type = cast(str, self._hostkey_sizes[hostkey_type]['ca_key_type'])
expected_ca_key_size = cast(int, self._hostkey_sizes[hostkey_type]['ca_key_size'])
actual_ca_key_type = cast(str, server_host_keys[hostkey_type]['ca_key_type'])
actual_ca_key_size = cast(int, server_host_keys[hostkey_type]['ca_key_size'])
# Ensure that the CA signature type is what's expected (i.e.: the server doesn't have an RSA sig when we're expecting an ED25519 sig).
if actual_ca_key_type != expected_ca_key_type:
ret = False
self._append_error(errors, 'CA signature type', [expected_ca_key_type], None, [actual_ca_key_type])
# Ensure that the actual and expected signature sizes match.
elif actual_ca_key_size != expected_ca_key_size:
ret = False
self._append_error(errors, 'CA signature size (%s)' % actual_ca_key_type, [str(expected_ca_key_size)], None, [str(actual_ca_key_size)])
if kex.kex_algorithms != self._kex:
ret = False
@ -387,7 +438,7 @@ macs = %s
for dh_modulus_type in dh_modulus_types:
expected_dh_modulus_size = self._dh_modulus_sizes[dh_modulus_type]
if dh_modulus_type in kex.dh_modulus_sizes():
actual_dh_modulus_size, _ = kex.dh_modulus_sizes()[dh_modulus_type]
actual_dh_modulus_size = kex.dh_modulus_sizes()[dh_modulus_type]
if expected_dh_modulus_size != actual_dh_modulus_size:
ret = False
self._append_error(errors, 'Group exchange (%s) modulus sizes' % dh_modulus_type, [str(expected_dh_modulus_size)], None, [str(actual_dh_modulus_size)])
@ -449,12 +500,12 @@ macs = %s
@staticmethod
def load_builtin_policy(policy_name: str) -> Optional['Policy']:
def load_builtin_policy(policy_name: str, json_output: bool = False) -> Optional['Policy']:
'''Returns a Policy with the specified built-in policy name loaded, or None if no policy of that name exists.'''
p = None
if policy_name in Policy.BUILTIN_POLICIES:
policy_struct = Policy.BUILTIN_POLICIES[policy_name]
p = Policy(manual_load=True)
p = Policy(manual_load=True, json_output=json_output)
policy_name_without_version = policy_name[0:policy_name.rfind(' (')]
p._name = policy_name_without_version # pylint: disable=protected-access
p._version = cast(str, policy_struct['version']) # pylint: disable=protected-access
@ -465,7 +516,7 @@ macs = %s
p._kex = cast(Optional[List[str]], policy_struct['kex']) # pylint: disable=protected-access
p._ciphers = cast(Optional[List[str]], policy_struct['ciphers']) # pylint: disable=protected-access
p._macs = cast(Optional[List[str]], policy_struct['macs']) # pylint: disable=protected-access
p._hostkey_sizes = cast(Optional[Dict[str, int]], policy_struct['hostkey_sizes']) # pylint: disable=protected-access
p._hostkey_sizes = cast(Optional[Dict[str, Dict[str, Union[int, str, bytes]]]], policy_struct['hostkey_sizes']) # pylint: disable=protected-access
p._cakey_sizes = cast(Optional[Dict[str, int]], policy_struct['cakey_sizes']) # pylint: disable=protected-access
p._dh_modulus_sizes = cast(Optional[Dict[str, int]], policy_struct['dh_modulus_sizes']) # pylint: disable=protected-access
p._server_policy = cast(bool, policy_struct['server_policy']) # pylint: disable=protected-access

View File

@ -22,17 +22,18 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
# pylint: disable=unused-import
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
from typing import Callable, Optional, Union, Any # noqa: F401
from typing import Dict, List
from typing import Union
from ssh_audit.ssh2_kexparty import SSH2_KexParty
from ssh_audit.outputbuffer import OutputBuffer
from ssh_audit.readbuf import ReadBuf
from ssh_audit.ssh2_kexparty import SSH2_KexParty
from ssh_audit.writebuf import WriteBuf
class SSH2_Kex:
def __init__(self, cookie: bytes, kex_algs: List[str], key_algs: List[str], cli: 'SSH2_KexParty', srv: 'SSH2_KexParty', follows: bool, unused: int = 0) -> None:
def __init__(self, outputbuffer: 'OutputBuffer', cookie: bytes, kex_algs: List[str], key_algs: List[str], cli: 'SSH2_KexParty', srv: 'SSH2_KexParty', follows: bool, unused: int = 0) -> None: # pylint: disable=too-many-arguments
self.__outputbuffer = outputbuffer
self.__cookie = cookie
self.__kex_algs = kex_algs
self.__key_algs = key_algs
@ -41,9 +42,8 @@ class SSH2_Kex:
self.__follows = follows
self.__unused = unused
self.__rsa_key_sizes: Dict[str, Tuple[int, int]] = {}
self.__dh_modulus_sizes: Dict[str, Tuple[int, int]] = {}
self.__host_keys: Dict[str, bytes] = {}
self.__dh_modulus_sizes: Dict[str, int] = {}
self.__host_keys: Dict[str, Dict[str, Union[bytes, str, int]]] = {}
@property
def cookie(self) -> bytes:
@ -75,22 +75,20 @@ class SSH2_Kex:
def unused(self) -> int:
return self.__unused
def set_rsa_key_size(self, rsa_type: str, hostkey_size: int, ca_size: int = -1) -> None:
self.__rsa_key_sizes[rsa_type] = (hostkey_size, ca_size)
def rsa_key_sizes(self) -> Dict[str, Tuple[int, int]]:
return self.__rsa_key_sizes
def set_dh_modulus_size(self, gex_alg: str, modulus_size: int) -> None:
self.__dh_modulus_sizes[gex_alg] = (modulus_size, -1)
self.__dh_modulus_sizes[gex_alg] = modulus_size
def dh_modulus_sizes(self) -> Dict[str, Tuple[int, int]]:
def dh_modulus_sizes(self) -> Dict[str, int]:
return self.__dh_modulus_sizes
def set_host_key(self, key_type: str, hostkey: bytes) -> None:
self.__host_keys[key_type] = hostkey
def set_host_key(self, key_type: str, raw_hostkey_bytes: bytes, hostkey_size: int, ca_key_type: str, ca_key_size: int) -> None:
def host_keys(self) -> Dict[str, bytes]:
if key_type not in self.__host_keys:
self.__host_keys[key_type] = {'raw_hostkey_bytes': raw_hostkey_bytes, 'hostkey_size': hostkey_size, 'ca_key_type': ca_key_type, 'ca_key_size': ca_key_size}
else: # A host key may only have one CA signature...
self.__outputbuffer.d("WARNING: called SSH2_Kex.set_host_key() multiple times with the same host key type (%s)! Existing info: %r, %r, %r; Duplicate (ignored) info: %r, %r, %r" % (key_type, self.__host_keys[key_type]['hostkey_size'], self.__host_keys[key_type]['ca_key_type'], self.__host_keys[key_type]['ca_key_size'], hostkey_size, ca_key_type, ca_key_size))
def host_keys(self) -> Dict[str, Dict[str, Union[bytes, str, int]]]:
return self.__host_keys
def write(self, wbuf: 'WriteBuf') -> None:
@ -115,7 +113,7 @@ class SSH2_Kex:
return wbuf.write_flush()
@classmethod
def parse(cls, payload: bytes) -> 'SSH2_Kex':
def parse(cls, outputbuffer: 'OutputBuffer', payload: bytes) -> 'SSH2_Kex':
buf = ReadBuf(payload)
cookie = buf.read(16)
kex_algs = buf.read_list()
@ -132,5 +130,5 @@ class SSH2_Kex:
unused = buf.read_int()
cli = SSH2_KexParty(cli_enc, cli_mac, cli_compression, cli_languages)
srv = SSH2_KexParty(srv_enc, srv_mac, srv_compression, srv_languages)
kex = cls(cookie, kex_algs, key_algs, cli, srv, follows, unused)
kex = cls(outputbuffer, cookie, kex_algs, key_algs, cli, srv, follows, unused)
return kex

View File

@ -34,7 +34,7 @@ import traceback
# pylint: disable=unused-import
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
from typing import Callable, Optional, Union, Any # noqa: F401
from typing import cast, Callable, Optional, Union, Any # noqa: F401
from ssh_audit.globals import SNAP_PACKAGE
from ssh_audit.globals import SNAP_PERMISSIONS_ERROR
@ -107,10 +107,10 @@ def usage(uout: OutputBuffer, err: Optional[str] = None) -> None:
sys.exit(retval)
def output_algorithms(out: OutputBuffer, title: str, alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], alg_type: str, algorithms: List[str], unknown_algs: List[str], is_json_output: bool, program_retval: int, maxlen: int = 0, alg_sizes: Optional[Dict[str, Tuple[int, int]]] = None) -> int: # pylint: disable=too-many-arguments
def output_algorithms(out: OutputBuffer, title: str, alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], alg_type: str, algorithms: List[str], unknown_algs: List[str], is_json_output: bool, program_retval: int, maxlen: int = 0, host_keys: Optional[Dict[str, Dict[str, Union[bytes, str, int]]]] = None, dh_modulus_sizes: Optional[Dict[str, int]] = None) -> int: # pylint: disable=too-many-arguments
with out:
for algorithm in algorithms:
program_retval = output_algorithm(out, alg_db, alg_type, algorithm, unknown_algs, program_retval, maxlen, alg_sizes)
program_retval = output_algorithm(out, alg_db, alg_type, algorithm, unknown_algs, program_retval, maxlen, host_keys=host_keys, dh_modulus_sizes=dh_modulus_sizes)
if not out.is_section_empty() and not is_json_output:
out.head('# ' + title)
out.flush_section()
@ -119,7 +119,7 @@ def output_algorithms(out: OutputBuffer, title: str, alg_db: Dict[str, Dict[str,
return program_retval
def output_algorithm(out: OutputBuffer, alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], alg_type: str, alg_name: str, unknown_algs: List[str], program_retval: int, alg_max_len: int = 0, alg_sizes: Optional[Dict[str, Tuple[int, int]]] = None) -> int:
def output_algorithm(out: OutputBuffer, alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], alg_type: str, alg_name: str, unknown_algs: List[str], program_retval: int, alg_max_len: int = 0, host_keys: Optional[Dict[str, Dict[str, Union[bytes, str, int]]]] = None, dh_modulus_sizes: Optional[Dict[str, int]] = None) -> int: # pylint: disable=too-many-arguments
prefix = '(' + alg_type + ') '
if alg_max_len == 0:
alg_max_len = len(alg_name)
@ -128,13 +128,23 @@ def output_algorithm(out: OutputBuffer, alg_db: Dict[str, Dict[str, List[List[Op
# If this is an RSA host key or DH GEX, append the size to its name and fix
# the padding.
alg_name_with_size = None
if (alg_sizes is not None) and (alg_name in alg_sizes):
hostkey_size, ca_size = alg_sizes[alg_name]
if ca_size > 0:
alg_name_with_size = '%s (%d-bit cert/%d-bit CA)' % (alg_name, hostkey_size, ca_size)
if (dh_modulus_sizes is not None) and (alg_name in dh_modulus_sizes):
alg_name_with_size = '%s (%u-bit)' % (alg_name, dh_modulus_sizes[alg_name])
padding = padding[0:-11]
elif (host_keys is not None) and (alg_name in host_keys):
hostkey_size = cast(int, host_keys[alg_name]['hostkey_size'])
ca_key_type = cast(str, host_keys[alg_name]['ca_key_type'])
ca_key_size = cast(int, host_keys[alg_name]['ca_key_size'])
# If this is an RSA variant, just print "RSA".
if ca_key_type in HostKeyTest.RSA_FAMILY:
ca_key_type = "RSA"
if len(ca_key_type) > 0 and ca_key_size > 0:
alg_name_with_size = '%s (%u-bit cert/%u-bit %s CA)' % (alg_name, hostkey_size, ca_key_size, ca_key_type)
padding = padding[0:-15]
else:
alg_name_with_size = '%s (%d-bit)' % (alg_name, hostkey_size)
elif alg_name in HostKeyTest.RSA_FAMILY:
alg_name_with_size = '%s (%u-bit)' % (alg_name, hostkey_size)
padding = padding[0:-11]
# If this is a kex algorithm and starts with 'gss-', then normalize its name (i.e.: 'gss-gex-sha1-vz8J1E9PzLr8b1K+0remTg==' => 'gss-gex-sha1-*'). The base64 field can vary, so we'll convert it to the wildcard that our database uses and we'll just resume doing a straight match like all other algorithm names.
@ -289,36 +299,36 @@ def output_security(out: OutputBuffer, banner: Optional[Banner], client_audit: b
def output_fingerprints(out: OutputBuffer, algs: Algorithms, is_json_output: bool) -> None:
with out:
fps = []
fps = {}
if algs.ssh1kex is not None:
name = 'ssh-rsa1'
fp = Fingerprint(algs.ssh1kex.host_key_fingerprint_data)
# bits = algs.ssh1kex.host_key_bits
fps.append((name, fp))
fps[name] = fp
if algs.ssh2kex is not None:
host_keys = algs.ssh2kex.host_keys()
for host_key_type in algs.ssh2kex.host_keys():
if host_keys[host_key_type] is None:
continue
fp = Fingerprint(host_keys[host_key_type])
fp = Fingerprint(cast(bytes, host_keys[host_key_type]['raw_hostkey_bytes']))
# Workaround for Python's order-indifference in dicts. We might get a random RSA type (ssh-rsa, rsa-sha2-256, or rsa-sha2-512), so running the tool against the same server three times may give three different host key types here. So if we have any RSA type, we will simply hard-code it to 'ssh-rsa'.
if host_key_type in HostKeyTest.RSA_FAMILY:
host_key_type = 'ssh-rsa'
# Skip over certificate host types (or we would return invalid fingerprints).
# Skip over certificate host types (or we would return invalid fingerprints), and only add one fingerprint in the RSA family.
if '-cert-' not in host_key_type:
fps.append((host_key_type, fp))
fps[host_key_type] = fp
# Similarly, the host keys can be processed in random order due to Python's order-indifference in dicts. So we sort this list before printing; this makes automated testing possible.
fps = sorted(fps)
for fpp in fps:
name, fp = fpp
out.good('(fin) {}: {}'.format(name, fp.sha256))
fp_types = sorted(fps.keys())
for fp_type in fp_types:
fp = fps[fp_type]
out.good('(fin) {}: {}'.format(fp_type, fp.sha256))
# Output the MD5 hash too if verbose mode is enabled.
if out.verbose:
out.info('(fin) {}: {} -- [info] do not rely on MD5 fingerprints for server identification; it is insecure for this use case'.format(name, fp.md5))
out.info('(fin) {}: {} -- [info] do not rely on MD5 fingerprints for server identification; it is insecure for this use case'.format(fp_type, fp.md5))
if not out.is_section_empty() and not is_json_output:
out.head('# fingerprints')
@ -422,7 +432,7 @@ def post_process_findings(banner: Optional[Banner], algs: Algorithms) -> List[st
algorithm_recommendation_suppress_list = []
# If the server is OpenSSH, and the diffie-hellman-group-exchange-sha256 key exchange was found with modulus size 2048, add a note regarding the bug that causes the server to support 2048-bit moduli no matter the configuration.
if (algs.ssh2kex is not None and 'diffie-hellman-group-exchange-sha256' in algs.ssh2kex.kex_algorithms and 'diffie-hellman-group-exchange-sha256' in algs.ssh2kex.dh_modulus_sizes() and algs.ssh2kex.dh_modulus_sizes()['diffie-hellman-group-exchange-sha256'][0] == 2048) and (banner is not None and banner.software is not None and banner.software.find('OpenSSH') != -1):
if (algs.ssh2kex is not None and 'diffie-hellman-group-exchange-sha256' in algs.ssh2kex.kex_algorithms and 'diffie-hellman-group-exchange-sha256' in algs.ssh2kex.dh_modulus_sizes() and algs.ssh2kex.dh_modulus_sizes()['diffie-hellman-group-exchange-sha256'] == 2048) and (banner is not None and banner.software is not None and banner.software.find('OpenSSH') != -1):
# Ensure a list for notes exists.
while len(SSH2_KexDB.ALGORITHMS['kex']['diffie-hellman-group-exchange-sha256']) < 4:
@ -498,6 +508,8 @@ def output(out: OutputBuffer, aconf: AuditConf, banner: Optional[Banner], header
cves = output_security(out, banner, client_audit, maxlen, aconf.json)
# Filled in by output_algorithms() with unidentified algs.
unknown_algorithms: List[str] = []
# SSHv1
if pkm is not None:
adb = SSH1_KexDB.ALGORITHMS
ciphers = pkm.supported_ciphers
@ -508,16 +520,19 @@ def output(out: OutputBuffer, aconf: AuditConf, banner: Optional[Banner], header
program_retval = output_algorithms(out, title, adb, atype, ciphers, unknown_algorithms, aconf.json, program_retval, maxlen)
title, atype = 'SSH1 authentication types', 'aut'
program_retval = output_algorithms(out, title, adb, atype, auths, unknown_algorithms, aconf.json, program_retval, maxlen)
# SSHv2
if kex is not None:
adb = SSH2_KexDB.ALGORITHMS
title, atype = 'key exchange algorithms', 'kex'
program_retval = output_algorithms(out, title, adb, atype, kex.kex_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, kex.dh_modulus_sizes())
program_retval = output_algorithms(out, title, adb, atype, kex.kex_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, dh_modulus_sizes=kex.dh_modulus_sizes())
title, atype = 'host-key algorithms', 'key'
program_retval = output_algorithms(out, title, adb, atype, kex.key_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, kex.rsa_key_sizes())
program_retval = output_algorithms(out, title, adb, atype, kex.key_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, host_keys=kex.host_keys())
title, atype = 'encryption algorithms (ciphers)', 'enc'
program_retval = output_algorithms(out, title, adb, atype, kex.server.encryption, unknown_algorithms, aconf.json, program_retval, maxlen)
title, atype = 'message authentication code algorithms', 'mac'
program_retval = output_algorithms(out, title, adb, atype, kex.server.mac, unknown_algorithms, aconf.json, program_retval, maxlen)
output_fingerprints(out, algs, aconf.json)
perfect_config = output_recommendations(out, algs, algorithm_recommendation_suppress_list, software, aconf.json, maxlen)
output_info(out, software, client_audit, not perfect_config, aconf.json)
@ -830,10 +845,10 @@ def process_commandline(out: OutputBuffer, args: List[str], usage_cb: Callable[.
if (aconf.policy_file is not None) and (aconf.make_policy is False):
# First, see if this is a built-in policy name. If not, assume a file path was provided, and try to load it from disk.
aconf.policy = Policy.load_builtin_policy(aconf.policy_file)
aconf.policy = Policy.load_builtin_policy(aconf.policy_file, json_output=aconf.json)
if aconf.policy is None:
try:
aconf.policy = Policy(policy_file=aconf.policy_file)
aconf.policy = Policy(policy_file=aconf.policy_file, json_output=aconf.json)
except Exception as e:
out.fail("Error while loading policy file: %s: %s" % (str(e), traceback.format_exc()))
out.write()
@ -885,28 +900,37 @@ def build_struct(target_host: str, banner: Optional['Banner'], cves: List[Dict[s
res['compression'] = kex.server.compression
res['kex'] = []
alg_sizes = kex.dh_modulus_sizes()
dh_alg_sizes = kex.dh_modulus_sizes()
for algorithm in kex.kex_algorithms:
entry: Any = {
'algorithm': algorithm,
}
if algorithm in alg_sizes:
hostkey_size, ca_size = alg_sizes[algorithm]
if algorithm in dh_alg_sizes:
hostkey_size = dh_alg_sizes[algorithm]
entry['keysize'] = hostkey_size
if ca_size > 0:
entry['casize'] = ca_size
res['kex'].append(entry)
res['key'] = []
alg_sizes = kex.rsa_key_sizes()
host_keys = kex.host_keys()
for algorithm in kex.key_algorithms:
entry = {
'algorithm': algorithm,
}
if algorithm in alg_sizes:
hostkey_size, ca_size = alg_sizes[algorithm]
entry['keysize'] = hostkey_size
if algorithm in host_keys:
hostkey_info = host_keys[algorithm]
hostkey_size = cast(int, hostkey_info['hostkey_size'])
ca_type = ''
ca_size = 0
if 'ca_key_type' in hostkey_info:
ca_type = cast(str, hostkey_info['ca_key_type'])
if 'ca_key_size' in hostkey_info:
ca_size = cast(int, hostkey_info['ca_key_size'])
if algorithm in HostKeyTest.RSA_FAMILY or algorithm.startswith('ssh-rsa-cert-v0'):
entry['keysize'] = hostkey_size
if ca_size > 0:
entry['ca_algorithm'] = ca_type
entry['casize'] = ca_size
res['key'].append(entry)
@ -926,7 +950,7 @@ def build_struct(target_host: str, banner: Optional['Banner'], cves: List[Dict[s
if host_keys[host_key_type] is None:
continue
fp = Fingerprint(host_keys[host_key_type])
fp = Fingerprint(cast(bytes, host_keys[host_key_type]['raw_hostkey_bytes']))
# Skip over certificate host types (or we would return invalid fingerprints).
if '-cert-' in host_key_type:
@ -1041,7 +1065,7 @@ def audit(out: OutputBuffer, aconf: AuditConf, sshv: Optional[int] = None, print
program_retval = output(out, aconf, banner, header, pkm=SSH1_PublicKeyMessage.parse(payload))
elif sshv == 2:
try:
kex = SSH2_Kex.parse(payload)
kex = SSH2_Kex.parse(out, payload)
except Exception:
out.fail("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()))
return exitcodes.CONNECTION_ERROR

View File

@ -236,7 +236,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
self.__outputbuffer.d('KEX initialisation...', write_now=True)
kexparty = SSH2_KexParty(ciphers, macs, compressions, languages)
kex = SSH2_Kex(os.urandom(16), key_exchanges, hostkeys, kexparty, kexparty, False, 0)
kex = SSH2_Kex(self.__outputbuffer, os.urandom(16), key_exchanges, hostkeys, kexparty, kexparty, False, 0)
self.write_byte(Protocol.MSG_KEXINIT)
kex.write(self)

View File

@ -10,7 +10,7 @@
"expected_required": [
"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": [
@ -22,7 +22,7 @@
"expected_required": [
"4096"
],
"mismatched_field": "RSA CA key (ssh-rsa-cert-v01@openssh.com) sizes"
"mismatched_field": "CA signature size (ssh-rsa)"
}
],
"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
Policy: Docker poliicy: test10 (version 1)
Result: ❌ Failed!

Errors:
* RSA CA key (ssh-rsa-cert-v01@openssh.com) sizes did not match.
* CA signature size (ssh-rsa) did not match.
- Expected: 4096
- 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
- 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
Policy: Docker poliicy: test7 (version 1)
Result: ✔ Passed

View File

@ -10,7 +10,7 @@
"expected_required": [
"2048"
],
"mismatched_field": "RSA CA key (ssh-rsa-cert-v01@openssh.com) sizes"
"mismatched_field": "CA signature size (ssh-rsa)"
}
],
"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
Policy: Docker poliicy: test8 (version 1)
Result: ❌ Failed!

Errors:
* RSA CA key (ssh-rsa-cert-v01@openssh.com) sizes did not match.
* CA signature size (ssh-rsa) did not match.
- Expected: 2048
- Actual: 1024


View File

@ -10,7 +10,7 @@
"expected_required": [
"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",

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
Policy: Docker poliicy: test9 (version 1)
Result: ❌ Failed!

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
- Actual: 3072


View File

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

View File

@ -40,10 +40,11 @@
 `- [fail] using small 1024-bit modulus
`- [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
(key) ssh-rsa-cert-v01@openssh.com (1024-bit cert/1024-bit CA) -- [fail] using broken SHA-1 hash algorithm
 `- [fail] using small 1024-bit modulus
`- [info] available since OpenSSH 5.6
`- [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 RSA CA) -- [fail] using broken SHA-1 hash algorithm
 `- [fail] using small 1024-bit hostkey modulus
 `- [fail] using small 1024-bit CA key modulus
`- [info] available since OpenSSH 5.6
`- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8
# encryption algorithms (ciphers)
(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",
"ca_algorithm": "ssh-rsa",
"casize": 3072,
"keysize": 1024
}

View File

@ -40,10 +40,10 @@
 `- [fail] using small 1024-bit modulus
`- [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
(key) ssh-rsa-cert-v01@openssh.com (1024-bit cert/3072-bit CA) -- [fail] using broken SHA-1 hash algorithm
 `- [fail] using small 1024-bit modulus
`- [info] available since OpenSSH 5.6
`- [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 RSA CA) -- [fail] using broken SHA-1 hash algorithm
 `- [fail] using small 1024-bit hostkey modulus
`- [info] available since OpenSSH 5.6
`- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8
# encryption algorithms (ciphers)
(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",
"ca_algorithm": "ssh-rsa",
"casize": 1024,
"keysize": 3072
}

View File

@ -39,10 +39,10 @@
(key) ssh-rsa (3072-bit) -- [fail] using broken SHA-1 hash algorithm
`- [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
(key) ssh-rsa-cert-v01@openssh.com (3072-bit cert/1024-bit CA) -- [fail] using broken SHA-1 hash algorithm
 `- [fail] using small 1024-bit modulus
`- [info] available since OpenSSH 5.6
`- [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 RSA CA) -- [fail] using broken SHA-1 hash algorithm
 `- [fail] using small 1024-bit CA key modulus
`- [info] available since OpenSSH 5.6
`- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8
# encryption algorithms (ciphers)
(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",
"ca_algorithm": "ssh-rsa",
"casize": 3072,
"keysize": 3072
}

View File

@ -39,9 +39,9 @@
(key) ssh-rsa (3072-bit) -- [fail] using broken SHA-1 hash algorithm
`- [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
(key) ssh-rsa-cert-v01@openssh.com (3072-bit cert/3072-bit CA) -- [fail] using broken SHA-1 hash algorithm
`- [info] available since OpenSSH 5.6
`- [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 RSA CA) -- [fail] using broken SHA-1 hash algorithm
`- [info] available since OpenSSH 5.6
`- [info] deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8
# encryption algorithms (ciphers)
(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
Policy: Docker policy: test11 (version 1)
Result: ✔ Passed

View File

@ -10,7 +10,7 @@
"expected_required": [
"4096"
],
"mismatched_field": "RSA host key (rsa-sha2-256) sizes"
"mismatched_field": "Host key (rsa-sha2-256) sizes"
},
{
"actual": [
@ -22,7 +22,7 @@
"expected_required": [
"4096"
],
"mismatched_field": "RSA host key (rsa-sha2-512) sizes"
"mismatched_field": "Host key (rsa-sha2-512) sizes"
},
{
"actual": [
@ -34,7 +34,7 @@
"expected_required": [
"4096"
],
"mismatched_field": "RSA host key (ssh-rsa) sizes"
"mismatched_field": "Host key (ssh-rsa) sizes"
}
],
"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
Policy: Docker policy: test12 (version 1)
Result: ❌ Failed!

Errors:
* RSA host key (rsa-sha2-256) sizes did not match.
* Host key (rsa-sha2-256) sizes did not match.
- Expected: 4096
- Actual: 3072
* RSA host key (rsa-sha2-512) sizes did not match.
* Host key (rsa-sha2-512) sizes did not match.
- Expected: 4096
- Actual: 3072
* RSA host key (ssh-rsa) sizes did not match.
* Host key (ssh-rsa) sizes did not match.
- Expected: 4096
- 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
Policy: Docker policy: test13 (version 1)
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
Policy: Docker policy: test14 (version 1)
Result: ❌ Failed!

View File

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

View File

@ -34,7 +34,7 @@
# host-key algorithms
(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)
(enc) chacha20-poly1305@openssh.com -- [info] available since OpenSSH 6.5

View File

@ -1,6 +1,7 @@
import os
import pytest
from ssh_audit.outputbuffer import OutputBuffer
from ssh_audit.ssh2_kex import SSH2_Kex
from ssh_audit.ssh2_kexparty import SSH2_KexParty
@ -13,7 +14,7 @@ def kex(ssh_audit):
enc, mac, compression, languages = [], [], ['none'], []
srv = SSH2_KexParty(enc, mac, compression, languages)
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
@ -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
same time.
"""
kex.set_host_key("ssh-rsa", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
kex.set_host_key("ssh-rsa1", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
kex.set_host_key("ssh-rsa2", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
kex.set_host_key("ssh-rsa3", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
kex.set_host_key("ssh-rsa4", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
kex.set_host_key("ssh-rsa5", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
kex.set_host_key("ssh-rsa6", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
kex.set_host_key("ssh-rsa7", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
kex.set_host_key("ssh-rsa8", 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", 1024, '', 0)
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", 1024, '', 0)
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", 1024, '', 0)
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", 1024, '', 0)
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)

View File

@ -2,6 +2,7 @@ import hashlib
import pytest
from datetime import date
from ssh_audit.outputbuffer import OutputBuffer
from ssh_audit.policy import Policy
from ssh_audit.ssh2_kex import SSH2_Kex
from ssh_audit.writebuf import WriteBuf
@ -10,6 +11,7 @@ from ssh_audit.writebuf import WriteBuf
class TestPolicy:
@pytest.fixture(autouse=True)
def init(self, ssh_audit):
self.OutputBuffer = OutputBuffer
self.Policy = Policy
self.wbuf = WriteBuf
self.ssh2_kex = SSH2_Kex
@ -32,7 +34,7 @@ class TestPolicy:
w.write_list([''])
w.write_byte(False)
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):

View File

@ -79,7 +79,7 @@ class TestSSH2:
return w.write_flush()
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.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']
@ -105,7 +105,7 @@ class TestSSH2:
srv = self.ssh2_kexparty(enc, mac, compression, languages)
if cookie is None:
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
def _get_kex_variat1(self):
@ -149,7 +149,7 @@ class TestSSH2:
def test_key_payload(self):
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
def test_ssh2_server_simple(self, output_spy, virtual_socket):