Host-key verification regression in 2025.1.0: KeyExchange.Finish throws "Host key could not be verified." against legacy non-OpenSSH SFTP servers (works on 2025.0.0)
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 55/100
- Tipo de issue
- Error
- Claridad
- Bastante claro
- Estado de actividad
- Activo
- Stack tecnológico
- csharp
- Área
- networking
Línea de trabajo
Comienza con la reproducción mínima de Program.cs y compara SSH.NET 2025.0.0 con 2025.1.0 frente a RoboFTP y el servidor de control de OpenSSH. Después, inspecciona KeyExchange.Start, KeyExchange.ValidateExchangeHash, KeyExchangeECDH.cs y la ruta de verificación de firmas RSA. Se considera terminado cuando el servidor heredado alcanza HostKeyReceived y se conecta en 2025.1.0 sin introducir una regresión en el caso del servidor moderno.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
SftpClient.Connect()throwsSshConnectionException: Host key could not be verified.insideKeyExchange.Finishagainst a RoboFTP v3.5.2 (2019) SFTP server with SSH.NET **2025.1.0**. TheHostKeyReceived` event never fires — verification fails before our event handler can inspect the fingerprint.
The same code path works with SSH.NET 2025.0.0 and 2024.2.0 against the same server, and works with 2025.1.0 against a modern OpenSSH server (atmoz/sftp:alpine). All other variablariables (private key, fingerprint, target host, .NET 10 runtime, Linux client) are identical across runs.
This appears to be a regression introduced between 2025.0.0 and 2025.1.0 — most likely in the major KEX-layer refactor (KeyExchange.cs +61/-55, consolidation of 10 DH classes, KeyExchangeECDH.cs, KeyExchangeECCurve25519.cs, KeyExchangeMLKem768X25519Sha256.cs, KeyExchangeSNtruP761X25519Sha512.cs all touched).
Environment
- SSH.NET versions tested: 2024.2.0, 2025.0.0, 2025.1.0
- Runtime: .NET 10 (
net10.0, Linux x64 / WSL2) - Failing server: RoboFTP v3.5.2 (2019), advertises older OpenSSH-compatible algorithms:
- KEX offered:
ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group18-sha512, diffie-hellman-group16-sha512, diffie-hellman-group-exchange-sha256, diffie-hellman-group14-sha1, diffie-hellman-group1-sha1 - Host key: RSA 2048
- HKA chosen (via
ssh -vvv):rsa-sha2-512
- KEX offered:
- Control server (works on 2025.1.0):
atmoz/sftp:alpine(modern OpenSSH 9.x) - OpenSSH client: connects successfully to the same RoboFTP server with the same key — server-side is not at fault.
Stack trace
Renci.SshNet.Common.SshConnectionException: Host key could not be verified.
at Renci.SshNet.Security.KeyExchange.Finish()
at Renci.SshNet.Security.KeyExchangeECDH.Finish()
at Renci.SshNet.Security.KeyExchangeECDH.Session_KeyExchangeEcdhReplyMessageReceived(Object sender, MessageEventArgs`1 e)
at Renci.SshNet.Session.OnKeyExchangeEcdhReplyMessageReceived(KeyExchangeEcdhReplyMessage message)
at Renci.SshNet.Messages.Transport.KeyExchangeEcdhReplyMessage.Process(Session session)
at Renci.SshNet.Session.MessageListener()
--- End of stack trace from previous location ---
at Renci.SshNet.Session.WaitOnHandle(WaitHandle waitHandle, TimeSpan timeout)
at Renci.SshNet.Session.Connect()
at Renci.SshNet.BaseClient.CreateAndConnectSession()
at Renci.SshNet.BaseClient.Connect()
HostKeyReceived is never raised — the failure happens inside KeyExchange.ValidateExchangeHash (called from Finish), so CanTrustHostKey is never reached.
Reproduction (minimal, ~80 lines)
SshNetRepro.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SSH.NET" Version="2025.1.0" />
</ItemGroup>
</Project>
Program.cs:
using Renci.SshNet;
using Renci.SshNet.Common;
string host = Environment.GetEnvironmentVariable("SFTP_HOST")!;
int port = int.Parse(Environment.GetEnvironmentVariable("SFTP_PORT") ?? "22");
string username = Environment.GetEnvironmentVariable("SFTP_USER")!;
string keyPath = Environment.GetEnvironmentVariable("SFTP_KEY")!;
string expectedFingerprint = Environment.GetEnvironmentVariable("SFTP_FINGERPRINT")!.TrimEnd('=');
string? passphrase = Environment.GetEnvironmentVariable("SFTP_PASSPHRASE");
Console.WriteLine($"SSH.NET: {typeof(SftpClient).Assembly.GetName().Version}");
PrivateKeyFile key = string.IsNullOrEmpty(passphrase)
? new PrivateKeyFile(keyPath)
: new PrivateKeyFile(keyPath, passphrase);
ConnectionInfo info = new(host, port, username,
new PrivateKeyAuthenticationMethod(username, key));
using SftpClient client = new(info);
client.HostKeyReceived += (_, e) =>
{
string fp = (e.FingerPrintSHA256 ?? "").TrimEnd('=');
Console.WriteLine($"HostKeyReceived fired — received='{fp}' match={fp == expectedFingerprint}");
e.CanTrust = fp == expectedFingerprint;
};
try
{
client.Connect();
Console.WriteLine("Connect() succeeded");
}
catch (SshConnectionException ex)
{
Console.Error.WriteLine($"FAIL: {ex.Message}");
Console.Error.WriteLine(ex.StackTrace);
}
Run with env vars pointing at the affected server. Swap the SSH.NET version to A/B-test:
| SSH.NET | Result against RoboFTP server | Result against atmoz/sftp |
|---|---|---|
| 2024.2.0 | ✅ HostKeyReceived fires, connect succeeds | ✅ |
| 2025.0.0 | ✅ HostKeyReceived fires, connect succeeds | ✅ |
| 2025.1.0 | ❌ HostKeyReceived NEVER fires, KeyExchange.Finish throws |
✅ |
KEX algorithm bisect on 2025.1.0 vs RoboFTP
Restricting ConnectionInfo.KeyExchangeAlgorithms to one category at a time:
| KEX restriction | Negotiated? | Outcome |
|---|---|---|
ecdh-sha2-nistp256 |
yes | ❌ KeyExchange.Finish |
ecdh-sha2-nistp384 |
yes | ❌ KeyExchange.Finish |
ecdh-sha2-nistp521 |
yes | ❌ KeyExchange.Finish |
diffie-hellman-group16-sha512 |
yes | ❌ KeyExchange.Finish |
diffie-hellman-group-exchange-sha256 |
yes | ❌ KeyExchange.Finish |
curve25519-sha256 |
no (server doesn't offer) | "No matching key exchange algorithm" |
mlkem768x25519-sha256 |
no | "No matching key exchange algorithm" |
sntrup761x25519-sha512 |
no | "No matching key exchange algorithm" |
Every KEX the server actually negotiates fails identically → not an algorithm-specific bug; the regression is in shared code (likely KeyExchange.ValidateExchangeHash or the signature-verification path it depends on).
What is ruled out
- Not a server-side fault: OpenSSH client (
sftp -i ... user@server) connects successfully to the same host with the same key. - Not a fingerprint mismatch:
HostKeyReceivedis never raised on 2025.1.0 — verification fails before fingerprint comparison. The same fingerprint matches and succeeds on 2025.0.0 (event fires,CanTrust = true). - Not host-key-algorithm related: explicitly restricting
HostKeyAlgorithms(RSA-only, including/excluding*-cert-v01@openssh.comvariants) does not change the outcome on the affected server. The same algorithm restriction works onatmoz/sftp. - Not a single KEX algorithm: see bisect table — every KEX that negotiates fails identically.
- Not a .NET 10 crypto issue alone: 2025.0.0 on the same .NET 10 runtime works fine against the same server.
Suspected region
The 2025.0.0 → 2025.1.0 diff touches KeyExchange.cs substantially (+61/-55). One notable change is that host-key-algorithm selection was moved into the base class KeyExchange.Start():
// New in 2025.1.0: KeyExchange.Start()
var hostKeyAlgorithmName = (from b in session.ConnectionInfo.HostKeyAlgorithms.Keys
from a in message.ServerHostKeyAlgorithms
where a == b
select a).FirstOrDefault();
session.ConnectionInfo.CurrentHostKeyAlgorithm = hostKeyAlgorithmName;
_hostKeyAlgorithmFactory = session.ConnectionInfo.HostKeyAlgorithms[hostKeyAlgorithmName];
The 10 DH-classes-into-one consolidation (KeyExchangeDiffieHellmanGroupExchange.cs +244, several KeyExchangeDiffieHellmanGroup*.cs removed) and the EC class rewrites (KeyExchangeEC.cs, KeyExchangeECCurve25519.cs) are the other large changes in the same release.
It's plausible that one of these refactors changed how the exchange hash is computed or how the server's signature blob is decoded for at least the older RoboFTP-style server's format, while behaving identically for modern OpenSSH.
What would help
If a maintainer can point at the specific commit/PR in the 2025.0.0 → 2025.1.0 range that touched ValidateExchangeHash or RSA signature verification, I can also test intermediate develop-branch builds locally — happy to do so.
Filed in the meantime: a downstream pin to SSH.NET 2025.0.0.
- Lenguaje dominante
- C#
- Estrellas
- 4.4k
- Forks
- 993
- Merge medio
- 9 d 21 h
- PR fusionados (30 d)
- 1
Guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de sshnet/SSH.NET
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
-
Dificultad 3/5 1-2 días Aptitud para principiantes 68/100
-
Using SshClient on Linux under Wine throws System.Security.Cryptography.CryptographicException Abierto
Dificultad 4/5 3-5 días Aptitud para principiantes 48/100
-
Dificultad 3/5 1-2 días Aptitud para principiantes 67/100
-
sshnet/SSH.NET#1795 · 1 comentario · 1 reacción · 2 asignados ·
Todos los issues de sshnet/SSH.NET
Issues similares
-
bug
Dificultad 1/5 Menos de una hora Aptitud para principiantes 75/100
sillsdev/languageforge-lexbox#2665 ·
-
bug documentation frontend
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
azurenoops/spin_agent#975 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 86/100
-
:watch: Not Triaged 11.0 fundamentals/subsvc
Dificultad 2/5 1-3 horas Aptitud para principiantes 92/100
dotnet/AspNetCore.Docs#37699 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
SubtitleEdit/subtitleedit#15108 · 1 comentario ·