Add support for mlkem768x25519-sha256 key exchange method (#1563)

Co-authored-by: Rob Hague <rob.hague00@gmail.com>
This commit is contained in:
Scott Xu
2025-01-11 22:46:22 +08:00
committed by GitHub
parent 9e1ee0a380
commit 7e71bb4192
13 changed files with 351 additions and 6 deletions
+3
View File
@@ -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
+1
View File
@@ -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();
}
+1 -1
View File
@@ -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);
}
}
}
+10
View File
@@ -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>
+3 -2
View File
@@ -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)
{