mirror of
https://github.com/sshnet/SSH.NET.git
synced 2026-09-10 09:15:47 +00:00
Add support for mlkem768x25519-sha256 key exchange method (#1563)
Co-authored-by: Rob Hague <rob.hague00@gmail.com>
This commit is contained in:
@@ -82,6 +82,9 @@ The main types provided by this library are:
|
||||
## Key Exchange Methods
|
||||
|
||||
**SSH.NET** supports the following key exchange methods:
|
||||
* mlkem768x25519-sha256
|
||||
* sntrup761x25519-sha512
|
||||
* sntrup761x25519-sha512<span></span>@openssh.com
|
||||
* curve25519-sha256
|
||||
* curve25519-sha256<span></span>@libssh.org
|
||||
* ecdh-sha2-nistp256
|
||||
|
||||
@@ -349,6 +349,7 @@ namespace Renci.SshNet
|
||||
|
||||
KeyExchangeAlgorithms = new Dictionary<string, Func<IKeyExchange>>
|
||||
{
|
||||
{ "mlkem768x25519-sha256", () => new KeyExchangeMLKem768X25519Sha256() },
|
||||
{ "sntrup761x25519-sha512", () => new KeyExchangeSNtruP761X25519Sha512() },
|
||||
{ "sntrup761x25519-sha512@openssh.com", () => new KeyExchangeSNtruP761X25519Sha512() },
|
||||
{ "curve25519-sha256", () => new KeyExchangeECCurve25519() },
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace Renci.SshNet.Messages.Transport
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents SSH_MSG_KEXECDH_INIT message.
|
||||
/// Represents SSH_MSG_KEX_ECDH_INIT message.
|
||||
/// </summary>
|
||||
internal sealed class KeyExchangeEcdhInitMessage : Message, IKeyExchangedAllowed
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace Renci.SshNet.Messages.Transport
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents SSH_MSG_KEXECDH_REPLY message.
|
||||
/// Represents SSH_MSG_KEX_ECDH_REPLY message.
|
||||
/// </summary>
|
||||
public class KeyExchangeEcdhReplyMessage : Message
|
||||
{
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
|
||||
namespace Renci.SshNet.Messages.Transport
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents SSH_MSG_KEX_HYBRID_INIT message.
|
||||
/// </summary>
|
||||
internal sealed class KeyExchangeHybridInitMessage : Message, IKeyExchangedAllowed
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string MessageName
|
||||
{
|
||||
get
|
||||
{
|
||||
return "SSH_MSG_KEX_HYBRID_INIT";
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override byte MessageNumber
|
||||
{
|
||||
get
|
||||
{
|
||||
return 30;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client init data.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The init data is the concatenation of C_PK2 and C_PK1 (C_INIT = C_PK2 || C_PK1, where || depicts concatenation).
|
||||
/// C_PK1 and C_PK2 represent the ephemeral client public keys used for each key exchange of the PQ/T Hybrid mechanism.
|
||||
/// Typically, C_PK1 represents a traditional / classical (i.e., ECDH) key exchange public key.
|
||||
/// C_PK2 represents the 'pk' output of the corresponding post-quantum KEM's 'KeyGen' at the client.
|
||||
/// </remarks>
|
||||
public byte[] CInit { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size of the message in bytes.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The size of the messages in bytes.
|
||||
/// </value>
|
||||
protected override int BufferCapacity
|
||||
{
|
||||
get
|
||||
{
|
||||
var capacity = base.BufferCapacity;
|
||||
capacity += 4; // CInit length
|
||||
capacity += CInit.Length; // CInit
|
||||
return capacity;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="KeyExchangeHybridInitMessage"/> class.
|
||||
/// </summary>
|
||||
public KeyExchangeHybridInitMessage(byte[] init)
|
||||
{
|
||||
CInit = init;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when type specific data need to be loaded.
|
||||
/// </summary>
|
||||
protected override void LoadData()
|
||||
{
|
||||
CInit = ReadBinary();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when type specific data need to be saved.
|
||||
/// </summary>
|
||||
protected override void SaveData()
|
||||
{
|
||||
WriteBinaryString(CInit);
|
||||
}
|
||||
|
||||
internal override void Process(Session session)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
namespace Renci.SshNet.Messages.Transport
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents SSH_MSG_KEX_HYBRID_REPLY message.
|
||||
/// </summary>
|
||||
public class KeyExchangeHybridReplyMessage : Message
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string MessageName
|
||||
{
|
||||
get
|
||||
{
|
||||
return "SSH_MSG_KEX_HYBRID_REPLY";
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override byte MessageNumber
|
||||
{
|
||||
get
|
||||
{
|
||||
return 31;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string encoding an X.509v3 certificate containing the server's ECDSA public host key.
|
||||
/// </summary>
|
||||
/// <value>The host key.</value>
|
||||
public byte[] KS { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the server reply.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The server reply is the concatenation of S_CT2 and S_PK1 (S_REPLY = S_CT2 || S_PK1).
|
||||
/// Typically, S_PK1 represents the ephemeral (EC)DH server public key.
|
||||
/// S_CT2 represents the ciphertext 'ct' output of the corresponding KEM's 'Encaps' algorithm generated by
|
||||
/// the server which encapsulates a secret to the client public key C_PK2.
|
||||
/// </remarks>
|
||||
public byte[] SReply { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an octet string containing the server's signature of the newly established exchange hash value.
|
||||
/// </summary>
|
||||
/// <value>The signature.</value>
|
||||
public byte[] Signature { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size of the message in bytes.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The size of the messages in bytes.
|
||||
/// </value>
|
||||
protected override int BufferCapacity
|
||||
{
|
||||
get
|
||||
{
|
||||
var capacity = base.BufferCapacity;
|
||||
capacity += 4; // KS length
|
||||
capacity += KS.Length; // KS
|
||||
capacity += 4; // SReply length
|
||||
capacity += SReply.Length; // SReply
|
||||
capacity += 4; // Signature length
|
||||
capacity += Signature.Length; // Signature
|
||||
return capacity;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when type specific data need to be loaded.
|
||||
/// </summary>
|
||||
protected override void LoadData()
|
||||
{
|
||||
KS = ReadBinary();
|
||||
SReply = ReadBinary();
|
||||
Signature = ReadBinary();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when type specific data need to be saved.
|
||||
/// </summary>
|
||||
protected override void SaveData()
|
||||
{
|
||||
WriteBinaryString(KS);
|
||||
WriteBinaryString(SReply);
|
||||
WriteBinaryString(Signature);
|
||||
}
|
||||
|
||||
internal override void Process(Session session)
|
||||
{
|
||||
session.OnKeyExchangeHybridReplyMessageReceived(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ namespace Renci.SshNet.Security
|
||||
|
||||
HandleServerEcdhReply(message.KS, message.QS, message.Signature);
|
||||
|
||||
// When SSH_MSG_KEXDH_REPLY received key exchange is completed
|
||||
// When SSH_MSG_KEX_ECDH_REPLY received key exchange is completed
|
||||
Finish();
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace Renci.SshNet.Security
|
||||
|
||||
HandleServerEcdhReply(message.KS, message.QS, message.Signature);
|
||||
|
||||
// When SSH_MSG_KEXDH_REPLY received key exchange is completed
|
||||
// When SSH_MSG_KEX_ECDH_REPLY received key exchange is completed
|
||||
Finish();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
using Org.BouncyCastle.Crypto.Agreement;
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
using Org.BouncyCastle.Crypto.Kems;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
|
||||
using Renci.SshNet.Abstractions;
|
||||
using Renci.SshNet.Common;
|
||||
using Renci.SshNet.Messages.Transport;
|
||||
|
||||
namespace Renci.SshNet.Security
|
||||
{
|
||||
internal sealed class KeyExchangeMLKem768X25519Sha256 : KeyExchangeEC
|
||||
{
|
||||
private MLKemDecapsulator _mlkemDecapsulator;
|
||||
private X25519Agreement _x25519Agreement;
|
||||
|
||||
/// <summary>
|
||||
/// Gets algorithm name.
|
||||
/// </summary>
|
||||
public override string Name
|
||||
{
|
||||
get { return "mlkem768x25519-sha256"; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size, in bits, of the computed hash code.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The size, in bits, of the computed hash code.
|
||||
/// </value>
|
||||
protected override int HashSize
|
||||
{
|
||||
get { return 256; }
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Start(Session session, KeyExchangeInitMessage message, bool sendClientInitMessage)
|
||||
{
|
||||
base.Start(session, message, sendClientInitMessage);
|
||||
|
||||
Session.RegisterMessage("SSH_MSG_KEX_HYBRID_REPLY");
|
||||
|
||||
Session.KeyExchangeHybridReplyMessageReceived += Session_KeyExchangeHybridReplyMessageReceived;
|
||||
|
||||
var mlkem768KeyPairGenerator = new MLKemKeyPairGenerator();
|
||||
mlkem768KeyPairGenerator.Init(new MLKemKeyGenerationParameters(CryptoAbstraction.SecureRandom, MLKemParameters.ml_kem_768));
|
||||
var mlkem768KeyPair = mlkem768KeyPairGenerator.GenerateKeyPair();
|
||||
|
||||
_mlkemDecapsulator = new MLKemDecapsulator(MLKemParameters.ml_kem_768);
|
||||
_mlkemDecapsulator.Init(mlkem768KeyPair.Private);
|
||||
|
||||
var x25519KeyPairGenerator = new X25519KeyPairGenerator();
|
||||
x25519KeyPairGenerator.Init(new X25519KeyGenerationParameters(CryptoAbstraction.SecureRandom));
|
||||
var x25519KeyPair = x25519KeyPairGenerator.GenerateKeyPair();
|
||||
|
||||
_x25519Agreement = new X25519Agreement();
|
||||
_x25519Agreement.Init(x25519KeyPair.Private);
|
||||
|
||||
var mlkem768PublicKey = ((MLKemPublicKeyParameters)mlkem768KeyPair.Public).GetEncoded();
|
||||
var x25519PublicKey = ((X25519PublicKeyParameters)x25519KeyPair.Public).GetEncoded();
|
||||
|
||||
_clientExchangeValue = mlkem768PublicKey.Concat(x25519PublicKey);
|
||||
|
||||
SendMessage(new KeyExchangeHybridInitMessage(_clientExchangeValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finishes key exchange algorithm.
|
||||
/// </summary>
|
||||
public override void Finish()
|
||||
{
|
||||
base.Finish();
|
||||
|
||||
Session.KeyExchangeHybridReplyMessageReceived -= Session_KeyExchangeHybridReplyMessageReceived;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hashes the specified data bytes.
|
||||
/// </summary>
|
||||
/// <param name="hashData">The hash data.</param>
|
||||
/// <returns>
|
||||
/// The hash of the data.
|
||||
/// </returns>
|
||||
protected override byte[] Hash(byte[] hashData)
|
||||
{
|
||||
return CryptoAbstraction.HashSHA256(hashData);
|
||||
}
|
||||
|
||||
private void Session_KeyExchangeHybridReplyMessageReceived(object sender, MessageEventArgs<KeyExchangeHybridReplyMessage> e)
|
||||
{
|
||||
var message = e.Message;
|
||||
|
||||
// Unregister message once received
|
||||
Session.UnRegisterMessage("SSH_MSG_KEX_HYBRID_REPLY");
|
||||
|
||||
HandleServerHybridReply(message.KS, message.SReply, message.Signature);
|
||||
|
||||
// When SSH_MSG_KEX_HYBRID_REPLY received key exchange is completed
|
||||
Finish();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the server hybrid reply message.
|
||||
/// </summary>
|
||||
/// <param name="hostKey">The host key.</param>
|
||||
/// <param name="serverExchangeValue">The server exchange value.</param>
|
||||
/// <param name="signature">The signature.</param>
|
||||
private void HandleServerHybridReply(byte[] hostKey, byte[] serverExchangeValue, byte[] signature)
|
||||
{
|
||||
_serverExchangeValue = serverExchangeValue;
|
||||
_hostKey = hostKey;
|
||||
_signature = signature;
|
||||
|
||||
if (serverExchangeValue.Length != _mlkemDecapsulator.EncapsulationLength + _x25519Agreement.AgreementSize)
|
||||
{
|
||||
throw new SshConnectionException(
|
||||
string.Format(CultureInfo.CurrentCulture, "Bad S_Reply length: {0}.", serverExchangeValue.Length),
|
||||
DisconnectReason.KeyExchangeFailed);
|
||||
}
|
||||
|
||||
var secret = new byte[_mlkemDecapsulator.SecretLength + _x25519Agreement.AgreementSize];
|
||||
|
||||
_mlkemDecapsulator.Decapsulate(serverExchangeValue, 0, _mlkemDecapsulator.EncapsulationLength, secret, 0, _mlkemDecapsulator.SecretLength);
|
||||
|
||||
var x25519PublicKey = new X25519PublicKeyParameters(serverExchangeValue, _mlkemDecapsulator.EncapsulationLength);
|
||||
_x25519Agreement.CalculateAgreement(x25519PublicKey, secret, _mlkemDecapsulator.SecretLength);
|
||||
|
||||
SharedKey = CryptoAbstraction.HashSHA256(secret);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -447,6 +447,11 @@ namespace Renci.SshNet
|
||||
/// </summary>
|
||||
internal event EventHandler<MessageEventArgs<KeyExchangeEcdhReplyMessage>> KeyExchangeEcdhReplyMessageReceived;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a <see cref="KeyExchangeHybridReplyMessage"/> message is received from the SSH server.
|
||||
/// </summary>
|
||||
internal event EventHandler<MessageEventArgs<KeyExchangeHybridReplyMessage>> KeyExchangeHybridReplyMessageReceived;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when <see cref="NewKeysMessage"/> message received
|
||||
/// </summary>
|
||||
@@ -1535,6 +1540,11 @@ namespace Renci.SshNet
|
||||
KeyExchangeEcdhReplyMessageReceived?.Invoke(this, new MessageEventArgs<KeyExchangeEcdhReplyMessage>(message));
|
||||
}
|
||||
|
||||
internal void OnKeyExchangeHybridReplyMessageReceived(KeyExchangeHybridReplyMessage message)
|
||||
{
|
||||
KeyExchangeHybridReplyMessageReceived?.Invoke(this, new MessageEventArgs<KeyExchangeHybridReplyMessage>(message));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when <see cref="NewKeysMessage"/> message received.
|
||||
/// </summary>
|
||||
|
||||
@@ -52,7 +52,8 @@ namespace Renci.SshNet
|
||||
new MessageMetadata<KeyExchangeDhGroupExchangeGroup>(28, "SSH_MSG_KEX_DH_GEX_GROUP", 31),
|
||||
new MessageMetadata<KeyExchangeDhReplyMessage>(29, "SSH_MSG_KEXDH_REPLY", 31),
|
||||
new MessageMetadata<KeyExchangeDhGroupExchangeReply>(30, "SSH_MSG_KEX_DH_GEX_REPLY", 33),
|
||||
new MessageMetadata<KeyExchangeEcdhReplyMessage>(31, "SSH_MSG_KEX_ECDH_REPLY", 31)
|
||||
new MessageMetadata<KeyExchangeEcdhReplyMessage>(31, "SSH_MSG_KEX_ECDH_REPLY", 31),
|
||||
new MessageMetadata<KeyExchangeHybridReplyMessage>(32, "SSH_MSG_KEX_HYBRID_REPLY", 31)
|
||||
};
|
||||
private static readonly Dictionary<string, MessageMetadata> MessagesByName = CreateMessagesByNameMapping();
|
||||
|
||||
@@ -64,7 +65,7 @@ namespace Renci.SshNet
|
||||
/// <summary>
|
||||
/// Defines the total number of supported messages.
|
||||
/// </summary>
|
||||
internal const int TotalMessageCount = 32;
|
||||
internal const int TotalMessageCount = 33;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SshMessageFactory"/> class.
|
||||
|
||||
@@ -22,6 +22,21 @@ namespace Renci.SshNet.IntegrationTests
|
||||
_remoteSshdConfig?.Reset();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MLKem768X25519Sha256()
|
||||
{
|
||||
_remoteSshdConfig.ClearKeyExchangeAlgorithms()
|
||||
.AddKeyExchangeAlgorithm(KeyExchangeAlgorithm.MLKem768X25519Sha256)
|
||||
.Update()
|
||||
.Restart();
|
||||
|
||||
using (var client = new SshClient(_connectionInfoFactory.Create()))
|
||||
{
|
||||
client.Connect();
|
||||
client.Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SNtruP761X25519Sha512()
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
public static readonly KeyExchangeAlgorithm Curve25519Sha256Libssh = new KeyExchangeAlgorithm("curve25519-sha256@libssh.org");
|
||||
public static readonly KeyExchangeAlgorithm SNtruP761X25519Sha512 = new KeyExchangeAlgorithm("sntrup761x25519-sha512");
|
||||
public static readonly KeyExchangeAlgorithm SNtruP761X25519Sha512OpenSsh = new KeyExchangeAlgorithm("sntrup761x25519-sha512@openssh.com");
|
||||
public static readonly KeyExchangeAlgorithm MLKem768X25519Sha256 = new KeyExchangeAlgorithm("mlkem768x25519-sha256");
|
||||
|
||||
public KeyExchangeAlgorithm(string name)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user