2
0
Эх сурвалжийг харах

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

Co-authored-by: Rob Hague <rob.hague00@gmail.com>
Scott Xu 9 сар өмнө
parent
commit
7e71bb4192

+ 3 - 0
README.md

@@ -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 - 0
src/Renci.SshNet/ConnectionInfo.cs

@@ -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() },

+ 1 - 1
src/Renci.SshNet/Messages/Transport/KeyExchangeEcdhInitMessage.cs

@@ -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 - 1
src/Renci.SshNet/Messages/Transport/KeyExchangeEcdhReplyMessage.cs

@@ -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
     {

+ 85 - 0
src/Renci.SshNet/Messages/Transport/KeyExchangeHybridInitMessage.cs

@@ -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();
+        }
+    }
+}

+ 95 - 0
src/Renci.SshNet/Messages/Transport/KeyExchangeHybridReplyMessage.cs

@@ -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);
+        }
+    }
+}

+ 1 - 1
src/Renci.SshNet/Security/KeyExchangeECCurve25519.cs

@@ -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
src/Renci.SshNet/Security/KeyExchangeECDH.cs

@@ -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();
         }
 

+ 134 - 0
src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.cs

@@ -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 - 0
src/Renci.SshNet/Session.cs

@@ -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
src/Renci.SshNet/SshMessageFactory.cs

@@ -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.

+ 15 - 0
test/Renci.SshNet.IntegrationTests/KeyExchangeAlgorithmTests.cs

@@ -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()
         {

+ 1 - 0
test/Renci.SshNet.TestTools.OpenSSH/KeyExchangeAlgorithm.cs

@@ -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)
         {