GitHub Advisoryadvisory · crates.io
GHSA-j7h2-66j4-ghjf A russh SSH server accepts a publickey authentication whose signature uses a different algorithm than the one named in the request
LOW
Official page ↗
- Aliases
- CVE-2026-111062
- Ecosystem
- crates.io
- Packages
- russh
- Severity
- Low
- CVSS vector
- CVSS:4.0/AV:N/AC:H/AT:P/PR:L/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
- Weaknesses
- CWE-347
- Reviewed
- 2026-10-19
Affected packages
• crates.io russh: >= 0.34.0-beta.1, < 0.64.0 → fixed in 0.64.0
Description
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 {
Ok(Auth::Accept)
}
async fn auth_publickey(
&mut self,
_user: &str,
_public_key: &PublicKey,
) -> Result {
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,
announced: &str,
hash: Option,
) -> Vec {
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(_) => {
+ ::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.
Related
Alias · not in database (1)
Weakness · CWE (1)
- CWE-347 Improper Verification of Cryptographic Signature weakness