--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+using Hazel.Crypto;
+using System;
+using System.Diagnostics;
+using System.Security.Cryptography;
+
+namespace Hazel.Dtls
+{
+ /// <summary>
+ /// ECDHE_RSA_*_256 cipher suite
+ /// </summary>
+ public class X25519EcdheRsaSha256 : IHandshakeCipherSuite
+ {
+ private readonly ByteSpan privateAgreementKey;
+ private SHA256 sha256 = SHA256.Create();
+
+ /// <summary>
+ /// Create a new instance of the x25519 key exchange
+ /// </summary>
+ /// <param name="random">Random data source</param>
+ public X25519EcdheRsaSha256(RandomNumberGenerator random)
+ {
+ byte[] buffer = new byte[X25519.KeySize];
+ random.GetBytes(buffer);
+ this.privateAgreementKey = buffer;
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ this.sha256?.Dispose();
+ this.sha256 = null;
+ }
+
+ /// <inheritdoc />
+ public int SharedKeySize()
+ {
+ return X25519.KeySize;
+ }
+
+ /// <summary>
+ /// Calculate the server message size given an RSA key size
+ /// </summary>
+ /// <param name="keySize">
+ /// Size of the private key (in bits)
+ /// </param>
+ /// <returns>
+ /// Size of the ServerKeyExchange message in bytes
+ /// </returns>
+ 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
+ ;
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ 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));
+ }
+
+ /// <inheritdoc />
+ 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
+ ;
+
+ /// <inheritdoc />
+ public int CalculateClientMessageSize()
+ {
+ return ClientMessageSize;
+ }
+
+ /// <inheritdoc />
+ public void EncodeClientKeyExchangeMessage(ByteSpan output)
+ {
+ output[0] = (byte)X25519.KeySize;
+ X25519.Func(output.Slice(1), this.privateAgreementKey);
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+ }
+}