Skip to content

Commit caac95c

Browse files
authored
Check host key algorithms before continuing key exchange (#1642)
The library currently does not check for matching host key algorithms until needed at the end of the key exchange, in contrast to other algorithm types which are checked beforehand. This leads to confusing or uninformative errors, normally from the server (correctly) closing the connection. This change moves that check alongside the rest of them, and also improves the error messages that arise from no matching algorithms.
1 parent 26bc749 commit caac95c

File tree

3 files changed

+74
-57
lines changed

3 files changed

+74
-57
lines changed

src/Renci.SshNet/Security/KeyExchange.cs

Lines changed: 55 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ namespace Renci.SshNet.Security
1919
public abstract class KeyExchange : Algorithm, IKeyExchange
2020
{
2121
private readonly ILogger _logger;
22+
private Func<byte[], KeyHostAlgorithm> _hostKeyAlgorithmFactory;
2223
private CipherInfo _clientCipherInfo;
2324
private CipherInfo _serverCipherInfo;
2425
private HashInfo _clientHashInfo;
@@ -81,6 +82,33 @@ public virtual void Start(Session session, KeyExchangeInitMessage message, bool
8182
SendMessage(session.ClientInitMessage);
8283
}
8384

85+
// Determine host key algorithm
86+
var hostKeyAlgorithmName = (from b in session.ConnectionInfo.HostKeyAlgorithms.Keys
87+
from a in message.ServerHostKeyAlgorithms
88+
where a == b
89+
select a).FirstOrDefault();
90+
91+
if (_logger.IsEnabled(LogLevel.Trace))
92+
{
93+
_logger.LogTrace("[{SessionId}] Host key algorithm: we offer {WeOffer}",
94+
Session.SessionIdHex,
95+
session.ConnectionInfo.HostKeyAlgorithms.Keys.Join(","));
96+
97+
_logger.LogTrace("[{SessionId}] Host key algorithm: they offer {TheyOffer}",
98+
Session.SessionIdHex,
99+
message.ServerHostKeyAlgorithms.Join(","));
100+
}
101+
102+
if (hostKeyAlgorithmName is null)
103+
{
104+
throw new SshConnectionException(
105+
$"No matching host key algorithm (server offers {message.ServerHostKeyAlgorithms.Join(",")})",
106+
DisconnectReason.KeyExchangeFailed);
107+
}
108+
109+
session.ConnectionInfo.CurrentHostKeyAlgorithm = hostKeyAlgorithmName;
110+
_hostKeyAlgorithmFactory = session.ConnectionInfo.HostKeyAlgorithms[hostKeyAlgorithmName];
111+
84112
// Determine client encryption algorithm
85113
var clientEncryptionAlgorithmName = (from b in session.ConnectionInfo.Encryptions.Keys
86114
from a in message.EncryptionAlgorithmsClientToServer
@@ -98,9 +126,11 @@ from a in message.EncryptionAlgorithmsClientToServer
98126
message.EncryptionAlgorithmsClientToServer.Join(","));
99127
}
100128

101-
if (string.IsNullOrEmpty(clientEncryptionAlgorithmName))
129+
if (clientEncryptionAlgorithmName is null)
102130
{
103-
throw new SshConnectionException("Client encryption algorithm not found", DisconnectReason.KeyExchangeFailed);
131+
throw new SshConnectionException(
132+
$"No matching client encryption algorithm (server offers {message.EncryptionAlgorithmsClientToServer.Join(",")})",
133+
DisconnectReason.KeyExchangeFailed);
104134
}
105135

106136
session.ConnectionInfo.CurrentClientEncryption = clientEncryptionAlgorithmName;
@@ -123,9 +153,11 @@ from a in message.EncryptionAlgorithmsServerToClient
123153
message.EncryptionAlgorithmsServerToClient.Join(","));
124154
}
125155

