diff options
Diffstat (limited to 'Tools/Hazel-Networking/Hazel/Dtls/Handshake.cs')
-rw-r--r-- | Tools/Hazel-Networking/Hazel/Dtls/Handshake.cs | 734 |
1 files changed, 734 insertions, 0 deletions
diff --git a/Tools/Hazel-Networking/Hazel/Dtls/Handshake.cs b/Tools/Hazel-Networking/Hazel/Dtls/Handshake.cs new file mode 100644 index 0000000..f840053 --- /dev/null +++ b/Tools/Hazel-Networking/Hazel/Dtls/Handshake.cs @@ -0,0 +1,734 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace Hazel.Dtls +{ + /// <summary> + /// Handshake message type + /// </summary> + public enum HandshakeType : byte + { + HelloRequest = 0, + ClientHello = 1, + ServerHello = 2, + HelloVerifyRequest = 3, + Certificate = 11, + ServerKeyExchange = 12, + CertificateRequest = 13, + ServerHelloDone = 14, + CertificateVerify = 15, + ClientKeyExchange = 16, + Finished = 20, + } + + /// <summary> + /// List of cipher suites + /// </summary> + public enum CipherSuite + { + TLS_NULL_WITH_NULL_NULL = 0x0000, + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 = 0xC02F, + } + + /// <summary> + /// List of compression methods + /// </summary> + public enum CompressionMethod : byte + { + Null = 0, + } + + /// <summary> + /// Extension type + /// </summary> + public enum ExtensionType : ushort + { + EllipticCurves = 10, + } + + /// <summary> + /// Named curves + /// </summary> + public enum NamedCurve : ushort + { + Reserved = 0, + secp256r1 = 23, + x25519 = 29, + } + + /// <summary> + /// Elliptic curve type + /// </summary> + public enum ECCurveType : byte + { + NamedCurve = 3, + } + + /// <summary> + /// Hash algorithms + /// </summary> + public enum HashAlgorithm : byte + { + None = 0, + Sha256 = 4, + } + + /// <summary> + /// Signature algorithms + /// </summary> + public enum SignatureAlgorithm : byte + { + Anonymous = 0, + RSA = 1, + ECDSA = 3, + } + + /// <summary> + /// Random state for entropy + /// </summary> + public struct Random + { + public const int Size = 0 + + 4 // gmt_unix_time + + 28 // random_bytes + ; + } + + /// <summary> + /// Encode/decode handshake protocol header + /// </summary> + public struct Handshake + { + public HandshakeType MessageType; + public uint Length; + public ushort MessageSequence; + public uint FragmentOffset; + public uint FragmentLength; + + public const int Size = 12; + + /// <summary> + /// Parse a Handshake protocol header from wire format + /// </summary> + /// <returns>True if we successfully decode a handshake header. Otherwise false</returns> + public static bool Parse(out Handshake header, ByteSpan span) + { + header = new Handshake(); + + if (span.Length < Size) + { + return false; + } + + header.MessageType = (HandshakeType)span[0]; + header.Length = span.ReadBigEndian24(1); + header.MessageSequence = span.ReadBigEndian16(4); + header.FragmentOffset = span.ReadBigEndian24(6); + header.FragmentLength = span.ReadBigEndian24(9); + return true; + } + + /// <summary> + /// Encode the Handshake protocol header to wire format + /// </summary> + /// <param name="span"></param> + public void Encode(ByteSpan span) + { + span[0] = (byte)this.MessageType; + span.WriteBigEndian24(this.Length, 1); + span.WriteBigEndian16(this.MessageSequence, 4); + span.WriteBigEndian24(this.FragmentOffset, 6); + span.WriteBigEndian24(this.FragmentLength, 9); + } + } + + /// <summary> + /// Encode/decode ClientHello Handshake message + /// </summary> + public struct ClientHello + { + public ProtocolVersion ClientProtocolVersion; + public ByteSpan Random; + public ByteSpan Cookie; + public HazelDtlsSessionInfo Session; + public ByteSpan CipherSuites; + public ByteSpan SupportedCurves; + + public const int MinSize = 0 + + 2 // client_version + + Dtls.Random.Size // random + + 1 // session_id (size) + + 1 // cookie (size) + + 2 // cipher_suites (size) + + 1 // compression_methods (size) + + 1 // compression_method[0] (NULL) + + + 2 // extensions size + + + 0 // NamedCurveList extensions[0] + + 2 // extensions[0].extension_type + + 2 // extensions[0].extension_data (length) + + 2 // extensions[0].named_curve_list (size) + ; + + /// <summary> + /// Calculate the size in bytes required for the ClientHello payload + /// </summary> + /// <returns></returns> + public int CalculateSize() + { + return MinSize + + this.Session.PayloadSize + + this.Cookie.Length + + this.CipherSuites.Length + + this.SupportedCurves.Length + ; + } + + /// <summary> + /// Parse a Handshake ClientHello payload from wire format + /// </summary> + /// <returns>True if we successfully decode the ClientHello message. Otherwise false</returns> + public static bool Parse(out ClientHello result, ProtocolVersion? expectedProtocolVersion, ByteSpan span) + { + result = new ClientHello(); + if (span.Length < MinSize) + { + return false; + } + + result.ClientProtocolVersion = (ProtocolVersion)span.ReadBigEndian16(); + if (expectedProtocolVersion.HasValue && result.ClientProtocolVersion != expectedProtocolVersion.Value) + { + return false; + } + + span = span.Slice(2); + + result.Random = span.Slice(0, Dtls.Random.Size); + span = span.Slice(Dtls.Random.Size); + + if (!HazelDtlsSessionInfo.Parse(out result.Session, span)) + { + return false; + } + + span = span.Slice(result.Session.FullSize); + + byte cookieSize = span[0]; + if (span.Length < 1 + cookieSize) + { + return false; + } + result.Cookie = span.Slice(1, cookieSize); + span = span.Slice(1 + cookieSize); + + ushort cipherSuiteSize = span.ReadBigEndian16(); + if (span.Length < 2 + cipherSuiteSize) + { + return false; + } + else if (cipherSuiteSize % 2 != 0) + { + return false; + } + result.CipherSuites = span.Slice(2, cipherSuiteSize); + span = span.Slice(2 + cipherSuiteSize); + + int compressionMethodsSize = span[0]; + bool foundNullCompressionMethod = false; + for (int ii = 0; ii != compressionMethodsSize; ++ii) + { + if (span[1+ii] == (byte)CompressionMethod.Null) + { + foundNullCompressionMethod = true; + break; + } + } + + if (!foundNullCompressionMethod + || span.Length < 1 + compressionMethodsSize) + { + return false; + } + + span = span.Slice(1 + compressionMethodsSize); + + // Parse extensions + if (span.Length > 0) + { + if (span.Length < 2) + { + return false; + } + + ushort extensionsSize = span.ReadBigEndian16(); + span = span.Slice(2); + if (span.Length != extensionsSize) + { + return false; + } + + while (span.Length > 0) + { + // Parse extension header + if (span.Length < 4) + { + return false; + } + + ExtensionType extensionType = (ExtensionType)span.ReadBigEndian16(0); + ushort extensionLength = span.ReadBigEndian16(2); + + if (span.Length < 4 + extensionLength) + { + return false; + } + + ByteSpan extensionData = span.Slice(4, extensionLength); + span = span.Slice(4 + extensionLength); + result.ParseExtension(extensionType, extensionData); + } + } + + return true; + } + + /// <summary> + /// Decode a ClientHello extension + /// </summary> + /// <param name="extensionType">Extension type</param> + /// <param name="extensionData">Extension data</param> + private void ParseExtension(ExtensionType extensionType, ByteSpan extensionData) + { + switch (extensionType) + { + case ExtensionType.EllipticCurves: + if (extensionData.Length % 2 != 0) + { + break; + } + else if (extensionData.Length < 2) + { + break; + } + + ushort namedCurveSize = extensionData.ReadBigEndian16(0); + if (namedCurveSize % 2 != 0) + { + break; + } + + this.SupportedCurves = extensionData.Slice(2, namedCurveSize); + break; + } + } + + /// <summary> + /// Determines if the ClientHello message advertises support + /// for the specified cipher suite + /// </summary> + public bool ContainsCipherSuite(CipherSuite cipherSuite) + { + ByteSpan iterator = this.CipherSuites; + while (iterator.Length >= 2) + { + if (iterator.ReadBigEndian16() == (ushort)cipherSuite) + { + return true; + } + + iterator = iterator.Slice(2); + } + + return false; + } + + /// <summary> + /// Determines if the ClientHello message advertises support + /// for the specified curve + /// </summary> + public bool ContainsCurve(NamedCurve curve) + { + ByteSpan iterator = this.SupportedCurves; + while (iterator.Length >= 2) + { + if (iterator.ReadBigEndian16() == (ushort)curve) + { + return true; + } + + iterator = iterator.Slice(2); + } + + return false; + } + + /// <summary> + /// Encode Handshake ClientHello payload to wire format + /// </summary> + public void Encode(ByteSpan span) + { + span.WriteBigEndian16((ushort)this.ClientProtocolVersion); + span = span.Slice(2); + + Debug.Assert(this.Random.Length == Dtls.Random.Size); + this.Random.CopyTo(span); + span = span.Slice(Dtls.Random.Size); + + this.Session.Encode(span); + span = span.Slice(this.Session.FullSize); + + span[0] = (byte)this.Cookie.Length; + this.Cookie.CopyTo(span.Slice(1)); + span = span.Slice(1 + this.Cookie.Length); + + span.WriteBigEndian16((ushort)this.CipherSuites.Length); + this.CipherSuites.CopyTo(span.Slice(2)); + span = span.Slice(2 + this.CipherSuites.Length); + + span[0] = 1; + span[1] = (byte)CompressionMethod.Null; + span = span.Slice(2); + + // Extensions size + span.WriteBigEndian16((ushort)(6 + this.SupportedCurves.Length)); + span = span.Slice(2); + + // Supported curves extension + span.WriteBigEndian16((ushort)ExtensionType.EllipticCurves); + span.WriteBigEndian16((ushort)(2 + this.SupportedCurves.Length), 2); + span.WriteBigEndian16((ushort)this.SupportedCurves.Length, 4); + this.SupportedCurves.CopyTo(span.Slice(6)); + } + } + + /// <summary> + /// Encode/Decode session information in ClientHello + /// </summary> + public struct HazelDtlsSessionInfo + { + public const byte CurrentClientSessionSize = 1; + public const byte CurrentClientSessionVersion = 1; + + public byte FullSize => (byte)(1 + this.PayloadSize); + public byte PayloadSize; + public byte Version; + + public HazelDtlsSessionInfo(byte version) + { + this.Version = version; + switch (version) + { + case 0: // Does not write version byte + this.PayloadSize = 0; + return; + case 1: // Writes version byte only + this.PayloadSize = 1; + return; + } + + throw new ArgumentOutOfRangeException("Unimplemented Hazel session version"); + } + + public void Encode(ByteSpan writer) + { + writer[0] = this.PayloadSize; + + if (this.Version > 0) + { + writer[1] = this.Version; + } + } + + public static bool Parse(out HazelDtlsSessionInfo result, ByteSpan reader) + { + result = new HazelDtlsSessionInfo(); + if (reader.Length < 1) + { + return false; + } + + result.PayloadSize = reader[0]; + + // Back compat, length may be zero, version defaults to 0. + if (result.PayloadSize == 0) + { + result.Version = 0; + return true; + } + + // Forward compat, if length > 1, ignore the rest + result.Version = reader[1]; + return true; + } + } + + /// <summary> + /// Encode/decode Handshake HelloVerifyRequest message + /// </summary> + public struct HelloVerifyRequest + { + public const int CookieSize = 20; + public const int Size = 0 + + 2 // server_version + + 1 // cookie (size) + + CookieSize // cookie (data) + ; + + public ProtocolVersion ServerProtocolVersion; + public ByteSpan Cookie; + + /// <summary> + /// Parse a Handshake HelloVerifyRequest payload from wire + /// format + /// </summary> + /// <returns> + /// True if we successfully decode the HelloVerifyRequest + /// message. Otherwise false. + /// </returns> + public static bool Parse(out HelloVerifyRequest result, ProtocolVersion? expectedProtocolVersion, ByteSpan span) + { + result = new HelloVerifyRequest(); + if (span.Length < 3) + { + return false; + } + + result.ServerProtocolVersion = (ProtocolVersion)span.ReadBigEndian16(0); + if (expectedProtocolVersion.HasValue && result.ServerProtocolVersion != expectedProtocolVersion.Value) + { + return false; + } + + byte cookieSize = span[2]; + span = span.Slice(3); + + if (span.Length < cookieSize) + { + return false; + } + + result.Cookie = span; + return true; + } + + /// <summary> + /// Encode a HelloVerifyRequest payload to wire format + /// </summary> + /// <param name="peerAddress">Address of the remote peer</param> + /// <param name="hmac">Listener HMAC signature provider</param> + public static void Encode(ByteSpan span, EndPoint peerAddress, HMAC hmac, ProtocolVersion protocolVersion) + { + ByteSpan cookie = ComputeAddressMac(peerAddress, hmac); + + span.WriteBigEndian16((ushort)protocolVersion); + span[2] = (byte)CookieSize; + cookie.CopyTo(span.Slice(3)); + } + + /// <summary> + /// Generate an HMAC for a peer address + /// </summary> + /// <param name="peerAddress">Address of the remote peer</param> + /// <param name="hmac">Listener HMAC signature provider</param> + public static ByteSpan ComputeAddressMac(EndPoint peerAddress, HMAC hmac) + { + SocketAddress address = peerAddress.Serialize(); + byte[] data = new byte[address.Size]; + for (int ii = 0, nn = data.Length; ii != nn; ++ii) + { + data[ii] = address[ii]; + } + + ///NOTE(mendsley): Lame that we need to allocate+copy here + ByteSpan signature = hmac.ComputeHash(data); + return signature.Slice(0, CookieSize); + } + + /// <summary> + /// Verify a client's cookie was signed by our listener + /// </summary> + /// <param name="cookie">Wire format cookie</param> + /// <param name="peerAddress">Address of the remote peer</param> + /// <param name="hmac">Listener HMAC signature provider</param> + /// <returns>True if the cookie is valid. Otherwise false</returns> + public static bool VerifyCookie(ByteSpan cookie, EndPoint peerAddress, HMAC hmac) + { + if (cookie.Length != CookieSize) + { + return false; + } + + ByteSpan expectedHash = ComputeAddressMac(peerAddress, hmac); + if (expectedHash.Length != cookie.Length) + { + return false; + } + + return (1 == Crypto.Const.ConstantCompareSpans(cookie, expectedHash)); + } + } + + /// <summary> + /// Encode/decode Handshake ServerHello message + /// </summary> + public struct ServerHello + { + public ProtocolVersion ServerProtocolVersion; + public ByteSpan Random; + public CipherSuite CipherSuite; + public HazelDtlsSessionInfo Session; + + public const int MinSize = 0 + + 2 // server_version + + Dtls.Random.Size // random + + 1 // session_id (size) + + 2 // cipher_suite + + 1 // compression_method + ; + + public int Size => MinSize + Session.PayloadSize; + + /// <summary> + /// Parse a Handshake ServerHello payload from wire format + /// </summary> + /// <returns> + /// True if we successfully decode the ServerHello + /// message. Otherwise false. + /// </returns> + public static bool Parse(out ServerHello result, ByteSpan span) + { + result = new ServerHello(); + if (span.Length < MinSize) + { + return false; + } + + result.ServerProtocolVersion = (ProtocolVersion)span.ReadBigEndian16(); + span = span.Slice(2); + + result.Random = span.Slice(0, Dtls.Random.Size); + span = span.Slice(Dtls.Random.Size); + + if (!HazelDtlsSessionInfo.Parse(out result.Session, span)) + { + return false; + } + + span = span.Slice(result.Session.FullSize); + + result.CipherSuite = (CipherSuite)span.ReadBigEndian16(); + span = span.Slice(2); + + CompressionMethod compressionMethod = (CompressionMethod)span[0]; + if (compressionMethod != CompressionMethod.Null) + { + return false; + } + + return true; + } + + /// <summary> + /// Encode Handshake ServerHello to wire format + /// </summary> + public void Encode(ByteSpan span) + { + Debug.Assert(this.Random.Length == Dtls.Random.Size); + + span.WriteBigEndian16((ushort)this.ServerProtocolVersion, 0); + span = span.Slice(2); + + this.Random.CopyTo(span); + span = span.Slice(Dtls.Random.Size); + + this.Session.Encode(span); + span = span.Slice(this.Session.FullSize); + + span.WriteBigEndian16((ushort)this.CipherSuite); + span = span.Slice(2); + + span[0] = (byte)CompressionMethod.Null; + } + } + + /// <summary> + /// Encode/decode Handshake Certificate message + /// </summary> + public struct Certificate + { + /// <summary> + /// Encode a certificate to wire formate + /// </summary> + public static ByteSpan Encode(X509Certificate2 certificate) + { + ByteSpan certData = certificate.GetRawCertData(); + int totalSize = certData.Length + 3 + 3; + + ByteSpan result = new byte[totalSize]; + + ByteSpan writer = result; + writer.WriteBigEndian24((uint)certData.Length + 3); + writer = writer.Slice(3); + writer.WriteBigEndian24((uint)certData.Length); + writer = writer.Slice(3); + + certData.CopyTo(writer); + return result; + } + + /// <summary> + /// Parse a Handshake Certificate payload from wire format + /// </summary> + /// <returns>True if we successfully decode the Certificate message. Otherwise false</returns> + public static bool Parse(out X509Certificate2 certificate, ByteSpan span) + { + certificate = null; + if (span.Length < 6) + { + return false; + } + + uint totalSize = span.ReadBigEndian24(); + span = span.Slice(3); + + if (span.Length < totalSize) + { + return false; + } + + uint certificateSize = span.ReadBigEndian24(); + span = span.Slice(3); + if (span.Length < certificateSize) + { + return false; + } + + byte[] rawData = new byte[certificateSize]; + span.CopyTo(rawData, 0); + try + { + certificate = new X509Certificate2(rawData); + } + catch (Exception) + { + return false; + } + + return true; + } + } + + /// <summary> + /// Encode/decode Handshake Finished message + /// </summary> + public struct Finished + { + public const int Size = 12; + } +} |