Skip to content

Commit 486b69d

Browse files
scott-xuRob-HagueWojciechNagorski
authored
Add support for AEAD ChaCha20Poly1305 Cipher (#1416)
* Implements ChaCha20 cipher algorithm. * Implements [email protected] * 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 <[email protected]> * Fix build; Add net48 integration test in CI --------- Co-authored-by: Rob Hague <[email protected]> Co-authored-by: Wojciech Nagórski <[email protected]>
1 parent c614f54 commit 486b69d

File tree

7 files changed

+204
-8
lines changed

7 files changed

+204
-8
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ The main types provided by this library are:
7272
* aes256-ctr
7373
* aes128-gcm<span></span>@openssh.com (.NET 6 and higher)
7474
* aes256-gcm<span></span>@openssh.com (.NET 6 and higher)
75+
* chacha20-poly1305<span></span>@openssh.com
7576
* aes128-cbc
7677
* aes192-cbc
7778
* aes256-cbc

appveyor.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ for:
2828
- 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
2929
- sh: echo "Run integration tests"
3030
- 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
31-
- 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
31+
- 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
3232

3333
-
3434
matrix:

src/Renci.SshNet/ConnectionInfo.cs

+1
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy
392392
Encryptions.Add("[email protected]", new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv), isAead: true));
393393
}
394394
#endif
395+
Encryptions.Add("[email protected]", new CipherInfo(512, (key, iv) => new ChaCha20Poly1305Cipher(key), isAead: true));
395396
Encryptions.Add("aes128-cbc", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));
396397
Encryptions.Add("aes192-cbc", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));
397398
Encryptions.Add("aes256-cbc", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));

