From: Matthew Endsley Date: Wed, 13 Jan 2021 07:58:16 +0000 (-0800) Subject: Add AesGcm record protection X-Git-Tag: 1.0.0~20^2~24^2 X-Git-Url: https://git.deb.at/?a=commitdiff_plain;h=286352c1ab67f90d26021136439830bb5cd94331;p=rhonda%2Fimpostor.hazel.git Add AesGcm record protection --- diff --git a/Hazel.UnitTests/Dtls/AesGcmRecordProtectedTests.cs b/Hazel.UnitTests/Dtls/AesGcmRecordProtectedTests.cs new file mode 100644 index 0000000..941f849 --- /dev/null +++ b/Hazel.UnitTests/Dtls/AesGcmRecordProtectedTests.cs @@ -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)); + } + } + } +} diff --git a/Hazel.UnitTests/Hazel.UnitTests.csproj b/Hazel.UnitTests/Hazel.UnitTests.csproj index b52e6e5..b62a95f 100644 --- a/Hazel.UnitTests/Hazel.UnitTests.csproj +++ b/Hazel.UnitTests/Hazel.UnitTests.csproj @@ -59,6 +59,7 @@ + diff --git a/Hazel/Dtls/AesGcmRecordProtection.cs b/Hazel/Dtls/AesGcmRecordProtection.cs new file mode 100644 index 0000000..65df39e --- /dev/null +++ b/Hazel/Dtls/AesGcmRecordProtection.cs @@ -0,0 +1,147 @@ +using Hazel.Crypto; +using System; +using System.Diagnostics; + +namespace Hazel.Dtls +{ + /// + /// *_AES_128_GCM_* cipher suite + /// + 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; + + /// + /// Create a new instance of the AES128_GCM record protection + /// + /// Shared secret + /// Server random data + /// Client random data + 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); + } + + /// + public void Dispose() + { + this.serverWriteCipher.Dispose(); + this.clientWriteCipher.Dispose(); + } + + /// + private static int GetEncryptedSizeImpl(int dataSize) + { + return dataSize + Aes128Gcm.CiphertextOverhead; + } + + /// + public int GetEncryptedSize(int dataSize) + { + return GetEncryptedSizeImpl(dataSize); + } + + private static int GetDecryptedSizeImpl(int dataSize) + { + return dataSize - Aes128Gcm.CiphertextOverhead; + } + + /// + public int GetDecryptedSize(int dataSize) + { + return GetDecryptedSizeImpl(dataSize); + } + + /// + public void EncryptServerPlaintext(ByteSpan output, ByteSpan input, ref Record record) + { + EncryptPlaintext(output, input, ref record, this.serverWriteCipher, this.serverWriteIV); + } + + /// + 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); + } + + /// + public bool DecryptCiphertextFromServer(ByteSpan output, ByteSpan input, ref Record record) + { + return DecryptCiphertext(output, input, ref record, this.serverWriteCipher, this.serverWriteIV); + } + + /// + 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); + } + } +} diff --git a/Hazel/Dtls/Record.cs b/Hazel/Dtls/Record.cs index affb8a6..04369bb 100644 --- a/Hazel/Dtls/Record.cs +++ b/Hazel/Dtls/Record.cs @@ -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); diff --git a/Hazel/Hazel.csproj b/Hazel/Hazel.csproj index 0d176f3..18e7fa0 100644 --- a/Hazel/Hazel.csproj +++ b/Hazel/Hazel.csproj @@ -78,6 +78,7 @@ +