126-
if (string.IsNullOrEmpty(serverDecryptionAlgorithmName))
156+
if (serverDecryptionAlgorithmName is null)
127157
{
128-
throw new SshConnectionException("Server decryption algorithm not found", DisconnectReason.KeyExchangeFailed);
158+
throw new SshConnectionException(
159+
$"No matching server encryption algorithm (server offers {message.EncryptionAlgorithmsServerToClient.Join(",")})",
160+
DisconnectReason.KeyExchangeFailed);
129161
}
130162

131163
session.ConnectionInfo.CurrentServerEncryption = serverDecryptionAlgorithmName;
@@ -150,9 +182,11 @@ from a in message.MacAlgorithmsClientToServer
150182
message.MacAlgorithmsClientToServer.Join(","));
151183
}
152184

153-
if (string.IsNullOrEmpty(clientHmacAlgorithmName))
185+
if (clientHmacAlgorithmName is null)
154186
{
155-
throw new SshConnectionException("Client HMAC algorithm not found", DisconnectReason.KeyExchangeFailed);
187+
throw new SshConnectionException(
188+
$"No matching client MAC algorithm (server offers {message.MacAlgorithmsClientToServer.Join(",")})",
189+
DisconnectReason.KeyExchangeFailed);
156190
}
157191

158192
session.ConnectionInfo.CurrentClientHmacAlgorithm = clientHmacAlgorithmName;
@@ -178,9 +212,11 @@ from a in message.MacAlgorithmsServerToClient
178212
message.MacAlgorithmsServerToClient.Join(","));
179213
}
180214

181-
if (string.IsNullOrEmpty(serverHmacAlgorithmName))
215+
if (serverHmacAlgorithmName is null)
182216
{
183-
throw new SshConnectionException("Server HMAC algorithm not found", DisconnectReason.KeyExchangeFailed);
217+
throw new SshConnectionException(
218+
$"No matching server MAC algorithm (server offers {message.MacAlgorithmsServerToClient.Join(",")})",
219+
DisconnectReason.KeyExchangeFailed);
184220
}
185221

186222
session.ConnectionInfo.CurrentServerHmacAlgorithm = serverHmacAlgorithmName;
@@ -204,9 +240,11 @@ from a in message.CompressionAlgorithmsClientToServer
204240
message.CompressionAlgorithmsClientToServer.Join(","));
205241
}
206242

207-
if (string.IsNullOrEmpty(compressionAlgorithmName))
243+
if (compressionAlgorithmName is null)
208244
{
209-
throw new SshConnectionException("Compression algorithm not found", DisconnectReason.KeyExchangeFailed);
245+
throw new SshConnectionException(
246+
$"No matching client compression algorithm (server offers {message.CompressionAlgorithmsClientToServer.Join(",")})",
247+
DisconnectReason.KeyExchangeFailed);
210248
}
211249

212250
session.ConnectionInfo.CurrentClientCompressionAlgorithm = compressionAlgorithmName;
@@ -229,9 +267,11 @@ from a in message.CompressionAlgorithmsServerToClient
229267
message.CompressionAlgorithmsServerToClient.Join(","));
230268
}
231269

232-
if (string.IsNullOrEmpty(decompressionAlgorithmName))
270+
if (decompressionAlgorithmName is null)
233271
{
234-
throw new SshConnectionException("Decompression algorithm not found", DisconnectReason.KeyExchangeFailed);
272+
throw new SshConnectionException(
273+
$"No matching server compression algorithm (server offers {message.CompressionAlgorithmsServerToClient.Join(",")})",
274+
DisconnectReason.KeyExchangeFailed);
235275
}
236276

