{"id":"CVE-2026-111062","aliases":["GHSA-j7h2-66j4-ghjf"],"title":"A russh SSH server accepts a publickey authentication whose signature uses a different algorithm than the one named in the request","summary":"A russh SSH server accepts a publickey authentication whose signature uses a different algorithm than the one named in the request","severity":"low","cwe":["CWE-347"],"vendor":"russh","product":"russh","ecosystem":"rust","affected":["russh >= 0.34.0-beta.1, < 0.64.0"],"patched":["russh 0.64.0"],"published":"2026-10-19","updated":"2026-10-19","sourceUpdated":"2026-10-19T17:06:30Z","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-j7h2-66j4-ghjf","references":[{"url":"https://github.com/Eugeny/russh/security/advisories/GHSA-j7h2-66j4-ghjf"},{"url":"https://github.com/Eugeny/russh/commit/a0ded76bdc352f8fbeaaeb175fb2a0c586fbc8f2"},{"url":"https://github.com/Eugeny/russh/releases/tag/v0.64.0"},{"url":"https://github.com/advisories/GHSA-j7h2-66j4-ghjf"}],"tags":["ghsa","rust"],"ingestedAt":"2026-10-19T17:33:42.442Z","slug":"CVE-2026-111062","body":"## Overview\n\n### Details\n\nIn `russh/src/server/encrypted.rs`, `server_read_auth_request_pk` (0.63.3, around lines 880-990):\n\n* The requested algorithm name (`pubkey_algo`) is read and only used to decode the key: `PublicKeyOrCertificate::decode(&pubkey_algo, &pubkey_key)`. The derived `hash_alg` is then dropped (`PublicKeyOrCertificate::PublicKey { ref key, .. }`).\n* 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()`).\n* `pubkey_algo` and `sig.algorithm()` are never compared.\n\nSo 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::auth_publickey` is called.\n\n**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.\n\nThe mechanism changed over time, but the outcome is the same: the hash is always taken from the signature blob, never checked against the request.\n\n* **Up to 0.46:** after reading the algorithm name inside the signature blob, russh overwrites the key's hash with it (`pubkey.set_algorithm(algo_)`), then verifies.\n* **From 0.47/0.48 (move to `ssh-key`):** the signature is decoded with its own algorithm (`sig_workaround::Sig::decode`, later `Signature::decode`) and verified with it.\n\nConfirmed 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.\n\n### PoC\n\nA unit test in the existing test module of `russh/src/server/encrypted.rs`. It uses the module's own helpers (`test_auth_session`, `sign_with_hash_alg`, `PrivateKeyWithHashAlg`) and needs no network or configuration.\n\n1. Check out russh 0.63.3, or `main`.\n2. Paste the code below inside `mod tests { ... }` of `russh/src/server/encrypted.rs`.\n3. Run `cargo test --lib poc_sha1`.\n\n```rust\n// ---- PoC: signature algorithm not checked against the requested one ----\n\n/// Records whether `auth_publickey` (called only after the signature\n/// verified) was reached.\n#[derive(Default)]\nstruct AcceptAnyKey {\n    authenticated: bool,\n}\n\nimpl Handler for AcceptAnyKey {\n    type Error = Error;\n\n    async fn auth_publickey_offered(\n        &mut self,\n        _user: &str,\n        _public_key: &PublicKey,\n    ) -> Result<Auth, Self::Error> {\n        Ok(Auth::Accept)\n    }\n\n    async fn auth_publickey(\n        &mut self,\n        _user: &str,\n        _public_key: &PublicKey,\n    ) -> Result<Auth, Self::Error> {\n        self.authenticated = true;\n        Ok(Auth::Accept)\n    }\n}\n\n/// A signed publickey request that names `announced` as its public key\n/// algorithm, but is signed with `hash` (`None` = `ssh-rsa`, SHA-1).\nfn signed_request_announcing(\n    user: &str,\n    private_key: Arc<PrivateKey>,\n    announced: &str,\n    hash: Option<crate::keys::HashAlg>,\n) -> Vec<u8> {\n    let mut packet = Vec::new();\n    packet.push(msg::USERAUTH_REQUEST);\n    user.encode(&mut packet).unwrap();\n    \"ssh-connection\".encode(&mut packet).unwrap();\n    \"publickey\".encode(&mut packet).unwrap();\n    1u8.encode(&mut packet).unwrap();\n    announced.encode(&mut packet).unwrap();\n    private_key.public_key().to_bytes().unwrap().encode(&mut packet).unwrap();\n\n    let mut signed = Vec::new();\n    CryptoVec::new().as_ref().encode(&mut signed).unwrap();\n    signed.extend_from_slice(&packet);\n    let signature =\n        sign_with_hash_alg(&PrivateKeyWithHashAlg::new(private_key, hash), &signed).unwrap();\n    signature.encode(&mut packet).unwrap();\n    packet\n}\n\n#[tokio::test]\nasync fn poc_sha1_signature_accepted_when_rsa_sha2_512_is_announced() {\n    let rsa = ssh_key::private::RsaKeypair::random(&mut rand::rng(), 2048).unwrap();\n    let private = Arc::new(PrivateKey::from(rsa));\n    let mut session = test_auth_session();\n    let mut handler = AcceptAnyKey::default();\n\n    // The request says rsa-sha2-512; the signature blob is ssh-rsa (SHA-1).\n    let packet = signed_request_announcing(\"alice\", private, \"rsa-sha2-512\", None);\n    session.process_packet(&mut handler, &packet).await.unwrap();\n\n    // RFC 8332 §3: the signature algorithm must be the requested one, so\n    // this request must be rejected. On russh 0.63.3 it is accepted.\n    assert!(\n        !handler.authenticated,\n        \"an ssh-rsa (SHA-1) signature was accepted for a request announcing rsa-sha2-512\"\n    );\n}\n```\n\nResult on unmodified 0.63.3:\n\n```\ntest server::encrypted::tests::poc_sha1_signature_accepted_when_rsa_sha2_512_is_announced ... FAILED\npanicked at src/server/encrypted.rs: an ssh-rsa (SHA-1) signature was accepted for a request announcing rsa-sha2-512\n```\n\nWith the fix below, the test passes and the whole library suite still passes: `cargo test --lib` gives 186 passed.\n\nSuggested fix: reject a mismatch through the existing `reject_auth_request(until, ...)` path, so the constant rejection time is kept.\n\n```diff\n--- a/russh/src/server/encrypted.rs\n+++ b/russh/src/server/encrypted.rs\n@@ -1023,6 +1023,21 @@\n                     map_err!(ensure_end(&signature_reader))?;\n                     map_err!(ensure_end(r))?;\n \n+                    // RFC 8332 §3: the signature must use the algorithm the\n+                    // request names.\n+                    let offered_algorithm = match pk_or_cert {\n+                        PublicKeyOrCertificate::PublicKey { .. } => ssh_key::Algorithm::new(&pubkey_algo),\n+                        PublicKeyOrCertificate::Certificate(_) => {\n+                            <ssh_key::Algorithm as crate::helpers::AlgorithmExt>::new_certificate_ext(&pubkey_algo)\n+                        }\n+                    };\n+                    if offered_algorithm.ok().as_ref() != Some(&sig.algorithm()) {\n+                        debug!(\"signature algorithm does not match the requested one\");\n+                        auth_user.clear();\n+                        reject_auth_request(until, &mut self.write, auth_request).await?;\n+                        return Ok(());\n+                    }\n+\n                     let is_valid = if accepted_probe_matches && user == auth_user {\n                         true\n                     } else {\n```\n\n### Impact\n\nThis is a missing check on the signature algorithm (improper verification of a cryptographic signature's algorithm, CWE-347) in the server's `publickey` authentication.\n\nWho 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:\n\n* a server that refuses SHA-1 (`ssh-rsa`) signatures, as OpenSSH does by default since 8.8;\n* a server that logs which algorithm a client authenticated with.\n\nSuch 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.\n\nNot impacted:\n\n* russh clients;\n* authentication strength beyond the choice of hash, since a valid signature by the private key is still required;\n* 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.\n\n## Affected packages\n\n- `russh >= 0.34.0-beta.1, < 0.64.0`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `russh 0.64.0`","depth":"sunlit","depthScore":14,"depthScoreParts":{"impact":13.8,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}