---
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'
---

## Overview

### 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.

## Affected packages

- `russh >= 0.34.0-beta.1, < 0.64.0`

## Remediation

Upgrade to a patched release:

- `russh 0.64.0`