237277
session.ConnectionInfo.CurrentServerCompressionAlgorithm = decompressionAlgorithmName;
@@ -245,7 +285,7 @@ public virtual void Finish()
245285
{
246286
if (!ValidateExchangeHash())
247287
{
248-
throw new SshConnectionException("Key exchange negotiation failed.", DisconnectReason.KeyExchangeFailed);
288+
throw new SshConnectionException("Host key could not be verified.", DisconnectReason.KeyExchangeFailed);
249289
}
250290

251291
SendMessage(new NewKeysMessage());
@@ -449,40 +489,9 @@ private protected bool ValidateExchangeHash(byte[] encodedKey, byte[] encodedSig
449489
{
450490
var exchangeHash = CalculateHash();
451491

452-
// We need to inspect both the key and signature format identifers to find the correct
453-
// HostAlgorithm instance. Example cases:
454-
455-
// Key identifier Signature identifier | Algorithm name
456-
// ssh-rsa ssh-rsa | ssh-rsa
457-
// ssh-rsa rsa-sha2-256 | rsa-sha2-256
458-
459-
460-
461-
var signatureData = new KeyHostAlgorithm.SignatureKeyData();
462-
signatureData.Load(encodedSignature);
463-
464-
string keyName;
465-
using (var keyReader = new SshDataStream(encodedKey))
466-
{
467-
keyName = keyReader.ReadString();
468-
}
469-
470-
string algorithmName;
471-
472-
if (signatureData.AlgorithmName.StartsWith("rsa-sha2", StringComparison.Ordinal))
473-
{
474-
algorithmName = keyName.Replace("ssh-rsa", signatureData.AlgorithmName);
475-
}
476-
else
477-
{
478-
algorithmName = keyName;
479-
}
480-
481-
var keyAlgorithm = Session.ConnectionInfo.HostKeyAlgorithms[algorithmName](encodedKey);
482-
483-
Session.ConnectionInfo.CurrentHostKeyAlgorithm = algorithmName;
492+
var keyAlgorithm = _hostKeyAlgorithmFactory(encodedKey);
484493

485-
return keyAlgorithm.VerifySignatureBlob(exchangeHash, signatureData.Signature) && CanTrustHostKey(keyAlgorithm);
494+
return keyAlgorithm.VerifySignature(exchangeHash, encodedSignature) && CanTrustHostKey(keyAlgorithm);
486495
}
487496

488497
/// <summary>

src/Renci.SshNet/ServiceFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ from s in serverAlgorithms
101101

102102
if (keyExchangeAlgorithmFactory is null)
103103
{
104-
throw new SshConnectionException("Failed to negotiate key exchange algorithm.", DisconnectReason.KeyExchangeFailed);
104+
throw new SshConnectionException($"No matching key exchange algorithm (server offers {serverAlgorithms.Join(",")})", DisconnectReason.KeyExchangeFailed);
105105
}
106106

107107
return keyExchangeAlgorithmFactory();

test/Renci.SshNet.IntegrationTests/ConnectivityTests.cs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -415,16 +415,24 @@ public void Common_HostKeyValidation_Failure()
415415
{
416416
client.HostKeyReceived += (sender, e) => { e.CanTrust = false; };
417417

418-
try
419-
{
420-
client.Connect();
421-
Assert.Fail();
422-
}
423-
catch (SshConnectionException ex)
424-
{
425-
Assert.IsNull(ex.InnerException);
426-
Assert.AreEqual("Key exchange negotiation failed.", ex.Message);
427-
}
418+
var ex = Assert.Throws<SshConnectionException>(client.Connect);
419+
420+
Assert.AreEqual(DisconnectReason.KeyExchangeFailed, ex.DisconnectReason);
421+
Assert.AreEqual("Host key could not be verified.", ex.Message);
422+
}
423+
}
424+
425+
[TestMethod]
426+
public void Common_HostKeyAlgorithms_NoMatch()
427+
{
428+
using (var client = new SshClient(_connectionInfoFactory.Create()))
429+
{
430+
client.ConnectionInfo.HostKeyAlgorithms.Clear();
431+
432+
var ex = Assert.Throws<SshConnectionException>(client.Connect);
433+
434+
Assert.AreEqual(DisconnectReason.KeyExchangeFailed, ex.DisconnectReason);
435+
Assert.IsTrue(ex.Message.StartsWith("No matching host key algorithm"), ex.Message);
428436
}
429437
}
430438

0 commit comments

Comments
 (0)