]> git.deb.at Git - rhonda/impostor.hazel.git/commitdiff
Add AesGcm record protection
authorMatthew Endsley <mendsley@gmail.com>
Wed, 13 Jan 2021 07:58:16 +0000 (23:58 -0800)
committerMatthew Endsley <mendsley@gmail.com>
Tue, 2 Feb 2021 16:53:33 +0000 (08:53 -0800)
Hazel.UnitTests/Dtls/AesGcmRecordProtectedTests.cs [new file with mode: 0644]
Hazel.UnitTests/Hazel.UnitTests.csproj
Hazel/Dtls/AesGcmRecordProtection.cs [new file with mode: 0644]
Hazel/Dtls/Record.cs
Hazel/Hazel.csproj

diff --git a/Hazel.UnitTests/Dtls/AesGcmRecordProtectedTests.cs b/Hazel.UnitTests/Dtls/AesGcmRecordProtectedTests.cs
new file mode 100644 (file)
index 0000000..941f849
--- /dev/null
@@ -0,0 +1,199 @@
+using Hazel.Dtls;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Hazel.UnitTests.Dtls
+{
+    [TestClass]
+    public class AesGcmRecordProtectedTests
+    {
+        private readonly ByteSpan masterSecret;
+        private readonly ByteSpan serverRandom;
+        private readonly ByteSpan clientRandom;
+
+        private const string TestMessage = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
+
+        public AesGcmRecordProtectedTests()
+        {
+            this.masterSecret = new byte[48];
+            this.serverRandom = new byte[32];
+            this.clientRandom = new byte[32];
+
+            using (RandomNumberGenerator random = RandomNumberGenerator.Create())
+            {
+                random.GetBytes(this.masterSecret.GetUnderlyingArray());
+                random.GetBytes(this.serverRandom.GetUnderlyingArray());
+                random.GetBytes(this.clientRandom.GetUnderlyingArray());
+            }
+        }
+
+        [TestMethod]
+        public void ServerCanEncryptAndDecryptData()
+        {
+            using (Aes128GcmRecordProtection recordProtection = new Aes128GcmRecordProtection(this.masterSecret, this.serverRandom, this.clientRandom))
+            {
+                byte[] messageAsBytes = Encoding.UTF8.GetBytes(TestMessage);
+
+                Record record = new Record();
+                record.ContentType = ContentType.ApplicationData;
+                record.Epoch = 1;
+                record.SequenceNumber = 124;
+                record.Length = (ushort)recordProtection.GetEncryptedSize(messageAsBytes.Length);
+
+                ByteSpan encrypted = new byte[record.Length];
+                recordProtection.EncryptServerPlaintext(encrypted, messageAsBytes, ref record);
+
+                ByteSpan plaintext = new byte[recordProtection.GetDecryptedSize(encrypted.Length)];
+                bool couldDecrypt = recordProtection.DecryptCiphertextFromServer(plaintext, encrypted, ref record);
+                Assert.IsTrue(couldDecrypt);
+                Assert.AreEqual(messageAsBytes.Length, plaintext.Length);
+                Assert.AreEqual(TestMessage, Encoding.UTF8.GetString(plaintext.GetUnderlyingArray(), plaintext.Offset, plaintext.Length));
+            }
+        }
+
+        [TestMethod]
+        public void ClientCanEncryptAndDecryptData()
+        {
+            using (Aes128GcmRecordProtection recordProtection = new Aes128GcmRecordProtection(this.masterSecret, this.serverRandom, this.clientRandom))
+            {
+                byte[] messageAsBytes = Encoding.UTF8.GetBytes(TestMessage);
+
+                Record record = new Record();
+                record.ContentType = ContentType.ApplicationData;
+                record.Epoch = 1;
+                record.SequenceNumber = 124;
+                record.Length = (ushort)recordProtection.GetEncryptedSize(messageAsBytes.Length);
+
+                ByteSpan encrypted = new byte[record.Length];
+                recordProtection.EncryptClientPlaintext(encrypted, messageAsBytes, ref record);
+
+                ByteSpan plaintext = new byte[recordProtection.GetDecryptedSize(encrypted.Length)];
+                bool couldDecrypt = recordProtection.DecryptCiphertextFromClient(plaintext, encrypted, ref record);
+                Assert.IsTrue(couldDecrypt);
+                Assert.AreEqual(messageAsBytes.Length, plaintext.Length);
+                Assert.AreEqual(TestMessage, Encoding.UTF8.GetString(plaintext.GetUnderlyingArray(), plaintext.Offset, plaintext.Length));
+            }
+        }
+
+        [TestMethod]
+        public void ServerDecryptionFailsWhenRecordModified()
+        {
+            using (Aes128GcmRecordProtection recordProtection = new Aes128GcmRecordProtection(this.masterSecret, this.serverRandom, this.clientRandom))
+            {
+                byte[] messageAsBytes = Encoding.UTF8.GetBytes(TestMessage);
+
+                Record originalRecord = new Record();
+                originalRecord.ContentType = ContentType.ApplicationData;
+                originalRecord.Epoch = 1;
+                originalRecord.SequenceNumber = 124;
+                originalRecord.Length = (ushort)recordProtection.GetEncryptedSize(messageAsBytes.Length);
+
+                ByteSpan encrypted = new byte[originalRecord.Length];
+                recordProtection.EncryptServerPlaintext(encrypted, messageAsBytes, ref originalRecord);
+
+                ByteSpan plaintext = new byte[recordProtection.GetDecryptedSize(encrypted.Length)];
+
+                Record record = originalRecord;
+                record.ContentType = ContentType.Handshake;
+                bool couldDecrypt = recordProtection.DecryptCiphertextFromServer(plaintext, encrypted, ref record);
+                Assert.IsFalse(couldDecrypt);
+
+                record = originalRecord;
+                record.Epoch++;
+                couldDecrypt = recordProtection.DecryptCiphertextFromServer(plaintext, encrypted, ref record);
+                Assert.IsFalse(couldDecrypt);
+
+                record = originalRecord;
+                record.SequenceNumber++;
+                couldDecrypt = recordProtection.DecryptCiphertextFromServer(plaintext, encrypted, ref record);
+                Assert.IsFalse(couldDecrypt);
+            }
+        }
+
+        [TestMethod]
+        public void ClientDecryptionFailsWhenRecordModified()
+        {
+            using (Aes128GcmRecordProtection recordProtection = new Aes128GcmRecordProtection(this.masterSecret, this.serverRandom, this.clientRandom))
+            {
+                byte[] messageAsBytes = Encoding.UTF8.GetBytes(TestMessage);
+
+                Record originalRecord = new Record();
+                originalRecord.ContentType = ContentType.ApplicationData;
+                originalRecord.Epoch = 1;
+                originalRecord.SequenceNumber = 124;
+                originalRecord.Length = (ushort)recordProtection.GetEncryptedSize(messageAsBytes.Length);
+
+                ByteSpan encrypted = new byte[originalRecord.Length];
+                recordProtection.EncryptClientPlaintext(encrypted, messageAsBytes, ref originalRecord);
+
+                ByteSpan plaintext = new byte[recordProtection.GetDecryptedSize(encrypted.Length)];
+
+                Record record = originalRecord;
+                record.ContentType = ContentType.Handshake;
+                bool couldDecrypt = recordProtection.DecryptCiphertextFromClient(plaintext, encrypted, ref record);
+                Assert.IsFalse(couldDecrypt);
+
+                record = originalRecord;
+                record.Epoch++;
+                couldDecrypt = recordProtection.DecryptCiphertextFromClient(plaintext, encrypted, ref record);
+                Assert.IsFalse(couldDecrypt);
+
+                record = originalRecord;
+                record.SequenceNumber++;
+                couldDecrypt = recordProtection.DecryptCiphertextFromClient(plaintext, encrypted, ref record);
+                Assert.IsFalse(couldDecrypt);
+            }
+        }
+
+        [TestMethod]
+        public void ServerEncryptionCanoverlap()
+        {
+            using (Aes128GcmRecordProtection recordProtection = new Aes128GcmRecordProtection(this.masterSecret, this.serverRandom, this.clientRandom))
+            {
+                ByteSpan messageAsBytes = Encoding.UTF8.GetBytes(TestMessage);
+
+                Record record = new Record();
+                record.ContentType = ContentType.ApplicationData;
+                record.Epoch = 1;
+                record.SequenceNumber = 124;
+                record.Length = (ushort)recordProtection.GetEncryptedSize(messageAsBytes.Length);
+
+                ByteSpan encrypted = new byte[record.Length];
+                messageAsBytes.CopyTo(encrypted);
+                recordProtection.EncryptServerPlaintext(encrypted, encrypted.Slice(0, messageAsBytes.Length), ref record);
+
+                ByteSpan plaintext = encrypted.Slice(0, recordProtection.GetDecryptedSize(record.Length));
+                bool couldDecrypt = recordProtection.DecryptCiphertextFromServer(plaintext, encrypted, ref record);
+                Assert.IsTrue(couldDecrypt);
+                Assert.AreEqual(messageAsBytes.Length, plaintext.Length);
+                Assert.AreEqual(TestMessage, Encoding.UTF8.GetString(plaintext.GetUnderlyingArray(), plaintext.Offset, plaintext.Length));
+            }
+        }
+
+        [TestMethod]
+        public void ClientEncryptionCanoverlap()
+        {
+            using (Aes128GcmRecordProtection recordProtection = new Aes128GcmRecordProtection(this.masterSecret, this.serverRandom, this.clientRandom))
+            {
+                ByteSpan messageAsBytes = Encoding.UTF8.GetBytes(TestMessage);
+
+                Record record = new Record();
+                record.ContentType = ContentType.ApplicationData;
+                record.Epoch = 1;
+                record.SequenceNumber = 124;
+                record.Length = (ushort)recordProtection.GetEncryptedSize(messageAsBytes.Length);
+
+                ByteSpan encrypted = new byte[record.Length];
+                messageAsBytes.CopyTo(encrypted);
+                recordProtection.EncryptClientPlaintext(encrypted, encrypted.Slice(0, messageAsBytes.Length), ref record);
+
+                ByteSpan plaintext = encrypted.Slice(0, recordProtection.GetDecryptedSize(record.Length));
+                bool couldDecrypt = recordProtection.DecryptCiphertextFromClient(plaintext, encrypted, ref record);
+                Assert.IsTrue(couldDecrypt);
+                Assert.AreEqual(messageAsBytes.Length, plaintext.Length);
+                Assert.AreEqual(TestMessage, Encoding.UTF8.GetString(plaintext.GetUnderlyingArray(), plaintext.Offset, plaintext.Length));
+            }
+        }
+    }
+}
index b52e6e51c28246a2916c923b39785db0e3a3f7c7..b62a95f2a8c1bdbd84908cf23360ae5622bfa729 100644 (file)
@@ -59,6 +59,7 @@
     <Compile Include="BroadcastTests.cs" />
     <Compile Include="Crypto\AesGcmTest.cs" />
     <Compile Include="Crypto\X25519Tests.cs" />
