From 263267c5ad013d314574fcd9bc6a744699e226c5 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Tue, 25 Apr 2023 09:17:32 -0400 Subject: [PATCH] Added support for mixed host key/CA key types (i.e.: RSA host keys signed by ED25519 CAs) (#120). --- README.md | 1 + docker_test.sh | 4 +- src/ssh_audit/gextest.py | 18 +- src/ssh_audit/hostkeytest.py | 100 +++++--- src/ssh_audit/kexdh.py | 242 ++++++++++-------- src/ssh_audit/policy.py | 159 ++++++++---- src/ssh_audit/ssh2_kex.py | 40 ++- src/ssh_audit/ssh_audit.py | 96 ++++--- src/ssh_audit/ssh_socket.py | 2 +- .../openssh_5.6p1_custom_policy_test10.json | 4 +- .../openssh_5.6p1_custom_policy_test10.txt | 19 +- .../openssh_5.6p1_custom_policy_test7.txt | 15 ++ .../openssh_5.6p1_custom_policy_test8.json | 2 +- .../openssh_5.6p1_custom_policy_test8.txt | 17 +- .../openssh_5.6p1_custom_policy_test9.json | 2 +- .../openssh_5.6p1_custom_policy_test9.txt | 17 +- .../expected_results/openssh_5.6p1_test2.json | 1 + .../expected_results/openssh_5.6p1_test2.txt | 9 +- .../expected_results/openssh_5.6p1_test3.json | 1 + .../expected_results/openssh_5.6p1_test3.txt | 8 +- .../expected_results/openssh_5.6p1_test4.json | 1 + .../expected_results/openssh_5.6p1_test4.txt | 8 +- .../expected_results/openssh_5.6p1_test5.json | 1 + .../expected_results/openssh_5.6p1_test5.txt | 6 +- .../openssh_8.0p1_custom_policy_test11.txt | 9 + .../openssh_8.0p1_custom_policy_test12.json | 6 +- .../openssh_8.0p1_custom_policy_test12.txt | 15 +- .../openssh_8.0p1_custom_policy_test13.txt | 12 + .../openssh_8.0p1_custom_policy_test14.txt | 12 + .../expected_results/openssh_8.0p1_test2.json | 4 +- .../expected_results/openssh_8.0p1_test2.txt | 2 +- test/test_build_struct.py | 21 +- test/test_policy.py | 4 +- test/test_ssh2.py | 6 +- 34 files changed, 556 insertions(+), 308 deletions(-) diff --git a/README.md b/README.md index 18f611b..02d1f29 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docker_test.sh b/docker_test.sh index b619368..d7c71d4 100755 --- a/docker_test.sh +++ b/docker_test.sh @@ -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" diff --git a/src/ssh_audit/gextest.py b/src/ssh_audit/gextest.py index 3eb1bf7..97bafd4 100644 --- a/src/ssh_audit/gextest.py +++ b/src/ssh_audit/gextest.py @@ -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 diff --git a/src/ssh_audit/hostkeytest.py b/src/ssh_audit/hostkeytest.py index 26aa48d..400a674 100644 --- a/src/ssh_audit/hostkeytest.py +++ b/src/ssh_audit/hostkeytest.py @@ -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: diff --git a/src/ssh_audit/kexdh.py b/src/ssh_audit/kexdh.py index 12edc9e..9baab73 100644 --- a/src/ssh_audit/kexdh.py +++ b/src/ssh_audit/kexdh.py @@ -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') diff --git a/src/ssh_audit/policy.py b/src/ssh_audit/policy.py index 2e28699..7fb3fb7 100644 --- a/src/ssh_audit/policy.py +++ b/src/ssh_audit/policy.py @@ -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 diff --git a/src/ssh_audit/ssh2_kex.py b/src/ssh_audit/ssh2_kex.py index 0c31f3d..d71724f 100644 --- a/src/ssh_audit/ssh2_kex.py +++ b/src/ssh_audit/ssh2_kex.py @@ -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 diff --git a/src/ssh_audit/ssh_audit.py b/src/ssh_audit/ssh_audit.py index 2ebbc37..bc86153 100755 --- a/src/ssh_audit/ssh_audit.py +++ b/src/ssh_audit/ssh_audit.py @@ -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 diff --git a/src/ssh_audit/ssh_socket.py b/src/ssh_audit/ssh_socket.py index 99ccbd2..5afa4ce 100644 --- a/src/ssh_audit/ssh_socket.py +++ b/src/ssh_audit/ssh_socket.py @@ -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) diff --git a/test/docker/expected_results/openssh_5.6p1_custom_policy_test10.json b/test/docker/expected_results/openssh_5.6p1_custom_policy_test10.json index b07940c..0a1e148 100644 --- a/test/docker/expected_results/openssh_5.6p1_custom_policy_test10.json +++ b/test/docker/expected_results/openssh_5.6p1_custom_policy_test10.json @@ -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", diff --git a/test/docker/expected_results/openssh_5.6p1_custom_policy_test10.txt b/test/docker/expected_results/openssh_5.6p1_custom_policy_test10.txt index 7f8befc..425d463 100644 --- a/test/docker/expected_results/openssh_5.6p1_custom_policy_test10.txt +++ b/test/docker/expected_results/openssh_5.6p1_custom_policy_test10.txt @@ -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  diff --git a/test/docker/expected_results/openssh_5.6p1_custom_policy_test7.txt b/test/docker/expected_results/openssh_5.6p1_custom_policy_test7.txt index 4014b23..1d3af14 100644 --- a/test/docker/expected_results/openssh_5.6p1_custom_policy_test7.txt +++ b/test/docker/expected_results/openssh_5.6p1_custom_policy_test7.txt @@ -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 diff --git a/test/docker/expected_results/openssh_5.6p1_custom_policy_test8.json b/test/docker/expected_results/openssh_5.6p1_custom_policy_test8.json index 391f224..e7f06a6 100644 --- a/test/docker/expected_results/openssh_5.6p1_custom_policy_test8.json +++ b/test/docker/expected_results/openssh_5.6p1_custom_policy_test8.json @@ -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", diff --git a/test/docker/expected_results/openssh_5.6p1_custom_policy_test8.txt b/test/docker/expected_results/openssh_5.6p1_custom_policy_test8.txt index 36dceba..05ab91d 100644 --- a/test/docker/expected_results/openssh_5.6p1_custom_policy_test8.txt +++ b/test/docker/expected_results/openssh_5.6p1_custom_policy_test8.txt @@ -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  diff --git a/test/docker/expected_results/openssh_5.6p1_custom_policy_test9.json b/test/docker/expected_results/openssh_5.6p1_custom_policy_test9.json index b32dfe6..51d1067 100644 --- a/test/docker/expected_results/openssh_5.6p1_custom_policy_test9.json +++ b/test/docker/expected_results/openssh_5.6p1_custom_policy_test9.json @@ -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", diff --git a/test/docker/expected_results/openssh_5.6p1_custom_policy_test9.txt b/test/docker/expected_results/openssh_5.6p1_custom_policy_test9.txt index fc91c9f..94060ab 100644 --- a/test/docker/expected_results/openssh_5.6p1_custom_policy_test9.txt +++ b/test/docker/expected_results/openssh_5.6p1_custom_policy_test9.txt @@ -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  diff --git a/test/docker/expected_results/openssh_5.6p1_test2.json b/test/docker/expected_results/openssh_5.6p1_test2.json index 2f19adb..ffe6e98 100644 --- a/test/docker/expected_results/openssh_5.6p1_test2.json +++ b/test/docker/expected_results/openssh_5.6p1_test2.json @@ -139,6 +139,7 @@ }, { "algorithm": "ssh-rsa-cert-v01@openssh.com", + "ca_algorithm": "ssh-rsa", "casize": 1024, "keysize": 1024 } diff --git a/test/docker/expected_results/openssh_5.6p1_test2.txt b/test/docker/expected_results/openssh_5.6p1_test2.txt index 649e3c0..6b3b975 100644 --- a/test/docker/expected_results/openssh_5.6p1_test2.txt +++ b/test/docker/expected_results/openssh_5.6p1_test2.txt @@ -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 diff --git a/test/docker/expected_results/openssh_5.6p1_test3.json b/test/docker/expected_results/openssh_5.6p1_test3.json index 4cf7456..b4f01af 100644 --- a/test/docker/expected_results/openssh_5.6p1_test3.json +++ b/test/docker/expected_results/openssh_5.6p1_test3.json @@ -139,6 +139,7 @@ }, { "algorithm": "ssh-rsa-cert-v01@openssh.com", + "ca_algorithm": "ssh-rsa", "casize": 3072, "keysize": 1024 } diff --git a/test/docker/expected_results/openssh_5.6p1_test3.txt b/test/docker/expected_results/openssh_5.6p1_test3.txt index ce2edb2..991c502 100644 --- a/test/docker/expected_results/openssh_5.6p1_test3.txt +++ b/test/docker/expected_results/openssh_5.6p1_test3.txt @@ -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 diff --git a/test/docker/expected_results/openssh_5.6p1_test4.json b/test/docker/expected_results/openssh_5.6p1_test4.json index bfc84d8..074f2b1 100644 --- a/test/docker/expected_results/openssh_5.6p1_test4.json +++ b/test/docker/expected_results/openssh_5.6p1_test4.json @@ -139,6 +139,7 @@ }, { "algorithm": "ssh-rsa-cert-v01@openssh.com", + "ca_algorithm": "ssh-rsa", "casize": 1024, "keysize": 3072 } diff --git a/test/docker/expected_results/openssh_5.6p1_test4.txt b/test/docker/expected_results/openssh_5.6p1_test4.txt index c362c18..2fb3e19 100644 --- a/test/docker/expected_results/openssh_5.6p1_test4.txt +++ b/test/docker/expected_results/openssh_5.6p1_test4.txt @@ -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 diff --git a/test/docker/expected_results/openssh_5.6p1_test5.json b/test/docker/expected_results/openssh_5.6p1_test5.json index c950633..a645f0e 100644 --- a/test/docker/expected_results/openssh_5.6p1_test5.json +++ b/test/docker/expected_results/openssh_5.6p1_test5.json @@ -139,6 +139,7 @@ }, { "algorithm": "ssh-rsa-cert-v01@openssh.com", + "ca_algorithm": "ssh-rsa", "casize": 3072, "keysize": 3072 } diff --git a/test/docker/expected_results/openssh_5.6p1_test5.txt b/test/docker/expected_results/openssh_5.6p1_test5.txt index 4d388d1..b9e7cd7 100644 --- a/test/docker/expected_results/openssh_5.6p1_test5.txt +++ b/test/docker/expected_results/openssh_5.6p1_test5.txt @@ -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 diff --git a/test/docker/expected_results/openssh_8.0p1_custom_policy_test11.txt b/test/docker/expected_results/openssh_8.0p1_custom_policy_test11.txt index 024bcb9..0ac0671 100644 --- a/test/docker/expected_results/openssh_8.0p1_custom_policy_test11.txt +++ b/test/docker/expected_results/openssh_8.0p1_custom_policy_test11.txt @@ -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 diff --git a/test/docker/expected_results/openssh_8.0p1_custom_policy_test12.json b/test/docker/expected_results/openssh_8.0p1_custom_policy_test12.json index 2a21591..8ddcf39 100644 --- a/test/docker/expected_results/openssh_8.0p1_custom_policy_test12.json +++ b/test/docker/expected_results/openssh_8.0p1_custom_policy_test12.json @@ -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", diff --git a/test/docker/expected_results/openssh_8.0p1_custom_policy_test12.txt b/test/docker/expected_results/openssh_8.0p1_custom_policy_test12.txt index 6fb0561..de615e0 100644 --- a/test/docker/expected_results/openssh_8.0p1_custom_policy_test12.txt +++ b/test/docker/expected_results/openssh_8.0p1_custom_policy_test12.txt @@ -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  diff --git a/test/docker/expected_results/openssh_8.0p1_custom_policy_test13.txt b/test/docker/expected_results/openssh_8.0p1_custom_policy_test13.txt index b8b4b25..7734d88 100644 --- a/test/docker/expected_results/openssh_8.0p1_custom_policy_test13.txt +++ b/test/docker/expected_results/openssh_8.0p1_custom_policy_test13.txt @@ -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 diff --git a/test/docker/expected_results/openssh_8.0p1_custom_policy_test14.txt b/test/docker/expected_results/openssh_8.0p1_custom_policy_test14.txt index 2bb59fb..5d9bdbf 100644 --- a/test/docker/expected_results/openssh_8.0p1_custom_policy_test14.txt +++ b/test/docker/expected_results/openssh_8.0p1_custom_policy_test14.txt @@ -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! diff --git a/test/docker/expected_results/openssh_8.0p1_test2.json b/test/docker/expected_results/openssh_8.0p1_test2.json index 737d044..7f68635 100644 --- a/test/docker/expected_results/openssh_8.0p1_test2.json +++ b/test/docker/expected_results/openssh_8.0p1_test2.json @@ -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": [ diff --git a/test/docker/expected_results/openssh_8.0p1_test2.txt b/test/docker/expected_results/openssh_8.0p1_test2.txt index 8250da6..b049d90 100644 --- a/test/docker/expected_results/openssh_8.0p1_test2.txt +++ b/test/docker/expected_results/openssh_8.0p1_test2.txt @@ -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 diff --git a/test/test_build_struct.py b/test/test_build_struct.py index ce00142..e16baac 100644 --- a/test/test_build_struct.py +++ b/test/test_build_struct.py @@ -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) diff --git a/test/test_policy.py b/test/test_policy.py index a008b81..f6a0af9 100644 --- a/test/test_policy.py +++ b/test/test_policy.py @@ -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): diff --git a/test/test_ssh2.py b/test/test_ssh2.py index d0e5ba1..4674812 100644 --- a/test/test_ssh2.py +++ b/test/test_ssh2.py @@ -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):