Ver Fonte

Add support for AEAD ChaCha20Poly1305 Cipher (#1416)

* Implements ChaCha20 cipher algorithm.

* Implements chacha20-poly1305@openssh.com

* Update Cipher.cs

* Update ChaCha20Poly1305Cipher.cs

* Note that the length of the concatenation of 'packet_length',
'padding_length', 'payload', and 'random padding' MUST be a multiple
of the cipher block size or 8, whichever is larger.
See https://www.rfc-editor.org/rfc/rfc4253#section-6

* Use Chaos.Nacl Poly1305Donna

* Fix build. Fix typo. Update README

* Update README.md

* Fix build

* Remove trailing whitespace

* Fix build

* Change to BouncyCastle

* Inherit from SymmetricCipher instead of StreamCipher since StreamCipher is deleted

* Resolve conflicts

* Move field to local variable

* Compute poly key stream once

* Update test/Renci.SshNet.IntegrationTests/CipherTests.cs

Co-authored-by: Rob Hague <rob.hague00@gmail.com>

* Fix build; Add net48 integration test in CI

---------

Co-authored-by: Rob Hague <rob.hague00@gmail.com>
Co-authored-by: Wojciech Nagórski <wojtpl2@gmail.com>
Scott Xu há 1 ano atrás
pai
commit
486b69dc98

+ 1 - 0
README.md

@@ -72,6 +72,7 @@ The main types provided by this library are:
 * aes256-ctr
 * aes128-gcm<span></span>@openssh.com (.NET 6 and higher)
 * aes256-gcm<span></span>@openssh.com (.NET 6 and higher)
+* chacha20-poly1305<span></span>@openssh.com
 * aes128-cbc
 * aes192-cbc
 * aes256-cbc

+ 1 - 1
appveyor.yml

@@ -28,7 +28,7 @@ for:
     - sh: dotnet test -f net8.0 -c Debug --no-restore --no-build --results-directory artifacts --logger Appveyor --logger "console;verbosity=normal" --logger "liquid.md;LogFileName=linux_unit_test_net_8_report.md" -p:CollectCoverage=true -p:CoverletOutputFormat=cobertura -p:CoverletOutput=../../artifacts/linux_unit_test_net_8_coverage.xml test/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj
     - sh: echo "Run integration tests"
     - sh: dotnet test -f net8.0 -c Debug --no-restore --no-build --results-directory artifacts --logger Appveyor --logger "console;verbosity=normal" --logger "liquid.md;LogFileName=linux_integration_test_net_8_report.md" -p:CollectCoverage=true -p:CoverletOutputFormat=cobertura -p:CoverletOutput=../../artifacts/linux_integration_test_net_8_coverage.xml test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj
-    - sh: dotnet test -f net48 -c Debug --no-restore --no-build --results-directory artifacts --logger Appveyor --logger "console;verbosity=normal" --logger "liquid.md;LogFileName=linux_integration_test_net_48_report.md" -p:CollectCoverage=true -p:CoverletOutputFormat=cobertura -p:CoverletOutput=../../artifacts/linux_integration_test_net_48_coverage.xml --filter Name~Zlib test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj
+    - sh: dotnet test -f net48 -c Debug --no-restore --no-build --results-directory artifacts --logger Appveyor --logger "console;verbosity=normal" --logger "liquid.md;LogFileName=linux_integration_test_net_48_report.md" -p:CollectCoverage=true -p:CoverletOutputFormat=cobertura -p:CoverletOutput=../../artifacts/linux_integration_test_net_48_coverage.xml --filter "Name=ChaCha20Poly1305|Name~Zlib" test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj
 
 -
   matrix:

+ 1 - 0
src/Renci.SshNet/ConnectionInfo.cs

@@ -392,6 +392,7 @@ namespace Renci.SshNet
                 Encryptions.Add("aes256-gcm@openssh.com", new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv), isAead: true));
             }
 #endif
+            Encryptions.Add("chacha20-poly1305@openssh.com", new CipherInfo(512, (key, iv) => new ChaCha20Poly1305Cipher(key), isAead: true));
             Encryptions.Add("aes128-cbc", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));
             Encryptions.Add("aes192-cbc", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));
             Encryptions.Add("aes256-cbc", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));

+ 10 - 2
src/Renci.SshNet/Security/Cryptography/Cipher.cs