src/Renci.SshNet/Security/Cryptography/Cipher.cs

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace Renci.SshNet.Security.Cryptography
1+
namespace Renci.SshNet.Security.Cryptography
22
{
33
/// <summary>
44
/// Base class for cipher implementation.
@@ -22,6 +22,14 @@ public abstract class Cipher
2222
/// </value>
2323
public virtual int TagSize { get; }
2424

25+
/// <summary>
26+
/// Sets the sequence number.
27+
/// </summary>
28+
/// <param name="sequenceNumber">The sequence number.</param>
29+
internal virtual void SetSequenceNumber(uint sequenceNumber)
30+
{
31+
}
32+
2533
/// <summary>
2634
/// Encrypts the specified input.
2735
/// </summary>
@@ -50,7 +58,7 @@ public byte[] Encrypt(byte[] input)
5058
/// <returns>
5159
/// The decrypted data.
5260
/// </returns>
53-
public byte[] Decrypt(byte[] input)
61+
public virtual byte[] Decrypt(byte[] input)
5462
{
5563
return Decrypt(input, 0, input.Length);
5664
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
using System;
2+
using System.Buffers.Binary;
3+
using System.Diagnostics;
4+
5+
using Org.BouncyCastle.Crypto.Engines;
6+
using Org.BouncyCastle.Crypto.Macs;
7+
using Org.BouncyCastle.Crypto.Parameters;
8+
using Org.BouncyCastle.Utilities;
9+
10+
using Renci.SshNet.Common;
11+
using Renci.SshNet.Messages.Transport;
12+
13+
namespace Renci.SshNet.Security.Cryptography.Ciphers
14+
{
15+
/// <summary>
16+
/// ChaCha20Poly1305 cipher implementation.
17+
/// <see href="https://datatracker.ietf.org/doc/html/draft-josefsson-ssh-chacha20-poly1305-openssh-00"/>.
18+
/// </summary>
19+
internal sealed class ChaCha20Poly1305Cipher : SymmetricCipher
20+
{
21+
private readonly ChaCha7539Engine _aadCipher = new ChaCha7539Engine();
22+
private readonly ChaCha7539Engine _cipher = new ChaCha7539Engine();
23+
private readonly Poly1305 _mac = new Poly1305();
24+
25+
/// <summary>
26+
/// Gets the minimun block size.
27+
/// </summary>
28+
public override byte MinimumSize
29+
{
30+
get
31+
{
32+
return 16;
33+
}
34+
}
35+
36+
/// <summary>
37+
/// Gets the tag size in bytes.
38+
/// Poly1305 [Poly1305], also by Daniel Bernstein, is a one-time Carter-
39+
/// Wegman MAC that computes a 128 bit integrity tag given a message
40+
/// <see href="https://datatracker.ietf.org/doc/html/draft-josefsson-ssh-chacha20-poly1305-openssh-00#section-1"/>.
41+
/// </summary>
42+
public override int TagSize
43+
{
44+
get
45+
{
46+
return 16;
47+
}
48+
}
49+
50+
/// <summary>
51+
/// Initializes a new instance of the <see cref="ChaCha20Poly1305Cipher"/> class.
52+
/// </summary>
53+
/// <param name="key">The key.</param>
54+
public ChaCha20Poly1305Cipher(byte[] key)
55+
: base(key)
56+
{
57+
}
58+
59+
/// <summary>
60+
/// Encrypts the specified input.
61+
/// </summary>
62+
/// <param name="input">
63+
/// The input data with below format:
64+
/// <code>
65+
/// [outbound sequence field][packet length field][padding length field sz][payload][random paddings]
66+
/// [----4 bytes----(offset)][------4 bytes------][----------------Plain Text---------------(length)]
67+
/// </code>
68+
/// </param>
69+
/// <param name="offset">The zero-based offset in <paramref name="input"/> at which to begin encrypting.</param>
70+
/// <param name="length">The number of bytes to encrypt from <paramref name="input"/>.</param>
71+
/// <returns>
72+
/// The encrypted data with below format:
73+
/// <code>
74+
/// [packet length field][padding length field sz][payload][random paddings][Authenticated TAG]
75+
/// [------4 bytes------][------------------Cipher Text--------------------][-------TAG-------]
76+
/// </code>
77+
/// </returns>
78+
public override byte[] Encrypt(byte[] input, int offset, int length)
79+
{
80+
var output = new byte[length + TagSize];
81+
82+
_aadCipher.ProcessBytes(input, offset, 4, output, 0);
83+
_cipher.ProcessBytes(input, offset + 4, length - 4, output, 4);
84+
85+
_mac.BlockUpdate(output, 0, length);
86+
_ = _mac.DoFinal(output, length);
87+
88+
return output;
89+
}
90+
91+
/// <summary>
92+
/// Decrypts the first block which is packet length field.
93+
/// </summary>
94+
/// <param name="input">The encrypted packet length field.</param>
95+
/// <returns>The decrypted packet length field.</returns>
96+
public override byte[] Decrypt(byte[] input)
97+
{
98+
var output = new byte[input.Length];
99+
_aadCipher.ProcessBytes(input, 0, input.Length, output, 0);
100+
101+
return output;
102+
}
103+
104+
/// <summary>
105+
/// Decrypts the specified input.
106+
/// </summary>
107+
/// <param name="input">
108+
/// The input data with below format:
109+
/// <code>
110+
/// [inbound sequence field][packet length field][padding length field sz][payload][random paddings][Authenticated TAG]
111+
/// [--------4 bytes-------][--4 bytes--(offset)][--------------Cipher Text----------------(length)][-------TAG-------]
112+
/// </code>
113+
/// </param>
114+
/// <param name="offset">The zero-based offset in <paramref name="input"/> at which to begin decrypting and authenticating.</param>
115+
/// <param name="length">The number of bytes to decrypt and authenticate from <paramref name="input"/>.</param>
116+
/// <returns>
117+
/// The decrypted data with below format:
118+
/// <code>
119+
/// [padding length field sz][payload][random paddings]
120+
/// [--------------------Plain Text-------------------]
121+
/// </code>
122+
/// </returns>
123+
public override byte[] Decrypt(byte[] input, int offset, int length)
124+
{
125+
Debug.Assert(offset == 8, "The offset must be 8");
126+
127+
var tag = new byte[TagSize];
128+
_mac.BlockUpdate(input, offset - 4, length + 4);
129+
_ = _mac.DoFinal(tag, 0);
130+
if (!Arrays.FixedTimeEquals(TagSize, tag, 0, input, offset + length))
131+
{
132+
throw new SshConnectionException("MAC error", DisconnectReason.MacError);
133+
}
134+
135+
var output = new byte[length];
136+
_cipher.ProcessBytes(input, offset, length, output, 0);
137+
138+
return output;
139+
}
140+
141+
internal override void SetSequenceNumber(uint sequenceNumber)
142+
{
143+
var iv = new byte[12];
144+
BinaryPrimitives.WriteUInt64BigEndian(iv.AsSpan(4), sequenceNumber);
145+
146+
// ChaCha20 encryption and decryption is completely
147+
// symmetrical, so the 'forEncryption' is
148+
// irrelevant. (Like 90% of stream ciphers)
149+
_aadCipher.Init(forEncryption: true, new ParametersWithIV(new KeyParameter(Key, 32, 32), iv));
150+
_cipher.Init(forEncryption: true, new ParametersWithIV(new KeyParameter(Key, 0, 32), iv));
151+
152+
var keyStream = new byte[64];
153+
_cipher.ProcessBytes(keyStream, 0, keyStream.Length, keyStream, 0);
154+
_mac.Init(new KeyParameter(keyStream, 0, 32));
155+
}
156+
}
157+
}

src/Renci.SshNet/Session.cs

+28-5
Original file line numberDiff line numberDiff line change
@@ -1059,6 +1059,7 @@ internal void SendMessage(Message message)
10591059
// Encrypt packet data
10601060
if (_clientCipher != null)
10611061
{
1062+
_clientCipher.SetSequenceNumber(_outboundPacketSequence);
10621063
if (_clientEtm)
10631064
{
10641065
// The length of the "packet length" field in bytes
@@ -1246,15 +1247,28 @@ private Message ReceiveMessage(Socket socket)
12461247
return null;
12471248
}
12481249

1249-
if (_serverCipher != null && !_serverAead && (_serverMac == null || !_serverEtm))
1250+
var plainFirstBlock = firstBlock;
1251+
1252+
// First block is not encrypted in AES GCM mode.
1253+
if (_serverCipher is not null
1254+
#if NET6_0_OR_GREATER
1255+
and not Security.Cryptography.Ciphers.AesGcmCipher
1256+
#endif
1257+
)
12501258
{
1251-
firstBlock = _serverCipher.Decrypt(firstBlock);
1259+
_serverCipher.SetSequenceNumber(_inboundPacketSequence);
1260+
1261+
// First block is not encrypted in ETM mode.
1262+
if (_serverMac == null || !_serverEtm)
1263+
{
1264+
plainFirstBlock = _serverCipher.Decrypt(firstBlock);
1265+
}
12521266
}
12531267

1254-
packetLength = BinaryPrimitives.ReadUInt32BigEndian(firstBlock);
1268+
packetLength = BinaryPrimitives.ReadUInt32BigEndian(plainFirstBlock);
12551269

12561270
// Test packet minimum and maximum boundaries
1257-
if (packetLength < Math.Max((byte)16, blockSize) - 4 || packetLength > MaximumSshPacketSize - 4)
1271+
if (packetLength < Math.Max((byte)8, blockSize) - 4 || packetLength > MaximumSshPacketSize - 4)
12581272
{
12591273
throw new SshConnectionException(string.Format(CultureInfo.CurrentCulture, "Bad packet length: {0}.", packetLength),
12601274
DisconnectReason.ProtocolError);
@@ -1277,7 +1291,16 @@ private Message ReceiveMessage(Socket socket)
12771291
// to read the packet including server MAC in a single pass (except for the initial block).
12781292
data = new byte[bytesToRead + blockSize + inboundPacketSequenceLength];
12791293
BinaryPrimitives.WriteUInt32BigEndian(data, _inboundPacketSequence);
1280-
Buffer.BlockCopy(firstBlock, 0, data, inboundPacketSequenceLength, firstBlock.Length);
1294+
1295+
// Use raw packet length field to calculate the mac in AEAD mode.
1296+
if (_serverAead)
1297+
{
1298+
Buffer.BlockCopy(firstBlock, 0, data, inboundPacketSequenceLength, blockSize);
1299+
}
1300+
else
1301+
{
1302+
Buffer.BlockCopy(plainFirstBlock, 0, data, inboundPacketSequenceLength, blockSize);
1303+
}
12811304

12821305
if (bytesToRead > 0)
12831306
{

test/Renci.SshNet.IntegrationTests/CipherTests.cs

+6
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ public void Aes256Gcm()
7777
DoTest(Cipher.Aes256Gcm);
7878
}
7979
#endif
80+
[TestMethod]
81+
public void ChaCha20Poly1305()
82+
{
83+
DoTest(Cipher.Chacha20Poly1305);
84+
}
85+
8086
private void DoTest(Cipher cipher)
8187
{
8288
_remoteSshdConfig.ClearCiphers()

0 commit comments

Comments
 (0)