You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Server: concurrent connections share the host keypair's signature algorithm, so an RSA host key can sign with the wrong one #834
(asyncssh/connection.py, still the same on develop at 3d3331c; we hit it with 2.21.0, and 2.24.1 has the same code.)
self._server_host_keys comes from the SSHServerConnectionOptions, which every connection accepted by a listener shares. Its rsa-sha2-256, rsa-sha2-512 and ssh-rsa entries are one keypair object. Key exchange spans several packets, so with two connections on one event loop:
Connection A negotiates rsa-sha2-512; set_sig_algorithm(b'rsa-sha2-512') runs.
Connection B negotiates rsa-sha2-256 before A has signed; set_sig_algorithm(b'rsa-sha2-256') runs on the same object.
A signs its exchange hash with rsa-sha2-256, while its client expects rsa-sha2-512.
The client rightly rejects the signature: OpenSSH reports "incorrect signature", and our client (SshSharp, a .NET SSH client) reports that the signature over the exchange hash is invalid. The connection fails, though nothing is wrong on either side.
How we hit it: in our interop CI, 6 of 100 stress iterations failed against an AsyncSSH server, with several clients connecting at once and preferring different RSA signature algorithms. In every failure, the signature had been made with the other RSA algorithm.
Workaround: our test server wraps choose_server_host_key so that each connection works on a shallow copy of the keypair. set_sig_algorithm only sets instance attributes, so a copy is enough:
Suggested fix: keep the chosen signature algorithm per connection: copy the keypair when choosing it, or store the algorithm on the connection and pass it to sign.
The client side may have the same pattern: SSHClientConnection._choose_signature_alg calls keypair.set_sig_algorithm(alg) on a client key, and client keys can be shared through a reused SSHClientConnectionOptions. We haven't tested that path.
Interesting! In most cases, key pairs would not be shared like this, as a new key object would be created for each key loaded by load_keypairs. However, it could happen if you explicitly loaded a key pair and passed that in as arguments to something like the client or server key lists. In that case, the load_keypairs() function used by the SSH connection options classes could end up using the same keypair object for multiple connections.
You might want to give the following a try:
diff --git a/asyncssh/public_key.py b/asyncssh/public_key.py
index f36111b..7205ac4 100644
--- a/asyncssh/public_key.py+++ b/asyncssh/public_key.py@@ -22,6 +22,7 @@
import asyncio
import binascii
+import copy
import inspect
import os
import re
@@ -3647,6 +3648,8 @@ def load_keypairs(
cert = SSHX509CertificateChain.construct_from_certs(certs)
if isinstance(key, SSHKeyPair):
+ key = copy.copy(key)+
if cert:
key.set_certificate(cert)
If this works for you, let me know and I'll include it in the next release.
Thanks for looking into this and for the suggestion! It looks like copying in load_keypairs() isn't quite enough: listen() builds the options once, so the copy is still shared by every connection on the listener. I've opened #835, which copies the key per connection instead, with tests. Happy to adjust it however you prefer.
Activity
ronf commentedon Oct 25, 2026
Interesting! In most cases, key pairs would not be shared like this, as a new key object would be created for each key loaded by load_keypairs. However, it could happen if you explicitly loaded a key pair and passed that in as arguments to something like the client or server key lists. In that case, the load_keypairs() function used by the SSH connection options classes could end up using the same keypair object for multiple connections.
You might want to give the following a try:
If this works for you, let me know and I'll include it in the next release.
zcsizmadia commentedon Oct 25, 2026
Thanks for looking into this and for the suggestion! It looks like copying in
load_keypairs()isn't quite enough:listen()builds the options once, so the copy is still shared by every connection on the listener. I've opened #835, which copies the key per connection instead, with tests. Happy to adjust it however you prefer.