From 12ba5ced6bf28ded7aa92c131013d665907aeb05 Mon Sep 17 00:00:00 2001 From: Matthew Endsley Date: Fri, 18 Dec 2020 18:32:30 -0800 Subject: [PATCH] Add x25519 version of ECDHE_RSA_256 cipher suite This the portion of the cipher suite used during connection negotiation. --- .../Dtls/X25519EcdheRsaSha256Tests.cs | 226 ++++++++++++++++++ Hazel.UnitTests/Hazel.UnitTests.csproj | 1 + Hazel/Dtls/Handshake.cs | 20 ++ Hazel/Dtls/IHandshakeCipherSuite.cs | 5 +- Hazel/Dtls/X25519EcdheRsaSha256.cs | 182 ++++++++++++++ Hazel/Hazel.csproj | 2 + 6 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 Hazel.UnitTests/Dtls/X25519EcdheRsaSha256Tests.cs create mode 100644 Hazel/Dtls/Handshake.cs create mode 100644 Hazel/Dtls/X25519EcdheRsaSha256.cs diff --git a/Hazel.UnitTests/Dtls/X25519EcdheRsaSha256Tests.cs b/Hazel.UnitTests/Dtls/X25519EcdheRsaSha256Tests.cs new file mode 100644 index 0000000..33cd6dc --- /dev/null +++ b/Hazel.UnitTests/Dtls/X25519EcdheRsaSha256Tests.cs @@ -0,0 +1,226 @@ +using Hazel.Dtls; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Security.Cryptography; + +namespace Hazel.UnitTests.Dtls +{ + [TestClass] + public class X25519EcdheRsaSha256Tests + { + private readonly RandomNumberGenerator random = RandomNumberGenerator.Create(); + private readonly RSA privateKey = RSA.Create(); + private readonly RSA publicKey; + + public X25519EcdheRsaSha256Tests() + { + RSAParameters keyParameters = this.privateKey.ExportParameters(false); + this.publicKey = RSA.Create(); + this.publicKey.ImportParameters(keyParameters); + } + + [TestMethod] + public void SmallServerDataFails() + { + byte[] data; + + using (X25519EcdheRsaSha256 cipherSuite = new X25519EcdheRsaSha256(this.random)) + { + int expectedSize = cipherSuite.CalculateServerMessageSize(this.privateKey); + Assert.IsTrue(expectedSize/2 > 1); + + data = new byte[expectedSize/2]; + random.GetBytes(data); + } + + using (X25519EcdheRsaSha256 cipherSuite = new X25519EcdheRsaSha256(this.random)) + { + byte[] sharedKey = new byte[cipherSuite.SharedKeySize()]; + Assert.IsFalse(cipherSuite.VerifyServerMessageAndGenerateSharedKey(sharedKey, data, this.publicKey)); + } + } + + [TestMethod] + public void LargeServerDataFails() + { + byte[] data; + + using (X25519EcdheRsaSha256 cipherSuite = new X25519EcdheRsaSha256(this.random)) + { + int expectedSize = cipherSuite.CalculateServerMessageSize(this.privateKey); + Assert.IsTrue(expectedSize > 0); + + data = new byte[expectedSize * 2]; + random.GetBytes(data); + } + + using (X25519EcdheRsaSha256 cipherSuite = new X25519EcdheRsaSha256(this.random)) + { + byte[] sharedKey = new byte[cipherSuite.SharedKeySize()]; + Assert.IsFalse(cipherSuite.VerifyServerMessageAndGenerateSharedKey(sharedKey, data, this.publicKey)); + } + } + + [TestMethod] + public void RandomServerDataFails() + { + byte[] data; + + using (X25519EcdheRsaSha256 cipherSuite = new X25519EcdheRsaSha256(this.random)) + { + int expectedSize = cipherSuite.CalculateServerMessageSize(this.privateKey); + Assert.IsTrue(expectedSize > 0); + + data = new byte[expectedSize]; + random.GetBytes(data); + } + + using (X25519EcdheRsaSha256 cipherSuite = new X25519EcdheRsaSha256(this.random)) + { + byte[] sharedKey = new byte[cipherSuite.SharedKeySize()]; + Assert.IsFalse(cipherSuite.VerifyServerMessageAndGenerateSharedKey(sharedKey, data, this.publicKey)); + } + } + + [TestMethod] + public void SmallClientDataFails() + { + byte[] data; + + using (X25519EcdheRsaSha256 cipherSuite = new X25519EcdheRsaSha256(this.random)) + { + int expectedSize = cipherSuite.CalculateClientMessageSize(); + Assert.IsTrue(expectedSize / 2 > 1); + + data = new byte[expectedSize / 2]; + random.GetBytes(data); + } + + using (X25519EcdheRsaSha256 cipherSuite = new X25519EcdheRsaSha256(this.random)) + { + byte[] sharedKey = new byte[cipherSuite.SharedKeySize()]; + Assert.IsFalse(cipherSuite.VerifyClientMessageAndGenerateSharedKey(sharedKey, data)); + } + } + + [TestMethod] + public void LargeClientDataFails() + { + byte[] data; + + using (X25519EcdheRsaSha256 cipherSuite = new X25519EcdheRsaSha256(this.random)) + { + int expectedSize = cipherSuite.CalculateClientMessageSize(); + Assert.IsTrue(expectedSize > 0); + + data = new byte[expectedSize * 2]; + random.GetBytes(data); + } + + using (X25519EcdheRsaSha256 cipherSuite = new X25519EcdheRsaSha256(this.random)) + { + byte[] sharedKey = new byte[cipherSuite.SharedKeySize()]; + Assert.IsFalse(cipherSuite.VerifyClientMessageAndGenerateSharedKey(sharedKey, data)); + } + } + + [TestMethod] + public void RandomClientDataFails() + { + byte[] data; + + using (X25519EcdheRsaSha256 cipherSuite = new X25519EcdheRsaSha256(this.random)) + { + int expectedSize = cipherSuite.CalculateClientMessageSize(); + Assert.IsTrue(expectedSize > 0); + + data = new byte[expectedSize]; + random.GetBytes(data); + } + + using (X25519EcdheRsaSha256 cipherSuite = new X25519EcdheRsaSha256(this.random)) + { + byte[] sharedKey = new byte[cipherSuite.SharedKeySize()]; + Assert.IsFalse(cipherSuite.VerifyClientMessageAndGenerateSharedKey(sharedKey, data)); + } + } + + [TestMethod] + public void RandomSignatureFails() + { + byte[] data; + + using (X25519EcdheRsaSha256 cipherSuite = new X25519EcdheRsaSha256(this.random)) + { + int expectedSize = cipherSuite.CalculateServerMessageSize(this.privateKey); + Assert.IsTrue(expectedSize > 0); + + data = new byte[expectedSize]; + cipherSuite.EncodeServerKeyExchangeMessage(data, this.privateKey); + } + + // overwrite signature with random data + byte[] randomSignature = new byte[this.privateKey.KeySize/8]; + random.GetBytes(randomSignature); + new ByteSpan(randomSignature).CopyTo(new ByteSpan(data, data.Length - randomSignature.Length, randomSignature.Length)); + + using (X25519EcdheRsaSha256 cipherSuite = new X25519EcdheRsaSha256(this.random)) + { + byte[] sharedKey = new byte[cipherSuite.SharedKeySize()]; + Assert.IsFalse(cipherSuite.VerifyServerMessageAndGenerateSharedKey(sharedKey, data, this.publicKey)); + } + } + + [TestMethod] + public void VerifySignature() + { + byte[] data; + + using (X25519EcdheRsaSha256 cipherSuite = new X25519EcdheRsaSha256(this.random)) + { + int expectedSize = cipherSuite.CalculateServerMessageSize(this.privateKey); + Assert.IsTrue(expectedSize > 0); + + data = new byte[expectedSize]; + cipherSuite.EncodeServerKeyExchangeMessage(data, this.privateKey); + } + + using (X25519EcdheRsaSha256 cipherSuite = new X25519EcdheRsaSha256(this.random)) + { + byte[] sharedKey = new byte[cipherSuite.SharedKeySize()]; + Assert.IsTrue(cipherSuite.VerifyServerMessageAndGenerateSharedKey(sharedKey, data, this.publicKey)); + } + } + + [TestMethod] + public void GeneratesSameSharedKey() + { + byte[] serverSharedSecret; + byte[] clientSharedSecret; + + using (X25519EcdheRsaSha256 serverCipherSuite = new X25519EcdheRsaSha256(this.random)) + { + int expectedSize = serverCipherSuite.CalculateServerMessageSize(this.privateKey); + Assert.IsTrue(expectedSize > 0); + + byte[] serverKeyExchangeMessage = new byte[expectedSize]; + serverCipherSuite.EncodeServerKeyExchangeMessage(serverKeyExchangeMessage, this.privateKey); + + byte[] clientKeyExchange; + + using (X25519EcdheRsaSha256 clientCipherSuite = new X25519EcdheRsaSha256(this.random)) + { + clientSharedSecret = new byte[clientCipherSuite.SharedKeySize()]; + Assert.IsTrue(clientCipherSuite.VerifyServerMessageAndGenerateSharedKey(clientSharedSecret, serverKeyExchangeMessage, this.publicKey)); + + clientKeyExchange = new byte[clientCipherSuite.CalculateClientMessageSize()]; + clientCipherSuite.EncodeClientKeyExchangeMessage(clientKeyExchange); + } + + serverSharedSecret = new byte[serverCipherSuite.SharedKeySize()]; + Assert.IsTrue(serverCipherSuite.VerifyClientMessageAndGenerateSharedKey(serverSharedSecret, clientKeyExchange)); + } + + CollectionAssert.AreEqual(serverSharedSecret, clientSharedSecret); + } + } +} diff --git a/Hazel.UnitTests/Hazel.UnitTests.csproj b/Hazel.UnitTests/Hazel.UnitTests.csproj index 0a97afc..b52e6e5 100644 --- a/Hazel.UnitTests/Hazel.UnitTests.csproj +++ b/Hazel.UnitTests/Hazel.UnitTests.csproj @@ -59,6 +59,7 @@ + diff --git a/Hazel/Dtls/Handshake.cs b/Hazel/Dtls/Handshake.cs new file mode 100644 index 0000000..d9d8d0b --- /dev/null +++ b/Hazel/Dtls/Handshake.cs @@ -0,0 +1,20 @@ +namespace Hazel.Dtls +{ + /// + /// Named curves + /// + public enum NamedCurve : ushort + { + Reserved = 0, + secp256r1 = 23, + x25519 = 29, + } + + /// + /// Elliptic curve type + /// + public enum ECCurveType : byte + { + NamedCurve = 3, + } +} diff --git a/Hazel/Dtls/IHandshakeCipherSuite.cs b/Hazel/Dtls/IHandshakeCipherSuite.cs index 4428e85..eedd977 100644 --- a/Hazel/Dtls/IHandshakeCipherSuite.cs +++ b/Hazel/Dtls/IHandshakeCipherSuite.cs @@ -17,8 +17,11 @@ namespace Hazel.Dtls /// /// Calculate the size of the ServerKeyExchnage message /// + /// + /// Private key that will be used to sign the message + /// /// Size of the message in bytes - int CalculateServerMessageSize(); + int CalculateServerMessageSize(object privateKey); /// /// Encodes the ServerKeyExchange message diff --git a/Hazel/Dtls/X25519EcdheRsaSha256.cs b/Hazel/Dtls/X25519EcdheRsaSha256.cs new file mode 100644 index 0000000..505f2d2 --- /dev/null +++ b/Hazel/Dtls/X25519EcdheRsaSha256.cs @@ -0,0 +1,182 @@ +using Hazel.Crypto; +using System; +using System.Diagnostics; +using System.Security.Cryptography; + +namespace Hazel.Dtls +{ + /// + /// ECDHE_RSA_*_256 cipher suite + /// + public class X25519EcdheRsaSha256 : IHandshakeCipherSuite + { + private readonly ByteSpan privateAgreementKey; + private SHA256 sha256 = SHA256.Create(); + + /// + /// Create a new instance of the x25519 key exchange + /// + /// Random data source + public X25519EcdheRsaSha256(RandomNumberGenerator random) + { + byte[] buffer = new byte[X25519.KeySize]; + random.GetBytes(buffer); + this.privateAgreementKey = buffer; + } + + /// + public void Dispose() + { + this.sha256?.Dispose(); + this.sha256 = null; + } + + /// + public int SharedKeySize() + { + return X25519.KeySize; + } + + /// + /// Calculate the server message size given an RSA key size + /// + /// + /// Size of the private key (in bits) + /// + /// + /// Size of the ServerKeyExchange message in bytes + /// + private static int CalculateServerMessageSize(int keySize) + { + int signatureSize = keySize / 8; + + return 0 + + 1 // ECCurveType ServerKeyExchange.params.curve_params.curve_type + + 2 // NamedCurve ServerKeyExchange.params.curve_params.namedcurve + + 1 + X25519.KeySize // ECPoint ServerKeyExchange.params.public + + signatureSize // ServerKeyExchange.signed_params + ; + } + + /// + public int CalculateServerMessageSize(object privateKey) + { + RSA rsaPrivateKey = privateKey as RSA; + if (rsaPrivateKey == null) + { + throw new ArgumentException("Invalid private key", nameof(privateKey)); + } + + return CalculateServerMessageSize(rsaPrivateKey.KeySize); + } + + /// + public void EncodeServerKeyExchangeMessage(ByteSpan output, object privateKey) + { + RSA rsaPrivateKey = privateKey as RSA; + if (rsaPrivateKey == null) + { + throw new ArgumentException("Invalid private key", nameof(privateKey)); + } + + output[0] = (byte)ECCurveType.NamedCurve; + output.WriteBigEndian16((ushort)NamedCurve.x25519, 1); + output[3] = (byte)X25519.KeySize; + X25519.Func(output.Slice(4, X25519.KeySize), this.privateAgreementKey); + + // Hash the key parameters + byte[] paramterDigest = this.sha256.ComputeHash(output.GetUnderlyingArray(), output.Offset, 4 + X25519.KeySize); + + // Sign the paramter digest + RSAPKCS1SignatureFormatter signer = new RSAPKCS1SignatureFormatter(rsaPrivateKey); + signer.SetHashAlgorithm("SHA256"); + ByteSpan signature = signer.CreateSignature(paramterDigest); + + Debug.Assert(signature.Length == rsaPrivateKey.KeySize/8); + signature.CopyTo(output.Slice(4+X25519.KeySize)); + } + + /// + public bool VerifyServerMessageAndGenerateSharedKey(ByteSpan output, ByteSpan serverKeyExchangeMessage, object publicKey) + { + RSA rsaPublicKey = publicKey as RSA; + if (rsaPublicKey == null) + { + return false; + } + else if (output.Length != X25519.KeySize) + { + return false; + } + + // Verify message is compatible with this cipher suite + if (serverKeyExchangeMessage.Length != CalculateServerMessageSize(rsaPublicKey.KeySize)) + { + return false; + } + else if (serverKeyExchangeMessage[0] != (byte)ECCurveType.NamedCurve) + { + return false; + } + else if (serverKeyExchangeMessage.ReadBigEndian16(1) != (ushort)NamedCurve.x25519) + { + return false; + } + else if (serverKeyExchangeMessage[3] != X25519.KeySize) + { + return false; + } + + ByteSpan keyParameters = serverKeyExchangeMessage.Slice(0, 4+X25519.KeySize); + ByteSpan othersPublicKey = keyParameters.Slice(4); + ByteSpan signature = serverKeyExchangeMessage.Slice(keyParameters.Length); + + // Hash the key parameters + byte[] parameterDigest = this.sha256.ComputeHash(keyParameters.GetUnderlyingArray(), keyParameters.Offset, keyParameters.Length); + + // Verify the signature + RSAPKCS1SignatureDeformatter verifier = new RSAPKCS1SignatureDeformatter(rsaPublicKey); + verifier.SetHashAlgorithm("SHA256"); + if (!verifier.VerifySignature(parameterDigest, signature.ToArray())) + { + return false; + } + + // Signature has been validated, generate the shared key + return X25519.Func(output, this.privateAgreementKey, othersPublicKey); + } + + private static int ClientMessageSize = 0 + + 1 + X25519.KeySize // ECPoint ClientKeyExchange.ecdh_Yc + ; + + /// + public int CalculateClientMessageSize() + { + return ClientMessageSize; + } + + /// + public void EncodeClientKeyExchangeMessage(ByteSpan output) + { + output[0] = (byte)X25519.KeySize; + X25519.Func(output.Slice(1), this.privateAgreementKey); + } + + /// + public bool VerifyClientMessageAndGenerateSharedKey(ByteSpan output, ByteSpan clientKeyExchangeMessage) + { + if (clientKeyExchangeMessage.Length != ClientMessageSize) + { + return false; + } + else if (clientKeyExchangeMessage[0] != (byte)X25519.KeySize) + { + return false; + } + + ByteSpan othersPublicKey = clientKeyExchangeMessage.Slice(1); + return X25519.Func(output, this.privateAgreementKey, othersPublicKey); + } + } +} diff --git a/Hazel/Hazel.csproj b/Hazel/Hazel.csproj index ac4ed51..1f130f7 100644 --- a/Hazel/Hazel.csproj +++ b/Hazel/Hazel.csproj @@ -78,7 +78,9 @@ + + -- 2.39.5