2020-10-15 20:34:23 +02:00
"""
The MIT License ( MIT )
2023-02-06 22:27:30 +01:00
Copyright ( C ) 2017 - 2023 Joe Testa ( jtesta @positronsecurity.com )
2020-10-15 20:34:23 +02:00
Permission is hereby granted , free of charge , to any person obtaining a copy
of this software and associated documentation files ( the " Software " ) , to deal
in the Software without restriction , including without limitation the rights
to use , copy , modify , merge , publish , distribute , sublicense , and / or sell
copies of the Software , and to permit persons to whom the Software is
furnished to do so , subject to the following conditions :
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software .
THE SOFTWARE IS PROVIDED " AS IS " , WITHOUT WARRANTY OF ANY KIND , EXPRESS OR
IMPLIED , INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY ,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT . IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM , DAMAGES OR OTHER
LIABILITY , WHETHER IN AN ACTION OF CONTRACT , TORT OR OTHERWISE , ARISING FROM ,
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
2021-05-25 01:50:25 +02:00
import traceback
2023-04-25 15:17:32 +02:00
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
2020-10-15 20:34:23 +02:00
from ssh_audit . ssh2_kex import SSH2_Kex
from ssh_audit . ssh2_kexdb import SSH2_KexDB
from ssh_audit . ssh_socket import SSH_Socket
2021-03-02 17:06:40 +01:00
from ssh_audit . outputbuffer import OutputBuffer
2020-10-15 20:34:23 +02:00
# Obtains host keys, checks their size, and derives their fingerprints.
class HostKeyTest :
# Tracks the RSA host key types. As of this writing, testing one in this family yields valid results for the rest.
RSA_FAMILY = [ ' ssh-rsa ' , ' rsa-sha2-256 ' , ' rsa-sha2-512 ' ]
# Dict holding the host key types we should extract & parse. 'cert' is True to denote that a host key type handles certificates (thus requires additional parsing). 'variable_key_len' is True for host key types that can have variable sizes (True only for RSA types, as the rest are of fixed-size). After the host key type is fully parsed, the key 'parsed' is added with a value of True.
HOST_KEY_TYPES = {
' ssh-rsa ' : { ' cert ' : False , ' variable_key_len ' : True } ,
' rsa-sha2-256 ' : { ' cert ' : False , ' variable_key_len ' : True } ,
' rsa-sha2-512 ' : { ' cert ' : False , ' variable_key_len ' : True } ,
2020-10-19 23:42:12 +02:00
' ssh-rsa-cert-v01@openssh.com ' : { ' cert ' : True , ' variable_key_len ' : True } ,
' rsa-sha2-256-cert-v01@openssh.com ' : { ' cert ' : True , ' variable_key_len ' : True } ,
' rsa-sha2-512-cert-v01@openssh.com ' : { ' cert ' : True , ' variable_key_len ' : True } ,
2020-10-15 20:34:23 +02:00
' ssh-ed25519 ' : { ' cert ' : False , ' variable_key_len ' : False } ,
' ssh-ed25519-cert-v01@openssh.com ' : { ' cert ' : True , ' variable_key_len ' : False } ,
}
2023-02-06 22:27:30 +01:00
TWO2K_MODULUS_WARNING = ' 2048-bit modulus only provides 112-bits of symmetric strength '
2023-04-25 15:17:32 +02:00
SMALL_ECC_MODULUS_WARNING = ' 224-bit ECC modulus only provides 112-bits of symmetric strength '
2023-02-06 22:27:30 +01:00
2020-10-15 20:34:23 +02:00
@staticmethod
2021-03-02 17:06:40 +01:00
def run ( out : ' OutputBuffer ' , s : ' SSH_Socket ' , server_kex : ' SSH2_Kex ' ) - > None :
2020-10-15 20:34:23 +02:00
KEX_TO_DHGROUP = {
' diffie-hellman-group1-sha1 ' : KexGroup1 ,
' diffie-hellman-group14-sha1 ' : KexGroup14_SHA1 ,
' diffie-hellman-group14-sha256 ' : KexGroup14_SHA256 ,
' curve25519-sha256 ' : KexCurve25519_SHA256 ,
' curve25519-sha256@libssh.org ' : KexCurve25519_SHA256 ,
' diffie-hellman-group16-sha512 ' : KexGroup16_SHA512 ,
' diffie-hellman-group18-sha512 ' : KexGroup18_SHA512 ,
' diffie-hellman-group-exchange-sha1 ' : KexGroupExchange_SHA1 ,
' diffie-hellman-group-exchange-sha256 ' : KexGroupExchange_SHA256 ,
' ecdh-sha2-nistp256 ' : KexNISTP256 ,
' ecdh-sha2-nistp384 ' : KexNISTP384 ,
' ecdh-sha2-nistp521 ' : KexNISTP521 ,
# 'kexguess2@matt.ucc.asn.au': ???
}
# Pick the first kex algorithm that the server supports, which we
# happen to support as well.
kex_str = None
kex_group = None
for server_kex_alg in server_kex . kex_algorithms :
if server_kex_alg in KEX_TO_DHGROUP :
kex_str = server_kex_alg
2023-04-25 15:17:32 +02:00
kex_group = KEX_TO_DHGROUP [ kex_str ] ( out )
2020-10-15 20:34:23 +02:00
break
if kex_str is not None and kex_group is not None :
2021-03-02 17:06:40 +01:00
HostKeyTest . perform_test ( out , s , server_kex , kex_str , kex_group , HostKeyTest . HOST_KEY_TYPES )
2020-10-15 20:34:23 +02:00
@staticmethod
2021-03-02 17:06:40 +01:00
def perform_test ( out : ' OutputBuffer ' , s : ' SSH_Socket ' , server_kex : ' SSH2_Kex ' , kex_str : str , kex_group : ' KexDH ' , host_key_types : Dict [ str , Dict [ str , bool ] ] ) - > None :
2020-10-15 20:34:23 +02:00
hostkey_modulus_size = 0
ca_modulus_size = 0
# If the connection still exists, close it so we can test
# using a clean slate (otherwise it may exist in a non-testable
# state).
if s . is_connected ( ) :
s . close ( )
# For each host key type...
for host_key_type in host_key_types :
2023-04-29 17:39:29 +02:00
key_fail_comments = [ ]
key_warn_comments = [ ]
2020-10-15 20:34:23 +02:00
# Skip those already handled (i.e.: those in the RSA family, as testing one tests them all).
if ' parsed ' in host_key_types [ host_key_type ] and host_key_types [ host_key_type ] [ ' parsed ' ] :
continue
# If this host key type is supported by the server, we test it.
if host_key_type in server_kex . key_algorithms :
2021-03-02 17:06:40 +01:00
out . d ( ' Preparing to obtain ' + host_key_type + ' host key... ' , write_now = True )
2020-10-15 20:34:23 +02:00
cert = host_key_types [ host_key_type ] [ ' cert ' ]
# If the connection is closed, re-open it and get the kex again.
if not s . is_connected ( ) :
2021-03-02 17:25:37 +01:00
err = s . connect ( )
2020-10-15 20:34:23 +02:00
if err is not None :
2021-03-02 17:06:40 +01:00
out . v ( err , write_now = True )
2020-10-15 20:34:23 +02:00
return
2021-03-02 17:25:37 +01:00
_ , _ , err = s . get_banner ( )
2020-10-15 20:34:23 +02:00
if err is not None :
2021-03-02 17:06:40 +01:00
out . v ( err , write_now = True )
2020-10-15 20:34:23 +02:00
s . close ( )
return
2021-01-20 21:27:38 +01:00
# Send our KEX using the specified group-exchange and most of the server's own values.
2021-03-02 17:25:37 +01:00
s . send_kexinit ( key_exchanges = [ kex_str ] , hostkeys = [ host_key_type ] , ciphers = server_kex . server . encryption , macs = server_kex . server . mac , compressions = server_kex . server . compression , languages = server_kex . server . languages )
2020-10-15 20:34:23 +02:00
2021-05-25 01:50:25 +02:00
try :
# Parse the server's KEX.
_ , payload = s . read_packet ( )
2023-04-25 15:17:32 +02:00
SSH2_Kex . parse ( out , payload )
2021-05-25 01:50:25 +02:00
except Exception :
out . v ( " Failed to parse server ' s kex. Stack trace: \n %s " % str ( traceback . format_exc ( ) ) , write_now = True )
return
2020-10-15 20:34:23 +02:00
# 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.
2021-03-02 17:06:40 +01:00
kex_group . send_init ( s )
2023-04-25 15:17:32 +02:00
raw_hostkey_bytes = b ' '
2020-11-06 02:29:39 +01:00
try :
2023-04-25 15:17:32 +02:00
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
2020-10-15 20:34:23 +02:00
hostkey_modulus_size = kex_group . get_hostkey_size ( )
2023-04-25 15:17:32 +02:00
ca_key_type = kex_group . get_ca_type ( )
2020-10-15 20:34:23 +02:00
ca_modulus_size = kex_group . get_ca_size ( )
2023-04-25 15:17:32 +02:00
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 )
2020-10-15 20:34:23 +02:00
# Close the socket, as the connection has
# been put in a state that later tests can't use.
s . close ( )
# 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 :
2023-04-25 15:17:32 +02:00
# 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
2020-10-15 20:34:23 +02:00
2023-02-06 22:27:30 +01:00
# Keys smaller than 2048 result in a failure. Keys smaller 3072 result in a warning. Update the database accordingly.
2023-04-25 15:17:32 +02:00
if ( cert is False ) and ( hostkey_modulus_size < hostkey_min_good ) :
2023-02-06 22:27:30 +01:00
2023-04-25 15:17:32 +02:00
# If the key is under 2048, add to the failure list.
if hostkey_modulus_size < hostkey_min_warn :
2023-04-29 17:39:29 +02:00
key_fail_comments . append ( ' using small %d -bit modulus ' % hostkey_modulus_size )
elif hostkey_warn_str not in key_warn_comments : # Issue a warning about 2048-bit moduli.
key_warn_comments . append ( hostkey_warn_str )
2023-02-06 22:27:30 +01:00
2023-04-25 15:17:32 +02:00
elif ( cert is True ) and ( ( hostkey_modulus_size < hostkey_min_good ) or ( 0 < ca_modulus_size < cakey_min_good ) ) :
# If the host key is smaller than 2048-bit/224-bit, flag this as a failure.
if hostkey_modulus_size < hostkey_min_warn :
2023-04-29 17:39:29 +02:00
key_fail_comments . append ( ' using small %d -bit hostkey modulus ' % hostkey_modulus_size )
2023-04-25 15:17:32 +02:00
# Otherwise, this is just a warning.
2023-04-29 17:39:29 +02:00
elif ( hostkey_modulus_size < hostkey_min_good ) and ( hostkey_warn_str not in key_warn_comments ) :
key_warn_comments . append ( hostkey_warn_str )
2023-04-25 15:17:32 +02:00
# If the CA key is smaller than 2048-bit/224-bit, flag this as a failure.
if 0 < ca_modulus_size < cakey_min_warn :
2023-04-29 17:39:29 +02:00
key_fail_comments . append ( ' using small %d -bit CA key modulus ' % ca_modulus_size )
2023-04-25 15:17:32 +02:00
# Otherwise, this is just a warning.
2023-04-29 17:39:29 +02:00
elif ( 0 < ca_modulus_size < cakey_min_good ) and ( cakey_warn_str not in key_warn_comments ) :
key_warn_comments . append ( cakey_warn_str )
2020-10-15 20:34:23 +02:00
# 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 :
for rsa_type in HostKeyTest . RSA_FAMILY :
host_key_types [ rsa_type ] [ ' parsed ' ] = True
2023-04-29 17:39:29 +02:00
# If the current key is a member of the RSA family, then populate all RSA family members with the same
# failure and/or warning comments.
while len ( SSH2_KexDB . ALGORITHMS [ ' key ' ] [ rsa_type ] ) < 3 :
SSH2_KexDB . ALGORITHMS [ ' key ' ] [ rsa_type ] . append ( [ ] )
if key_fail_comments :
SSH2_KexDB . ALGORITHMS [ ' key ' ] [ rsa_type ] [ 1 ] . extend ( key_fail_comments )
if key_warn_comments :
SSH2_KexDB . ALGORITHMS [ ' key ' ] [ rsa_type ] [ 2 ] . extend ( key_warn_comments )
2020-10-15 20:34:23 +02:00
else :
host_key_types [ host_key_type ] [ ' parsed ' ] = True
2023-04-29 17:39:29 +02:00
while len ( SSH2_KexDB . ALGORITHMS [ ' key ' ] [ host_key_type ] ) < 3 :
SSH2_KexDB . ALGORITHMS [ ' key ' ] [ host_key_type ] . append ( [ ] )
if key_fail_comments :
SSH2_KexDB . ALGORITHMS [ ' key ' ] [ host_key_type ] [ 1 ] . extend ( key_fail_comments )
if key_warn_comments :
SSH2_KexDB . ALGORITHMS [ ' key ' ] [ host_key_type ] [ 2 ] . extend ( key_warn_comments )