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)
Nessuno ha ancora preso questa issue.
- Lingua principale
- C#
- Stelle
- 4.4k
- Fork
- 993
- Merge medio
- 9g 21h
- PR unite (30g)
- 1
Descrizione
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.
Guida per i contributori
Apri la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Direzione di ricerca
Inizia con la riproduzione minima di Program.cs e confronta SSH.NET 2025.0.0 con 2025.1.0 rispetto a RoboFTP e al server di controllo OpenSSH. Quindi esamina KeyExchange.Start, KeyExchange.ValidateExchangeHash, KeyExchangeECDH.cs e il percorso di verifica della firma RSA. Il lavoro è completato quando il server legacy raggiunge HostKeyReceived e si connette con 2025.1.0 senza introdurre regressioni nel caso del server moderno.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- csharp
- Ambito
- networking
- Tipo di issue
- Bug
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Stato di attività
- Attiva
- Chiarezza
- Abbastanza chiara
- Idoneità per principianti
- 55/100