+    <Compile Include="Dtls\AesGcmRecordProtectedTests.cs" />
     <Compile Include="Dtls\X25519EcdheRsaSha256Tests.cs" />
     <Compile Include="MessageReaderTests.cs" />
     <Compile Include="StatisticsTests.cs" />
diff --git a/Hazel/Dtls/AesGcmRecordProtection.cs b/Hazel/Dtls/AesGcmRecordProtection.cs
new file mode 100644 (file)
index 0000000..65df39e
--- /dev/null
@@ -0,0 +1,147 @@
+using Hazel.Crypto;
+using System;
+using System.Diagnostics;
+
+namespace Hazel.Dtls
+{
+    /// <summary>
+    /// *_AES_128_GCM_* cipher suite
+    /// </summary>
+    public class Aes128GcmRecordProtection: IRecordProtection
+    {
+        private const int ImplicitNonceSize = 4;
+        private const int ExplicitNonceSize = 8;
+
+        private readonly Aes128Gcm serverWriteCipher;
+        private readonly Aes128Gcm clientWriteCipher;
+
+        private readonly ByteSpan serverWriteIV;
+        private readonly ByteSpan clientWriteIV;
+
+        /// <summary>
+        /// Create a new instance of the AES128_GCM record protection
+        /// </summary>
+        /// <param name="masterSecret">Shared secret</param>
+        /// <param name="serverRandom">Server random data</param>
+        /// <param name="clientRandom">Client random data</param>
+        public Aes128GcmRecordProtection(ByteSpan masterSecret, ByteSpan serverRandom, ByteSpan clientRandom)
+        {
+            ByteSpan combinedRandom = new byte[serverRandom.Length + clientRandom.Length];
+            serverRandom.CopyTo(combinedRandom);
+            clientRandom.CopyTo(combinedRandom.Slice(serverRandom.Length));
+
+            // Expand master_secret to encryption keys
+            const int ExpandedSize = 0
+                + 0 // mac_key_length
+                + 0 // mac_key_length
+                + Aes128Gcm.KeySize // enc_key_length
+                + Aes128Gcm.KeySize // enc_key_length
+                + ImplicitNonceSize // fixed_iv_length
+                + ImplicitNonceSize // fixed_iv_length
+                ;
+
+            ByteSpan expandedKey = new byte[ExpandedSize];
+            PrfSha256.ExpandSecret(expandedKey, masterSecret, PrfLabel.KEY_EXPANSION, combinedRandom);
+
+            ByteSpan clientWriteKey = expandedKey.Slice(0, Aes128Gcm.KeySize);
+            ByteSpan serverWriteKey = expandedKey.Slice(Aes128Gcm.KeySize, Aes128Gcm.KeySize);
+            this.clientWriteIV = expandedKey.Slice(2 * Aes128Gcm.KeySize, ImplicitNonceSize);
+            this.serverWriteIV = expandedKey.Slice(2 * Aes128Gcm.KeySize + ImplicitNonceSize, ImplicitNonceSize);
+
+            this.serverWriteCipher = new Aes128Gcm(serverWriteKey);
+            this.clientWriteCipher = new Aes128Gcm(clientWriteKey);
+        }
+
+        /// <inheritdoc />
+        public void Dispose()
+        {
+            this.serverWriteCipher.Dispose();
+            this.clientWriteCipher.Dispose();
+        }
+
+        /// <inheritdoc />
+        private static int GetEncryptedSizeImpl(int dataSize)
+        {
+            return dataSize + Aes128Gcm.CiphertextOverhead;
+        }
+
+        /// <inheritdoc />
+        public int GetEncryptedSize(int dataSize)
+        {
+            return GetEncryptedSizeImpl(dataSize);
+        }
+
+        private static int GetDecryptedSizeImpl(int dataSize)
+        {
+            return dataSize - Aes128Gcm.CiphertextOverhead;
+        }
+
+        /// <inheritdoc />
+        public int GetDecryptedSize(int dataSize)
+        {
+            return GetDecryptedSizeImpl(dataSize);
+        }
+
+        /// <inheritdoc />
+        public void EncryptServerPlaintext(ByteSpan output, ByteSpan input, ref Record record)
+        {
+            EncryptPlaintext(output, input, ref record, this.serverWriteCipher, this.serverWriteIV);
+        }
+
+        /// <inheritdoc />
+        public void EncryptClientPlaintext(ByteSpan output, ByteSpan input, ref Record record)
+        {
+            EncryptPlaintext(output, input, ref record, this.clientWriteCipher, this.clientWriteIV);
+        }
+
+        private static void EncryptPlaintext(ByteSpan output, ByteSpan input, ref Record record, Aes128Gcm cipher, ByteSpan writeIV)
+        {
+            Debug.Assert(output.Length >= GetEncryptedSizeImpl(input.Length));
+
+            // Build GCM nonce (authenticated data)
+            ByteSpan nonce = new byte[ImplicitNonceSize + ExplicitNonceSize];
+            writeIV.CopyTo(nonce);
+            nonce.WriteBigEndian16(record.Epoch, ImplicitNonceSize);
+            nonce.WriteBigEndian48(record.SequenceNumber, ImplicitNonceSize + 2);
+
+            // Serialize record as additional data
+            Record plaintextRecord = record;
+            plaintextRecord.Length = (ushort)input.Length;
+            ByteSpan associatedData = new byte[Record.Size];
+            plaintextRecord.Encode(associatedData);
+
+            cipher.Seal(output, nonce, input, associatedData);
+        }
+
+        /// <inheritdoc />
+        public bool DecryptCiphertextFromServer(ByteSpan output, ByteSpan input, ref Record record)
+        {
+            return DecryptCiphertext(output, input, ref record, this.serverWriteCipher, this.serverWriteIV);
+        }
+
+        /// <inheritdoc />
+        public bool DecryptCiphertextFromClient(ByteSpan output, ByteSpan input, ref Record record)
+        {
+            return DecryptCiphertext(output, input, ref record, this.clientWriteCipher, this.clientWriteIV);
+        }
+
+        private static bool DecryptCiphertext(ByteSpan output, ByteSpan input, ref Record record, Aes128Gcm cipher, ByteSpan writeIV)
+        {
+            Debug.Assert(output.Length >= GetDecryptedSizeImpl(input.Length));
+
+            // Build GCM nonce (authenticated data)
+            ByteSpan nonce = new byte[ImplicitNonceSize + ExplicitNonceSize];
+            writeIV.CopyTo(nonce);
+            nonce.WriteBigEndian16(record.Epoch, ImplicitNonceSize);
+            nonce.WriteBigEndian48(record.SequenceNumber, ImplicitNonceSize + 2);
+
+            // Serialize record as additional data
+            Record plaintextRecord = record;
+            plaintextRecord.Length = (ushort)GetDecryptedSizeImpl(input.Length);
+            ByteSpan associatedData = new byte[Record.Size];
+            plaintextRecord.Encode(associatedData);
+
+            return cipher.Open(output, nonce, input, associatedData);
+        }
+    }
+}
index affb8a6b1e1bc64d61f93a1c7f981f40b6245593..04369bb56e68bf5849f25cec909d25f485d8558c 100644 (file)
@@ -67,7 +67,7 @@ namespace Hazel.Dtls
         public void Encode(ByteSpan span)
         {
             span[0] = (byte)this.ContentType;
-            span.WriteBigEndian16((ushort)ProtocolVersion.DTLS1_2);
+            span.WriteBigEndian16((ushort)ProtocolVersion.DTLS1_2, 1);
             span.WriteBigEndian16(this.Epoch, 3);
             span.WriteBigEndian48(this.SequenceNumber, 5);
             span.WriteBigEndian16(this.Length, 11);
index 0d176f3c0833a4bf57fa9368d21c5fd86e3d4fa2..18e7fa00a1dffd6e043b016927abf0edff28faaf 100644 (file)
@@ -78,6 +78,7 @@
     <Compile Include="Crypto\X25519.cs" />
     <Compile Include="DataReceivedEventArgs.cs" />
     <Compile Include="DisconnectedEventArgs.cs" />
+    <Compile Include="Dtls\AesGcmRecordProtection.cs" />
     <Compile Include="Dtls\Handshake.cs" />
     <Compile Include="Dtls\IHandshakeCipherSuite.cs" />
     <Compile Include="Dtls\IRecordProtection.cs" />