@@ -1,4 +1,4 @@
-namespace Renci.SshNet.Security.Cryptography
+namespace Renci.SshNet.Security.Cryptography
 {
     /// <summary>
     /// Base class for cipher implementation.
@@ -22,6 +22,14 @@
         /// </value>
         public virtual int TagSize { get; }
 
+        /// <summary>
+        /// Sets the sequence number.
+        /// </summary>
+        /// <param name="sequenceNumber">The sequence number.</param>
+        internal virtual void SetSequenceNumber(uint sequenceNumber)
+        {
+        }
+
         /// <summary>
         /// Encrypts the specified input.
         /// </summary>
@@ -50,7 +58,7 @@
         /// <returns>
         /// The decrypted data.
         /// </returns>
-        public byte[] Decrypt(byte[] input)
+        public virtual byte[] Decrypt(byte[] input)
         {
             return Decrypt(input, 0, input.Length);
         }

+ 157 - 0
src/Renci.SshNet/Security/Cryptography/Ciphers/ChaCha20Poly1305Cipher.cs

@@ -0,0 +1,157 @@
+using System;
+using System.Buffers.Binary;
+using System.Diagnostics;
+
+using Org.BouncyCastle.Crypto.Engines;
+using Org.BouncyCastle.Crypto.Macs;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Utilities;
+
+using Renci.SshNet.Common;
+using Renci.SshNet.Messages.Transport;
+
+namespace Renci.SshNet.Security.Cryptography.Ciphers
+{
+    /// <summary>
+    /// ChaCha20Poly1305 cipher implementation.
+    /// <see href="https://datatracker.ietf.org/doc/html/draft-josefsson-ssh-chacha20-poly1305-openssh-00"/>.
+    /// </summary>
+    internal sealed class ChaCha20Poly1305Cipher : SymmetricCipher
+    {
+        private readonly ChaCha7539Engine _aadCipher = new ChaCha7539Engine();
+        private readonly ChaCha7539Engine _cipher = new ChaCha7539Engine();
+        private readonly Poly1305 _mac = new Poly1305();
+
+        /// <summary>
+        /// Gets the minimun block size.
+        /// </summary>
+        public override byte MinimumSize
+        {
+            get
+            {
+                return 16;
+            }
+        }
+
+        /// <summary>
+        /// Gets the tag size in bytes.
+        /// Poly1305 [Poly1305], also by Daniel Bernstein, is a one-time Carter-
+        /// Wegman MAC that computes a 128 bit integrity tag given a message
+        /// <see href="https://datatracker.ietf.org/doc/html/draft-josefsson-ssh-chacha20-poly1305-openssh-00#section-1"/>.
+        /// </summary>
+        public override int TagSize
+        {
+            get
+            {
+                return 16;
+            }
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ChaCha20Poly1305Cipher"/> class.
+        /// </summary>
+        /// <param name="key">The key.</param>
+        public ChaCha20Poly1305Cipher(byte[] key)
+            : base(key)
+        {
+        }
+
+        /// <summary>
+        /// Encrypts the specified input.
+        /// </summary>
+        /// <param name="input">
+        /// The input data with below format:
+        ///   <code>
+        ///   [outbound sequence field][packet length field][padding length field sz][payload][random paddings]
+        ///   [----4 bytes----(offset)][------4 bytes------][----------------Plain Text---------------(length)]
+        ///   </code>
+        /// </param>
+        /// <param name="offset">The zero-based offset in <paramref name="input"/> at which to begin encrypting.</param>
+        /// <param name="length">The number of bytes to encrypt from <paramref name="input"/>.</param>
+        /// <returns>
+        /// The encrypted data with below format:
+        ///   <code>
+        ///   [packet length field][padding length field sz][payload][random paddings][Authenticated TAG]
+        ///   [------4 bytes------][------------------Cipher Text--------------------][-------TAG-------]
+        ///   </code>
+        /// </returns>
+        public override byte[] Encrypt(byte[] input, int offset, int length)
+        {
+            var output = new byte[length + TagSize];
+
+            _aadCipher.ProcessBytes(input, offset, 4, output, 0);
+            _cipher.ProcessBytes(input, offset + 4, length - 4, output, 4);
+
+            _mac.BlockUpdate(output, 0, length);
+            _ = _mac.DoFinal(output, length);
+
+            return output;
+        }
+
+        /// <summary>
+        /// Decrypts the first block which is packet length field.
+        /// </summary>
+        /// <param name="input">The encrypted packet length field.</param>
+        /// <returns>The decrypted packet length field.</returns>
+        public override byte[] Decrypt(byte[] input)
+        {
+            var output = new byte[input.Length];
+            _aadCipher.ProcessBytes(input, 0, input.Length, output, 0);
+
+            return output;
+        }
+
+        /// <summary>
+        /// Decrypts the specified input.
+        /// </summary>
+        /// <param name="input">
+        /// The input data with below format:
+        ///   <code>
+        ///   [inbound sequence field][packet length field][padding length field sz][payload][random paddings][Authenticated TAG]
+        ///   [--------4 bytes-------][--4 bytes--(offset)][--------------Cipher Text----------------(length)][-------TAG-------]
+        ///   </code>
+        /// </param>
+        /// <param name="offset">The zero-based offset in <paramref name="input"/> at which to begin decrypting and authenticating.</param>
+        /// <param name="length">The number of bytes to decrypt and authenticate from <paramref name="input"/>.</param>
+        /// <returns>
+        /// The decrypted data with below format:
+        /// <code>
+        ///   [padding length field sz][payload][random paddings]
+        ///   [--------------------Plain Text-------------------]
+        /// </code>
+        /// </returns>
+        public override byte[] Decrypt(byte[] input, int offset, int length)
+        {
+            Debug.Assert(offset == 8, "The offset must be 8");
+
+            var tag = new byte[TagSize];
+            _mac.BlockUpdate(input, offset - 4, length + 4);
+            _ = _mac.DoFinal(tag, 0);
+            if (!Arrays.FixedTimeEquals(TagSize, tag, 0, input, offset + length))
+            {
+                throw new SshConnectionException("MAC error", DisconnectReason.MacError);
+            }
+
+            var output = new byte[length];
+            _cipher.ProcessBytes(input, offset, length, output, 0);
+
+            return output;
+        }
+
+        internal override void SetSequenceNumber(uint sequenceNumber)
+        {
+            var iv = new byte[12];
+            BinaryPrimitives.WriteUInt64BigEndian(iv.AsSpan(4), sequenceNumber);
+
+            // ChaCha20 encryption and decryption is completely
+            // symmetrical, so the 'forEncryption' is
+            // irrelevant. (Like 90% of stream ciphers)
+            _aadCipher.Init(forEncryption: true, new ParametersWithIV(new KeyParameter(Key, 32, 32), iv));
+            _cipher.Init(forEncryption: true, new ParametersWithIV(new KeyParameter(Key, 0, 32), iv));
+
+            var keyStream = new byte[64];
+            _cipher.ProcessBytes(keyStream, 0, keyStream.Length, keyStream, 0);
+            _mac.Init(new KeyParameter(keyStream, 0, 32));
+        }
+    }
+}

+ 28 - 5
src/Renci.SshNet/Session.cs

@@ -1059,6 +1059,7 @@ namespace Renci.SshNet
                 // Encrypt packet data
                 if (_clientCipher != null)
                 {
+                    _clientCipher.SetSequenceNumber(_outboundPacketSequence);
                     if (_clientEtm)
                     {
                         // The length of the "packet length" field in bytes
@@ -1246,15 +1247,28 @@ namespace Renci.SshNet
                     return null;
                 }
 
-                if (_serverCipher != null && !_serverAead && (_serverMac == null || !_serverEtm))
+                var plainFirstBlock = firstBlock;
+
+                // First block is not encrypted in AES GCM mode.
+                if (_serverCipher is not null
+#if NET6_0_OR_GREATER
+        and not Security.Cryptography.Ciphers.AesGcmCipher
+#endif
+                    )
                 {
-                    firstBlock = _serverCipher.Decrypt(firstBlock);
+                    _serverCipher.SetSequenceNumber(_inboundPacketSequence);
+
+                    // First block is not encrypted in ETM mode.
+                    if (_serverMac == null || !_serverEtm)
+                    {
+                        plainFirstBlock = _serverCipher.Decrypt(firstBlock);
+                    }
                 }
 
-                packetLength = BinaryPrimitives.ReadUInt32BigEndian(firstBlock);
+                packetLength = BinaryPrimitives.ReadUInt32BigEndian(plainFirstBlock);
 
                 // Test packet minimum and maximum boundaries
-                if (packetLength < Math.Max((byte)16, blockSize) - 4 || packetLength > MaximumSshPacketSize - 4)
+                if (packetLength < Math.Max((byte)8, blockSize) - 4 || packetLength > MaximumSshPacketSize - 4)
                 {
                     throw new SshConnectionException(string.Format(CultureInfo.CurrentCulture, "Bad packet length: {0}.", packetLength),
                                                      DisconnectReason.ProtocolError);
@@ -1277,7 +1291,16 @@ namespace Renci.SshNet
                 // to read the packet including server MAC in a single pass (except for the initial block).
                 data = new byte[bytesToRead + blockSize + inboundPacketSequenceLength];
                 BinaryPrimitives.WriteUInt32BigEndian(data, _inboundPacketSequence);
-                Buffer.BlockCopy(firstBlock, 0, data, inboundPacketSequenceLength, firstBlock.Length);
+
+                // Use raw packet length field to calculate the mac in AEAD mode.
+                if (_serverAead)
+                {
+                    Buffer.BlockCopy(firstBlock, 0, data, inboundPacketSequenceLength, blockSize);
+                }
+                else
+                {
+                    Buffer.BlockCopy(plainFirstBlock, 0, data, inboundPacketSequenceLength, blockSize);
+                }
 
                 if (bytesToRead > 0)
                 {

+ 6 - 0
test/Renci.SshNet.IntegrationTests/CipherTests.cs

@@ -77,6 +77,12 @@ namespace Renci.SshNet.IntegrationTests
             DoTest(Cipher.Aes256Gcm);
         }
 #endif
+        [TestMethod]
+        public void ChaCha20Poly1305()
+        {
+            DoTest(Cipher.Chacha20Poly1305);
+        }
+
         private void DoTest(Cipher cipher)
         {
             _remoteSshdConfig.ClearCiphers()