185
185
_Conn = TypeVar ('_Conn' , bound = 'SSHConnection' )
186
186
_Options = TypeVar ('_Options' , bound = 'SSHConnectionOptions' )
187
187
188
+ _ServerHostKeysHandler = Optional [Callable [[List [SSHKey ], List [SSHKey ],
189
+ List [SSHKey ], List [SSHKey ]],
190
+ MaybeAwait [None ]]]
191
+
188
192
class _TunnelProtocol (Protocol ):
189
193
"""Base protocol for connections to tunnel SSH over"""
190
194
@@ -1995,6 +1999,11 @@ def send_userauth_success(self) -> None:
1995
1999
not self ._waiter .cancelled ():
1996
2000
self ._waiter .set_result (None )
1997
2001
self ._wait = None
2002
+ return
2003
+
2004
+ # This method is only in SSHServerConnection
2005
+ # pylint: disable=no-member
2006
+ cast (SSHServerConnection , self ).send_server_host_keys ()
1998
2007
1999
2008
def send_channel_open_confirmation (self , send_chan : int , recv_chan : int ,
2000
2009
recv_window : int , recv_pktsize : int ,
@@ -2012,6 +2021,13 @@ def send_channel_open_failure(self, send_chan: int, code: int,
2012
2021
self .send_packet (MSG_CHANNEL_OPEN_FAILURE , UInt32 (send_chan ),
2013
2022
UInt32 (code ), String (reason ), String (lang ))
2014
2023
2024
+ def _send_global_request (self , request : bytes , * args : bytes ,
2025
+ want_reply : bool = False ) -> None :
2026
+ """Send a global request"""
2027
+
2028
+ self .send_packet (MSG_GLOBAL_REQUEST , String (request ),
2029
+ Boolean (want_reply ), * args )
2030
+
2015
2031
async def _make_global_request (self , request : bytes ,
2016
2032
* args : bytes ) -> Tuple [int , SSHPacket ]:
2017
2033
"""Send a global request and wait for the response"""
@@ -2024,8 +2040,7 @@ async def _make_global_request(self, request: bytes,
2024
2040
2025
2041
self ._global_request_waiters .append (waiter )
2026
2042
2027
- self .send_packet (MSG_GLOBAL_REQUEST , String (request ),
2028
- Boolean (True ), * args )
2043
+ self ._send_global_request (request , * args , want_reply = True )
2029
2044
2030
2045
return await waiter
2031
2046
@@ -3266,6 +3281,8 @@ def __init__(self, loop: asyncio.AbstractEventLoop,
3266
3281
self ._server_host_key_algs : Optional [Sequence [bytes ]] = None
3267
3282
self ._server_host_key : Optional [SSHKey ] = None
3268
3283
3284
+ self ._server_host_keys_handler = options .server_host_keys_handler
3285
+
3269
3286
self ._username = options .username
3270
3287
self ._password = options .password
3271
3288
@@ -3924,6 +3941,80 @@ def _process_auth_agent_at_openssh_dot_com_open(
3924
3941
raise ChannelOpenError (OPEN_CONNECT_FAILED ,
3925
3942
'Auth agent forwarding disabled' )
3926
3943
3944
+ def _process_hostkeys_00_at_openssh_dot_com_global_request (
3945
+ self , packet : SSHPacket ) -> None :
3946
+ """Process a list of accepted server host keys"""
3947
+
3948
+ self .create_task (self ._finish_hostkeys (packet ))
3949
+
3950
+ async def _finish_hostkeys (self , packet : SSHPacket ) -> None :
3951
+ """Finish processing hostkeys global request"""
3952
+
3953
+ if not self ._server_host_keys_handler :
3954
+ self .logger .debug1 ('Ignoring server host key message: no handler' )
3955
+ self ._report_global_response (False )
3956
+ return
3957
+
3958
+ if self ._trusted_host_keys is None :
3959
+ self .logger .info ('Server host key not verified: handler disabled' )
3960
+ self ._report_global_response (False )
3961
+ return
3962
+
3963
+ added = []
3964
+ removed = list (self ._trusted_host_keys )
3965
+ retained = []
3966
+ revoked = []
3967
+ prove = []
3968
+
3969
+ while packet :
3970
+ try :
3971
+ key_data = packet .get_string ()
3972
+ key = decode_ssh_public_key (key_data )
3973
+
3974
+ if key in self ._revoked_host_keys :
3975
+ revoked .append (key )
3976
+ elif key in self ._trusted_host_keys :
3977
+ retained .append (key )
3978
+ removed .remove (key )
3979
+ else :
3980
+ prove .append ((key , String (key_data )))
3981
+ except KeyImportError :
3982
+ pass
3983
+
3984
+ if prove :
3985
+ pkttype , packet = await self ._make_global_request (
3986
+ b'hostkeys-prove-00@openssh.com' ,
3987
+ b'' .join (key_str for _ , key_str in prove ))
3988
+
3989
+ if pkttype == MSG_REQUEST_SUCCESS :
3990
+ prefix = String ('hostkeys-prove-00@openssh.com' ) + \
3991
+ String (self ._session_id )
3992
+
3993
+ for key , key_str in prove :
3994
+ sig = packet .get_string ()
3995
+
3996
+ if key .verify (prefix + key_str , sig ):
3997
+ added .append (key )
3998
+ else :
3999
+ self .logger .debug1 ('Server host key validation failed' )
4000
+ else :
4001
+ self .logger .debug1 ('Server host key prove request failed' )
4002
+
4003
+ packet .check_end ()
4004
+
4005
+ self .logger .info (f'Server host key report: { len (added )} added, '
4006
+ f'{ len (removed )} removed, { len (retained )} retained, '
4007
+ f'{ len (revoked )} revoked' )
4008
+
4009
+ result = self ._server_host_keys_handler (added , removed ,
4010
+ retained , revoked )
4011
+
4012
+ if inspect .isawaitable (result ):
4013
+ assert result is not None
4014
+ await result
4015
+
4016
+ self ._report_global_response (True )
4017
+
3927
4018
async def attach_x11_listener (self , chan : SSHClientChannel [AnyStr ],
3928
4019
display : Optional [str ],
3929
4020
auth_path : Optional [str ],
@@ -5594,6 +5685,7 @@ def __init__(self, loop: asyncio.AbstractEventLoop,
5594
5685
self ._options = options
5595
5686
5596
5687
self ._server_host_keys = options .server_host_keys
5688
+ self ._all_server_host_keys = options .all_server_host_keys
5597
5689
self ._server_host_key_algs = list (options .server_host_keys .keys ())
5598
5690
self ._known_client_hosts = options .known_client_hosts
5599
5691
self ._trust_client_host = options .trust_client_host
@@ -5719,6 +5811,17 @@ def get_server_host_key(self) -> Optional[SSHKeyPair]:
5719
5811
5720
5812
return self ._server_host_key
5721
5813
5814
+ def send_server_host_keys (self ) -> None :
5815
+ """Send list of available server host keys"""
5816
+
5817
+ if self ._all_server_host_keys :
5818
+ self .logger .info ('Sending server host keys' )
5819
+
5820
+ keys = [String (key ) for key in self ._all_server_host_keys .keys ()]
5821
+ self ._send_global_request (b'hostkeys-00@openssh.com' , * keys )
5822
+ else :
5823
+ self .logger .info ('Sending server host keys disabled' )
5824
+
5722
5825
def gss_kex_auth_supported (self ) -> bool :
5723
5826
"""Return whether GSS key exchange authentication is supported"""
5724
5827
@@ -6425,6 +6528,26 @@ def _process_tun_at_openssh_dot_com_open(
6425
6528
6426
6529
return chan , session
6427
6530
6531
+ def _process_hostkeys_prove_00_at_openssh_dot_com_global_request (
6532
+ self , packet : SSHPacket ) -> None :
6533
+ """Prove the server has private keys for all requested host keys"""
6534
+
6535
+ prefix = String ('hostkeys-prove-00@openssh.com' ) + \
6536
+ String (self ._session_id )
6537
+
6538
+ signatures = []
6539
+
6540
+ while packet :
6541
+ try :
6542
+ key_data = packet .get_string ()
6543
+ key = self ._all_server_host_keys [key_data ]
6544
+ signatures .append (String (key .sign (prefix + String (key_data ))))
6545
+ except (KeyError , KeyImportError ):
6546
+ self ._report_global_response (False )
6547
+ return
6548
+
6549
+ self ._report_global_response (b'' .join (signatures ))
6550
+
6428
6551
async def attach_x11_listener (self , chan : SSHServerChannel [AnyStr ],
6429
6552
auth_proto : bytes , auth_data : bytes ,
6430
6553
screen : int ) -> Optional [str ]:
@@ -7178,6 +7301,17 @@ class SSHClientConnectionOptions(SSHConnectionOptions):
7178
7301
caution, as it can result in a host key mismatch
7179
7302
if the client trusts only a subset of the host
7180
7303
keys the server might return.
7304
+ :param server_host_keys_handler: (optional)
7305
+ A `callable` or coroutine handler function which if set will be
7306
+ called when a global request from the server is received which
7307
+ provides an updated list of server host keys. The handler takes
7308
+ four arguments (added, removed, retained, and revoked), each of
7309
+ which is a list of SSHKey public keys, reflecting differences
7310
+ between what the server reported and what is currently matching
7311
+ in known_hosts.
7312
+
7313
+ .. note:: This handler will only be called when known
7314
+ host checking is enabled and the check succeeded.
7181
7315
:param x509_trusted_certs: (optional)
7182
7316
A list of certificates which should be trusted for X.509 server
7183
7317
certificate authentication. If no trusted certificates are
@@ -7513,6 +7647,7 @@ class SSHClientConnectionOptions(SSHConnectionOptions):
7513
7647
:type known_hosts: *see* :ref:`SpecifyingKnownHosts`
7514
7648
:type host_key_alias: `str`
7515
7649
:type server_host_key_algs: `str` or `list` of `str`
7650
+ :type server_host_keys_handler: `callable` or coroutine
7516
7651
:type x509_trusted_certs: *see* :ref:`SpecifyingCertificates`
7517
7652
:type x509_trusted_cert_paths: `list` of `str`
7518
7653
:type x509_purposes: *see* :ref:`SpecifyingX509Purposes`
@@ -7583,6 +7718,7 @@ class SSHClientConnectionOptions(SSHConnectionOptions):
7583
7718
known_hosts : KnownHostsArg
7584
7719
host_key_alias : Optional [str ]
7585
7720
server_host_key_algs : Union [str , Sequence [str ]]
7721
+ server_host_keys_handler : _ServerHostKeysHandler
7586
7722
username : str
7587
7723
password : Optional [str ]
7588
7724
client_host_keysign : Optional [str ]
@@ -7650,6 +7786,7 @@ def prepare(self, # type: ignore
7650
7786
known_hosts : KnownHostsArg = (),
7651
7787
host_key_alias : DefTuple [Optional [str ]] = (),
7652
7788
server_host_key_algs : _AlgsArg = (),
7789
+ server_host_keys_handler : _ServerHostKeysHandler = None ,
7653
7790
username : DefTuple [str ] = (), password : Optional [str ] = None ,
7654
7791
client_host_keysign : DefTuple [KeySignPath ] = (),
7655
7792
client_host_keys : Optional [_ClientKeysArg ] = None ,
@@ -7758,6 +7895,8 @@ def prepare(self, # type: ignore
7758
7895
_select_host_key_algs (server_host_key_algs ,
7759
7896
cast (DefTuple [str ], config .get ('HostKeyAlgorithms' , ())), [])
7760
7897
7898
+ self .server_host_keys_handler = server_host_keys_handler
7899
+
7761
7900
self .username = saslprep (cast (str , username if username != () else
7762
7901
config .get ('User' , local_username )))
7763
7902
@@ -7933,6 +8072,10 @@ class SSHServerConnectionOptions(SSHConnectionOptions):
7933
8072
:param server_host_certs: (optional)
7934
8073
A list of optional certificates which can be paired with the
7935
8074
provided server host keys.
8075
+ :param send_server_host_keys: (optional)
8076
+ Whether or not to send a list of the allowed server host keys
8077
+ for clients to use to update their known hosts like for the
8078
+ server.
7936
8079
:param passphrase: (optional)
7937
8080
The passphrase to use to decrypt server host keys if they are
7938
8081
encrypted, or a `callable` or coroutine which takes a filename
@@ -8174,6 +8317,7 @@ class SSHServerConnectionOptions(SSHConnectionOptions):
8174
8317
:type family: `socket.AF_UNSPEC`, `socket.AF_INET`, or `socket.AF_INET6`
8175
8318
:type server_host_keys: *see* :ref:`SpecifyingPrivateKeys`
8176
8319
:type server_host_certs: *see* :ref:`SpecifyingCertificates`
8320
+ :type send_server_host_keys: `bool`
8177
8321
:type passphrase: `str` or `bytes`
8178
8322
:type known_client_hosts: *see* :ref:`SpecifyingKnownHosts`
8179
8323
:type trust_client_host: `bool`
@@ -8227,6 +8371,8 @@ class SSHServerConnectionOptions(SSHConnectionOptions):
8227
8371
server_factory : _ServerFactory
8228
8372
server_version : bytes
8229
8373
server_host_keys : 'OrderedDict[bytes, SSHKeyPair]'
8374
+ all_server_host_keys : 'OrderedDict[bytes, SSHKeyPair]'
8375
+ send_server_host_keys : bool
8230
8376
known_client_hosts : KnownHostsArg
8231
8377
trust_client_host : bool
8232
8378
authorized_client_keys : DefTuple [Optional [SSHAuthorizedKeys ]]
@@ -8283,6 +8429,7 @@ def prepare(self, # type: ignore
8283
8429
keepalive_count_max : DefTuple [int ] = (),
8284
8430
server_host_keys : KeyPairListArg = (),
8285
8431
server_host_certs : CertListArg = (),
8432
+ send_server_host_keys : bool = False ,
8286
8433
passphrase : Optional [BytesOrStr ] = None ,
8287
8434
known_client_hosts : KnownHostsArg = None ,
8288
8435
trust_client_host : bool = False ,
@@ -8354,14 +8501,15 @@ def prepare(self, # type: ignore
8354
8501
server_host_certs , loop = loop )
8355
8502
8356
8503
self .server_host_keys = OrderedDict ()
8504
+ self .all_server_host_keys = OrderedDict ()
8357
8505
8358
8506
for keypair in server_keys :
8359
8507
for alg in keypair .host_key_algorithms :
8360
- if alg in self .server_host_keys :
8361
- raise ValueError ('Multiple keys of type %s found' %
8362
- alg .decode ('ascii' ))
8508
+ if alg not in self .server_host_keys :
8509
+ self .server_host_keys [alg ] = keypair
8363
8510
8364
- self .server_host_keys [alg ] = keypair
8511
+ if send_server_host_keys :
8512
+ self .all_server_host_keys [keypair .public_data ] = keypair
8365
8513
8366
8514
self .known_client_hosts = known_client_hosts
8367
8515
self .trust_client_host = trust_client_host
0 commit comments